62

アイデア/フィードバックは大歓迎です:)

大きなSymfony2 アプリケーションでDoctrine2 エンティティの周りのビジネス ロジックを処理する方法で問題が発生しました。(文章長くてすみません)

多くのブログ、クックブック、その他のリソースを読んだ後、次のことがわかりました。

  • エンティティは、データ マッピングの永続化 (「貧血モデル」) にのみ使用される場合があります。
  • コントローラーは可能な限りスリムにする必要があり、
  • ドメイン モデルは永続層から分離する必要があります (エンティティはエンティティ マネージャを認識しません)。

わかりました、私はそれに完全に同意し ますが、ドメインモデルの複雑なビジネスルールをどこでどのように処理しますか?


簡単な例

私たちのドメインモデル:

  • グループロールを使用できます
  • ロールは異なるグループで使用できます
  • ユーザーは、多くのロールを持つ多くのグループに所属できます。

SQL永続層では、これらの関係を次のようにモデル化できます。

ここに画像の説明を入力

私たちの特定のビジネスルール:

  • ロールがグループに関連付けられている場合にのみ、ユーザーはグループ内でロールを持つことができます。
  • グループ G1からロール R1を切り離す場合、グループ G1 とロール R1 を持つすべてのUserRoleAffectation を削除する必要があります。

これは非常に単純な例ですが、これらのビジネス ルールを管理するための最良の方法を知りたいです。


見つかった解決策

1- サービス層での実装

特定の Service クラスを次のように使用します。

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();   
}
  • (+) クラスごと/ビジネス ルールごとに 1 つのサービス
  • (-) 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 : エンティティ内でサービスを使用する

ここでの私の答えが役立つかもしれません。モデルと永続性とコントローラーレイヤーを「分離」する方法。

あなたの特定の質問では、ここに「トリック」があると思います...「グループ」とは何ですか? それは「一人」?それとも誰かに関係するときですか?

最初は、Model クラスはおそらく次のようになります。

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で追加したり、オブジェクトで追加したりできます。

でもこれを「自然言語」で考えてみると…

  1. 私はアリスが写真家に属していることを知っています。
  2. Alice オブジェクトを取得します。
  3. グループについて Alice に問い合わせます。グループ写真家を取得します。
  4. 役割についてカメラマンに問い合わせます。

詳細を参照してください。

  1. Alice はユーザー ID=33 で、写真家のグループに属していることはわかっています。
  2. 経由で UserManager に Alice をリクエストします$user = $manager->getUserById( 33 );
  3. Alice を介してグループ Photographers にアクセスします。
  4. グループの役割を確認したいのですが…どうすればいいですか?
    • オプション 1: $group->getRoles();
    • オプション 2: $group->getRolesForUser( $userId );

2 つ目は、Alice を介してグループを取得したため、冗長です。GroupSpecificToUserを継承した新しいクラスを作成できますGroup

ゲームに似ている... ゲームとは?一般的に「チェス」としての「ゲーム」?それとも、あなたと私が昨日始めた「チェス」の特定の「ゲーム」ですか?

この場合$user->getGroups()、GroupSpecificToUser オブジェクトのコレクションを返します。

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

この 2 番目のアプローチにより、遅かれ早かれ現れる他の多くのことをそこにカプセル化することができます: このユーザーはここで何かをすることを許可されていますか? グループのサブクラスを照会するだけです: $group->allowedToPost();$group->allowedToChangeName();$group->allowedToUploadImage();など。

いずれにせよ、奇妙なクラスを作成することを避け、$user->getRolesForGroup( $groupId );アプローチのように、この情報についてユーザーに尋ねるだけです。

モデルは永続層ではありません

私は、デザインするときは、環境を「忘れる」のが好きです。私は通常、チーム (または個人的なプロジェクトの場合は自分自身) と一緒に座って、コード行を書く前に考えるだけで 4 ~ 6 時間を費やします。txt doc に 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)許可しますが、グループ内のユーザーなどから新しい管理者を選択するか、適切なポリシーを選択してください。

同じように、ユーザーは (LinkedIn のように) 50 個のグループにしか所属できない場合があります。その後、preAddUserToGroup イベントをスローするだけで、ユーザーがグループ 51 に参加することを禁止するルールセットを任意のキャッチャーに含めることができます。

その「ルール」は、明らかにユーザー、グループ、およびロール クラスの外に出て、ユーザーがグループに参加またはグループから退出できる「ルール」を含む上位レベルのクラスに入ることができます。

他の回答を見ることを強くお勧めします。

お役に立てれば幸いです。

シャビ。

于 2014-08-27T18:59:37.300 に答える
1

エンティティ自体とは別にサービス層を使用することを検討します。エンティティ クラスは、データ構造を記述し、最終的にはその他の単純な計算を記述する必要があります。複雑なルールはサービスに適用されます。

サービスを使用している限り、より分離されたシステムやサービスなどを作成できます。依存性注入を利用し、イベント (ディスパッチャーとリスナー) を利用して、サービス間の通信を行い、それらを弱結合に保つことができます。

私自身の経験に基づいて言っています。最初は、すべてのロジックをエンティティ クラス内に配置していました (特に、symfony 1.x/doctrine 1.x アプリケーションを開発したとき)。アプリケーションが成長するにつれて、維持するのが非常に難しくなりました。

于 2017-11-10T17:18:00.203 に答える