web-dev-qa-db-ja.com

Doctrine Symfonyアプリケーションのエンティティとビジネスロジック

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

大きなSymfony2アプリケーションビジネスロジックを処理する私のDoctrine2エンティティの周りで問題が発生しました。 (ポストの長さで申し訳ありません)

多くのブログ、クックブック、その他のリソースを読んだ後、私はそれを見つけました:

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

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


簡単な例

ドメインモデル:

  • a Group使用できますRoles
  • a RoleはさまざまなGroupsで使用できます
  • a serは多数に属することができますGroups多数Rolesで、

[〜#〜] sql [〜#〜]永続化レイヤーでは、これらの関係を次のようにモデル化できます。

enter image description here

特定のビジネスルール:

  • serRolesGroupsに含めることができます。ただし、役割がグループにアタッチされている場合のみ
  • ロールR1グループG1から切り離す場合、グループ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();   
}
_
  • (+)クラスごと/ビジネスルールごとに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マネージャになりますか?

-可能な場合はリスナーを使用する

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-エンティティを拡張してリッチモデルを実装する

エンティティをドメインモデルクラスのサブ/親クラスとして使用し、ドメインロジックの多くをカプセル化します。しかし、このソリューションは私にとってより混乱しているようです。


あなたにとって、よりクリーンで分離されたテスト可能なコードに焦点を当てて、このビジネスロジックを管理する最良の方法は何ですか?あなたのフィードバックと優れた実践?具体的な例はありますか?

主なリソース:

56
Koryonik

私は解決策1)を、より長い視点から維持するのが最も簡単な方法だと考えています。ソリューション2は肥大化した「マネージャー」クラスを導き、最終的にはより小さなチャンクに分解されます。

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

「大きなアプリケーションのサービスクラスが多すぎる」ことは、SRPを回避する理由にはなりません。

ドメイン言語に関しては、次のコードが似ています。

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

そして

$group->removeRole($role);

また、あなたが説明したことから、グループからの役割の削除/追加には多くの依存関係(依存関係の逆転の原則)が必要であり、FAT /肥大化マネージャーでは難しい場合があります。

ソリューション3)は1)と非常によく似ています-各サブスクライバーは実際にはエンティティマネージャーによってバックグラウンドで自動的にトリガーされるサービスであり、より簡単なシナリオでは機能しますが、アクション(ロールの追加/削除)に多くのコンテキストが必要になるとすぐに問題が発生します例えば。どのユーザーがどのページから、またはその他のタイプの複雑な検証からアクションを実行したか。

4
Tomas Dermisek

ここを参照してください: 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で追加したり、オブジェクトで追加したりすることもできます。

しかし、これを「自然言語」で考えると...見てみましょう...

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

詳細をご覧ください:

  1. 私はアリスがユーザーid = 33で、写真家のグループに属していることを知っています。
  2. アリスを$user = $manager->getUserById( 33 );を介してUserManagerにリクエストします
  3. アリスを通じてグループPhotographersにアクセスします。おそらく、 `$ group = $ user-> getGroupByName( 'Photographers');を使用します。
  4. 次に、グループの役割を確認したいと思います。どうすればよいですか?
    • オプション1:$ group-> getRoles();
    • オプション2:$ group-> getRolesForUser($ userId);

2つ目は、アリスを介してグループを取得したため、冗長なものです。 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ドキュメントで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に参加したいときに禁止するルールセットを含めることができます。

その「ルール」は、ユーザー、グループ、およびロールのクラスの外に出て、ユーザーがグループに参加またはグループから離脱できる「ルール」を含むより高いレベルのクラスに離れることができます。

私は他の答えを見ることを強く勧めます。

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

シャビ。

5
Xavi Montero

個人的な好みとして、私は単純なものから始め、より多くのビジネスルールが適用されるにつれて成長することを好みます。そういうものとして、私はリスナーをより支持する傾向があります。

あなただけ

  • ビジネスルールが進化するにつれてリスナーを追加します
  • それぞれが単一の責任
  • また、これらのリスナーを個別に簡単にテストできます。

次のような単一のサービスクラスがある場合、多くのモック/スタブが必要になるもの:

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

        // feature you want to test.
        ...

        // some logic C.
        ...
    }
}
2
jorrel

business-awareエンティティに賛成です。 Doctrineは、インフラストラクチャの問題でモデルを汚染しないようにするための長い道のりです。リフレクションを使用しているため、アクセサを自由に変更できます。エンティティクラスに残る可能性がある2つの「Doctrine」の要素注釈(YMLマッピングのおかげで回避できます)、およびArrayCollectionです。これはDoctrine ORM(̀Doctrine/Common)、問題はありません。

したがって、DDDの基本に忠実に言えば、エンティティは実際にドメインロジックを配置する場所です。もちろん、これだけでは不十分な場合もあります。インフラストラクチャを気にすることなく、自由にdomain servicesを追加できます。

Doctrine repositoriesはより中立的です:エンティティをクエリする唯一の方法としてそれらを保持することを好む、それらが最初のリポジトリパターンに固執していない場合はイベントを生成し、生成されたメソッドを削除します。 managerサービスを追加して、特定のクラスのすべてのフェッチ/保存操作をカプセル化することは、数年前の一般的なSymfonyの慣行でしたが、私はあまり好きではありません。

私の経験では、Symfonyフォームコンポーネントではるかに多くの問題が発生する可能性があります。それを使用するかどうかはわかりません。彼らはあなたがコンストラクターをカスタマイズする能力を深刻に制限するでしょう、そしてあなたはむしろ名前付きコンストラクターを使うかもしれません。 PhpDocを追加する@deprecated̀タグは、ペアに、元のコンストラクターを訴えるべきではない視覚的なフィードバックを提供します。

最後に重要なことですが、Doctrineイベントに依存しすぎると、やがて噛み付くようになります。これらのイベントには技術的な制限が多すぎます。加えて、追跡するのが難しいと感じています。必要に応じて、- ドメインイベントコントローラー/コマンドからSymfonyイベントディスパッチャーにディスパッチされます。

0
romaricdrigon

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

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

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

0
Omar Alves