Symfony 애플리케이션의 교리 엔티티 및 비즈니스 로직
모든 아이디어 / 피드백을 환영합니다 :)
큰 Symfony2 응용 프로그램 에서 내 Doctrine2 엔터티 주변의 비즈니스 논리 를 처리 하는 방법에 문제가 있습니다 . (포스트 길이에 대해 죄송합니다)
많은 블로그, 요리 책 및 기타 리소스를 읽은 후 다음을 발견했습니다.
- 엔티티는 데이터 매핑 지속성 ( "빈혈 모델")에만 사용될 수 있습니다.
- 컨트롤러는 가능한 한 더 슬림해야합니다.
- 도메인 모델은 지속성 계층에서 분리되어야합니다 (엔티티는 엔티티 관리자를 알지 못함).
좋아, 나는 그것에 전적으로 동의하지만 : 도메인 모델에서 복잡한 비즈니스 규칙을 어디서 어떻게 처리합니까?
간단한 예
우리의 도메인 모델 :
- 그룹이 사용할 수있는 역할을
- 역할은 다른 사용할 수 있습니다 그룹
- 사용자가 많은에 속할 수 있습니다 그룹 많은과 역할 ,
A의 SQL의 지속성 계층, 우리는 이러한 관계로 modelize 수 :
당사의 특정 비즈니스 규칙 :
- 사용자 는 역할 이 그룹 에 연결된 경우에만 그룹 에서 역할 을 가질 수 있습니다 .
- 그룹 G1 에서 역할 R1 을 분리 하면 그룹 G1 및 역할 R1의 모든 UserRoleAffectation을 삭제해야합니다.
이것은 매우 간단한 예이지만 이러한 비즈니스 규칙을 관리하는 가장 좋은 방법을 알고 싶습니다.
찾은 솔루션
1- 서비스 계층에서 구현
특정 서비스 클래스를 다음과 같이 사용하십시오.
class GroupRoleAffectionService {
function linkRoleToGroup ($role, $group)
{
//...
}
function unlinkRoleToGroup ($role, $group)
{
//business logic to find all invalid UserRoleAffectation with these role and group
...
// BL to remove all found UserRoleAffectation OR to throw exception.
...
// detach role
$group->removeRole($role)
//save all handled entities;
$em->flush();
}
- (+) 클래스 당 / 비즈니스 규칙 당 하나의 서비스
- (-) API 엔티티가 도메인을 나타내지 않습니다
$group->removeRole($role)
.이 서비스에서 호출 할 수 있습니다. - (-) 큰 애플리케이션에 너무 많은 서비스 클래스가 있습니까?
2-도메인 엔터티 관리자의 구현
이러한 비즈니스 로직을 특정 "도메인 엔터티 관리자"에 캡슐화하고 모델 공급자라고도합니다.
class GroupManager {
function create($name){...}
function remove($group) {...}
function store($group){...}
// ...
function linkRole($group, $role) {...}
function unlinkRoleToGroup ($group, $role)
{
// ... (as in previous service code)
}
function otherBusinessRule($params) {...}
}
- (+) 모든 비즈니스 규칙이 중앙 집중화 됨
- (-) API 엔티티가 도메인을 나타내지 않습니다 : 서비스에서 $ group-> removeRole ($ role)을 호출 할 수 있습니다 ...
- (-) 도메인 관리자가 FAT 관리자가 되었습니까?
3-가능한 경우 리스너 사용
심포니 및 / 또는 Doctrine 이벤트 리스너 사용 :
class CheckUserRoleAffectationEventSubscriber implements EventSubscriber
{
// listen when a M2M relation between Group and Role is removed
public function getSubscribedEvents()
{
return array(
'preRemove'
);
}
public function preRemove(LifecycleEventArgs $event)
{
// BL here ...
}
4-엔터티를 확장하여 풍부한 모델 구현
많은 도메인 로직을 캡슐화하는 도메인 모델 클래스의 하위 / 상위 클래스로 엔티티를 사용합니다. 그러나이 솔루션은 저에게 더 혼란스러워 보입니다.
더 깔끔하고 분리 된 테스트 가능한 코드에 초점을 맞춰이 비즈니스 로직을 관리하는 가장 좋은 방법은 무엇입니까? 귀하의 피드백과 모범 사례? 구체적인 예가 있습니까?
주요 자원 :
- Symfony 관리 엔티티
- Symfony2 / Doctrine, 컨트롤러에 비즈니스 로직을 넣어야하나요? 그리고 복제 컨트롤러?
- 비즈니스 로직을 추가하기 위해 Doctrine Entity 확장
- http://iamproblematic.com/2012/03/12/putting-your-symfony2-controllers-on-a-diet-part-2/
- http://l3l0.eu/lang/en/2012/04/anemic-domain-model-problem-in-symfony2/
- https://leanpub.com/a-year-with-symfony
나는 해결책 1)이 더 긴 관점에서 유지하기 가장 쉬운 해결책이라고 생각합니다. 솔루션 2는 결국 더 작은 청크로 분할 될 부풀어 오른 "관리자"클래스를 이끌고 있습니다.
http://c2.com/cgi/wiki?DontNameClassesObjectManagerHandlerOrData
"대규모 애플리케이션에 너무 많은 서비스 클래스"는 SRP를 피할 이유가 아닙니다.
도메인 언어 측면에서 다음 코드가 비슷합니다.
$groupRoleService->removeRoleFromGroup($role, $group);
과
$group->removeRole($role);
또한 설명한 내용에서 그룹에서 역할을 제거 / 추가하려면 많은 종속성 (종속성 반전 원칙)이 필요하며 이는 FAT / 부풀린 관리자에게는 어려울 수 있습니다.
솔루션 3) 1)과 매우 유사 해 보입니다. 각 가입자는 실제로 Entity Manager에 의해 백그라운드에서 자동으로 트리거되며 더 간단한 시나리오에서는 작동 할 수 있지만 작업 (역할 추가 / 제거)에 많은 컨텍스트가 필요하면 문제가 발생합니다. 예. 작업을 수행 한 사용자, 페이지 또는 기타 복합 유효성 검사 유형
여기 참조 : Sf2 : 엔티티 내부에서 서비스 사용
아마도 여기 내 대답이 도움이 될 것입니다. 그것은 단지 그것을 다룹니다 : 모델 대 지속성 대 컨트롤러 계층을 "분리"하는 방법.
구체적인 질문에서 여기에 "트릭"이 있다고 말할 수 있습니다. "그룹"이 무엇입니까? "혼자"? 아니면 누군가와 관련이있을 때?
처음에 모델 클래스는 다음과 같을 수 있습니다.
UserManager (service, entry point for all others)
Users
User
Groups
Group
Roles
Role
UserManager는 모델 객체를 얻는 방법을 가질 것입니다 (그 대답에서 말했듯이 절대로해서는 안됩니다 new
). 컨트롤러에서 다음을 수행 할 수 있습니다.
$userManager = $this->get( 'myproject.user.manager' );
$user = $userManager->getUserById( 33 );
$user->whatever();
그러면 ... User
당신이 말했듯이, 역할을 할당 할 수 있는지 여부를 지정할 수 있습니다.
// Using metalanguage similar to C++ to show return datatypes.
User
{
// Role managing
Roles getAllRolesTheUserHasInAnyGroup();
void addRoleById( Id $roleId, Id $groupId );
void removeRoleById( Id $roleId );
// Group managing
Groups getGroups();
void addGroupById( Id $groupId );
void removeGroupById( Id $groupId );
}
물론 Id로 추가하거나 Object로 추가 할 수 있습니다.
하지만 이것을 "자연어"로 생각하면 ... 보자 ...
- 나는 앨리스가 사진사에 속한다는 것을 알고 있습니다.
- 나는 Alice 객체를 얻습니다.
- Alice에게 그룹에 대해 질문합니다. 나는 그룹 사진가를 얻습니다.
- 사진 작가에게 역할에 대해 질문합니다.
자세히보기 :
- 나는 Alice가 사용자 id = 33이고 그녀가 사진사 그룹에 있다는 것을 알고 있습니다.
- 나는 다음을 통해 Alice를 UserManager에게 요청합니다.
$user = $manager->getUserById( 33 );
- 나는 Alice를 통해 그룹 Photographers에 접속합니다. 아마도`$ group = $ user-> getGroupByName ( 'Photographers');
- 그런 다음 그룹의 역할을보고 싶습니다 ... 어떻게해야합니까?
- 옵션 1 : $ group-> getRoles ();
- 옵션 2 : $ group-> getRolesForUser ($ userId);
두 번째는 Alice를 통해 그룹을 얻었 기 때문에 중복과 같습니다. GroupSpecificToUser
에서 상속 하는 새 클래스 를 만들 수 있습니다 Group
.
게임과 비슷합니다 ... 게임이란 무엇입니까? 일반적으로 "체스"로 "게임"? 아니면 어제 시작했던 "체스"의 특정 "게임"?
In this case $user->getGroups()
would return a collection of GroupSpecificToUser objects.
GroupSpecificToUser extends Group
{
User getPointOfViewUser()
Roles getRoles()
}
This second approach will allow you to encapsulate there many other things that will appear sooner or later: Is this user allowed to do something here? you can just query the group subclass: $group->allowedToPost();
, $group->allowedToChangeName();
, $group->allowedToUploadImage();
, etc.
In any case, you can avoid creating taht weird class and just ask the user about this information, like a $user->getRolesForGroup( $groupId );
approach.
Model is not persistance layer
I like to 'forget' about the peristance when designing. I usually sit with my team (or with myself, for personal projects) and spend 4 or 6 hours just thinking before writing any line of code. We write an API in a txt doc. Then iterate on it adding, removing methods, etc.
A possible "starting point" API for your example could contain queries of anything, like a triangle:
User
getId()
getName()
getAllGroups() // Returns all the groups to which the user belongs.
getAllRoles() // Returns the list of roles the user has in any possible group.
getRolesOfACertainGroup( $group ) // Returns the list of groups for which the user has that specific role.
getGroupsOfRole( $role ) // Returns all the roles the user has in a specific group.
addRoleToGroup( $group, $role )
removeRoleFromGroup( $group, $role )
removeFromGroup() // Probably you want to remove the user from a group without having to loop over all the roles.
// removeRole() ?? // Maybe you want (or not) remove all admin privileges to this user, no care of what groups.
Group
getId()
getName()
getAllUsers()
getAllRoles()
getAllUsersWithRole( $role )
getAllRolesOfUser( $user )
addUserWithRole( $user, $role )
removeUserWithRole( $user, $role )
removeUser( $user ) // Probably you want to be able to remove a user completely instead of doing it role by role.
// removeRole( $role ) ?? // Probably you don't want to be able to remove all the roles at a time (say, remove all admins, and leave the group without any admin)
Roles
getId()
getName()
getAllUsers() // All users that have this role in one or another group.
getAllGroups() // All groups for which any user has this role.
getAllUsersForGroup( $group ) // All users that have this role in the given group.
getAllGroupsForUser( $user ) // All groups for which the given user is granted that role
// Querying redundantly is natural, but maybe "adding this user to this group"
// from the role object is a bit weird, and we already have the add group
// to the user and its redundant add user to group.
// Adding it to here maybe is too much.
Events
As said in the pointed article, I would also throw events in the model,
For example, when removing a role from a user in a group, I could detect in a "listener" that if that was the last administrator, I can a) cancel the deletion of the role, b) allow it and leave the group without administrator, c) allow it but choose a new admin from with the users in the group, etc or whatever policy is suitable for you.
The same way, maybe a user can only belong to 50 groups (as in LinkedIn). You can then just throw a preAddUserToGroup event and any catcher could contain the ruleset of forbidding that when the user wants to join group 51.
That "rule" can clearly leave outside the User, Group and Role class and leave in a higher level class that contains the "rules" by which users can join or leave groups.
I strongly suggest to see the other answer.
Hope to help!
Xavi.
As a personal preference, I like to start simple and grow as more business rules are applied. As such I tend to favour the listeners approach better.
You just
- add more listeners as business rules evolve,
- each having a single responsibility,
- and you can test these listeners independently easier.
Something that would require lots of mocks/stubs if you have a single service class such as:
class SomeService
{
function someMethod($argA, $argB)
{
// some logic A.
...
// some logic B.
...
// feature you want to test.
...
// some logic C.
...
}
}
I'm in favour of business-aware entities. Doctrine goes a long way not to pollute your model with infrastructure concerns ; it uses reflection so you are free to modify accessors as you want. The 2 "Doctrine" things that may remain in your entity classes are annotations (you can avoid thanks to YML mapping), and the ArrayCollection
. This is a library outside of Doctrine ORM (̀Doctrine/Common
), so no issues there.
So, sticking to the basics of DDD, entities are really the place to put your domain logic. Of course, sometimes this is not enough, then you are free to add domain services, services without infrastructure concerns.
Doctrine repositories are more middle-ground: I prefer to keep those as the only way to query for entities, event if they are not sticking to the initial repository pattern and I would rather remove the generated methods. Adding manager service to encapsulate all fetch/save operations of a given class was a common Symfony practice some years ago, I don't quite like it.
In my experience, you may come with far more issues with Symfony form component, I don't know if you use it. They will serisouly limit your ability to customize the constructor, then you may rather use named constructors. Adding PhpDoc @deprecated̀
tag wil give your pairs some visual feedback they should not sue the original constructor.
Last but not least, relying too much on Doctrine events will eventually bite you. They are too many technical limitations there, plus I find those hard to keep track of. When needed, I add domain events dispatched from the controller/command to Symfony event dispatcher.
I would consider using a service layer apart from the entities itself. Entities classes should describe the data structures and eventually some other simple calculations. Complex rules go to services.
As long you use services you can create more decoupled systems, services and so on. You can take the advantage of dependency injection and utilize events (dispatchers and listeners) to do the communication between the services keeping them weakly coupled.
나는 내 경험을 바탕으로 그렇게 말한다. 처음에는 모든 로직을 엔티티 클래스에 넣었습니다 (특히 심포니 1.x / doctrine 1.x 애플리케이션을 개발할 때). 애플리케이션이 성장하는 동안 유지 관리가 정말 어려워졌습니다.
'Nice programing' 카테고리의 다른 글
buildozer를 사용하여 Android 용 numpy 라이브러리로 Kivy 패키징 오류 (0) | 2021.01.08 |
---|---|
RecyclerView는 NestedScrollView 내에서 사용할 때 뷰를 재활용하지 않습니다. (0) | 2021.01.08 |
파일 업로드를위한 REST 디자인 (0) | 2021.01.08 |
Inkscape의 투명도 대 흰색 배경 (0) | 2021.01.08 |
로봇 시뮬레이터 (0) | 2021.01.08 |