62

欢迎任何想法/反馈:)

我遇到了如何在大型Symfony2 应用程序中处理围绕我的Doctrine2 实体的业务逻辑的问题。(对不起,帖子长度)

在阅读了许多博客、食谱和其他资源后,我发现:

  • 实体可能仅用于数据映射持久性(“贫血模型”),
  • 控制器必须尽可能纤薄,
  • 领域模型必须与持久层解耦(实体不知道实体管理器)

好的,我完全同意它,但是: 在哪里以及如何处理域模型上的复杂业务规则?


一个简单的例子

我们的领域模型:

  • 可以使用角色
  • 一个角色可以被不同的组使用
  • 一个用户可以属于许多具有许多角色的

SQL持久层中,我们可以将这些关系建模为:

在此处输入图像描述

我们的具体业务规则:

  • 仅当角色附加到组时,用户才能在组中拥有角色
  • 如果我们从组 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 - 尽可能使用监听器

使用 symfony 和/或 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 - 通过扩展实体实现富模型

使用实体作为领域模型类的子/父类,它封装了许多领域逻辑。但是这个解决方案对我来说似乎更困惑。


对您而言,管理此业务逻辑的最佳方式是什么,专注于更干净、解耦、可测试的代码?您的反馈和良好做法?你有具体的例子吗?

主要资源:

4

5 回答 5

6

见这里:Sf2:在实体内使用服务

也许我在这里的回答会有所帮助。它只是解决了这个问题:如何“解耦”模型与持久性与控制器层。

在您的具体问题中,我想说这里有一个“技巧”......什么是“组”?它“单独”?或者当它与某人有关时?

最初,您的模型类可能如下所示:

UserManager (service, entry point for all others)

Users
User
Groups
Group
Roles
Role

UserManager 将具有获取模型对象的方法(如该答案中所述,您永远不应该做 a 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 添加等。

但是当你用“自然语言”来思考这个时......让我们看看......

  1. 我知道爱丽丝属于摄影师。
  2. 我得到爱丽丝对象。
  3. 我向爱丽丝询问有关这些组的信息。我得到了组摄影师。
  4. 我向摄影师询问角色。

详细查看:

  1. 我知道 Alice 的用户 id=33 并且她在摄影师组中。
  2. 我通过请求 Alice 到 UserManager$user = $manager->getUserById( 33 );
  3. 我通过 Alice 访问摄影师组,可能使用 `$group = $user->getGroupByName('Photographers');
  4. 然后我想看看小组的角色......我该怎么办?
    • 选项 1:$group->getRoles();
    • 选项 2: $group->getRolesForUser( $userId );

第二个是多余的,因为我通过 Alice 得到了这个组。您可以创建一个GroupSpecificToUser继承自Group.

类似于游戏……什么是游戏?把“游戏”当成“棋”一般?还是你我昨天开始的“棋”的具体“游戏”?

在这种情况下$user->getGroups(),将返回 GroupSpecificToUser 对象的集合。

GroupSpecificToUser extends Group
{
    User getPointOfViewUser()
    Roles getRoles()
}

第二种方法将允许您封装许多其他迟早会出现的东西:这个用户是否允许在这里做某事?您可以只查询组子类:$group->allowedToPost();、、、$group->allowedToChangeName();$group->allowedToUploadImage();

在任何情况下,您都可以避免创建奇怪的类,而只需向用户询问这些信息,就像一种$user->getRolesForGroup( $groupId );方法一样。

模型不是持久层

我喜欢在设计时“忘记”持久性。我通常和我的团队(或我自己,对于个人项目)坐在一起,在编写任何代码之前花 4 或 6 个小时思考。我们在 txt 文档中编写 API。然后迭代它添加,删除方法等。

您的示例可能的“起点”API 可以包含任何查询,例如三角形:

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.

活动

正如指出的文章中所说,我也会在模型中抛出事件,

例如,当从组中的用户删除角色时,我可以在“侦听器”中检测到,如果那是最后一个管理员,我可以 a) 取消删除该角色,b) 允许它并离开该组而不管理员,c)允许它,但从组中的用户中选择一个新管理员,等等或任何适合您的策略。

同样的,也许一个用户只能属于 50 个组(如 LinkedIn)。然后,您只需抛出一个 preAddUserToGroup 事件,任何捕手都可以包含在用户想要加入组 51 时禁止该事件的规则集。

该“规则”可以清楚地离开用户、组和角色类,并留在包含用户可以加入或离开组的“规则”的更高级别的类中。

我强烈建议查看其他答案。

希望有所帮助!

哈维。

于 2014-08-27T18:59:37.300 回答
5

我发现解决方案 1) 从长远来看是最容易维护的解决方案。解决方案 2 导致臃肿的“经理”类,最终将被分解成更小的块。

http://c2.com/cgi/wiki?DontNameClassesObjectManagerHandlerOrData

“大型应用程序中有太多的服务类”不是避免 SRP 的理由。

在领域语言方面,我发现以下代码类似:

$groupRoleService->removeRoleFromGroup($role, $group);

$group->removeRole($role);

同样根据您的描述,从组中删除/添加角色需要许多依赖项(依赖倒置原则),而这对于 FAT/臃肿的管理器可能会很困难。

解决方案 3) 看起来与 1) 非常相似 - 每个订阅者实际上都是由实体管理器在后台自动触发的服务,在更简单的场景中它可以工作,但是一旦操作(添加/删除角色)需要大量上下文就会出现问题例如。哪个用户执行了操作,从哪个页面或任何其他类型的复杂验证。

于 2013-10-08T03:52:00.833 回答
3

我赞成具有商业意识的实体。Doctrine 在很大程度上不会因为基础设施问题而污染您的模型;它使用反射,因此您可以随意修改访问器。实体类中可能保留的 2 个“原则”是注解(由于 YML 或 XML 映射,您可以避免使用它们),以及ArrayCollection. 这是 Doctrine ORM ( Doctrine/Commoǹ),所以没有问题。

因此,坚持 DDD 的基础知识,实体确实是放置域逻辑的地方。当然,有时这还不够,那么您可以自由添加域服务,无需担心基础设施的服务。

Doctrine存储库更多的是中间立场:我更愿意将它们作为查询实体的唯一方法,如果它们不遵守初始存储库模式,我宁愿删除生成的方法。添加管理器服务来封装给定类的所有获取/保存操作是几年前 Symfony 的常见做法,我不太喜欢它。

根据我的经验,您可能会遇到更多 Symfony 表单组件的问题,我不知道您是否使用它。它们会严重限制您自定义构造函数的能力;那么您可能宁愿使用命名构造函数。添加 PhpDoc@deprecated̀注释将为您的配对提供一些视觉反馈,即他们不应使用原始构造函数。

最后但同样重要的是,过度依赖教义事件最终会咬你一口。它们有太多的技术限制,而且我发现那些很难跟踪。需要时,我将从控制器/命令分派的域事件添加到 Symfony 事件分派器。

于 2016-06-24T14:13:33.720 回答
1

作为个人喜好,我喜欢从简单的开始,随着更多业务规则的应用而成长。因此,我倾向于更好地支持听众的方法

你刚才

  • 随着业务规则的发展添加更多的侦听器,
  • 每个人都有一个单一的责任
  • 您可以更轻松地独立测试这些侦听器。

如果您有一个服务类,则需要大量模拟/存根,例如:

class SomeService 
{
    function someMethod($argA, $argB)
    {
        // some logic A.
        ... 
        // some logic B.
        ...

        // feature you want to test.
        ...

        // some logic C.
        ...
    }
}
于 2013-10-08T03:54:12.707 回答
1

我会考虑使用实体本身之外的服务层。实体类应该描述数据结构并最终描述一些其他简单的计算。复杂的规则用于服务。

只要你使用服务,你就可以创建更多解耦的系统、服务等等。您可以利用依赖注入并利用事件(调度程序和侦听器)在服务之间进行通信,以保持它们的弱耦合。

我是根据我自己的经验这么说的。一开始我把所有的逻辑都放在实体类中(特别是当我开发 symfony 1.x/doctrine 1.x 应用程序时)。只要应用程序增长,它们就很难维护。

于 2017-11-10T17:18:00.203 回答