DAOを忘れ、リポジトリを使用

最近、データウェアハウスでの作業から抽象化できるパターンの違いについて考えました。 多くの場合、DAOとリポジトリの説明とさまざまな実装を表面的に読み、それらをプロジェクトに適用しましたが、明らかに概念的な違いを完全に理解していませんでした。 私はそれを理解し、Googleに身を潜め、私にとってすべてを明確にする記事を見つけました。 ロシア語に翻訳するといいと思いました。 英語の読者向けのオリジナルはこちら 。 興味のある方は誰でも猫を歓迎します。



データアクセスオブジェクト(DAO)は、ビジネスエリアオブジェクトをデータベースに格納するための広範なパターンです。 最も広い意味では、DAOは特定のエンティティのCRUDメソッドを含むクラスです。

次のクラスで表されるAccountエンティティがあるとします:

package com.thinkinginobjects.domainobject; public class Account { private String userName; private String firstName; private String lastName; private String email; private int age; public boolean hasUseName(String desiredUserName) { return this.userName.equals(desiredUserName); } public boolean ageBetween(int minAge, int maxAge) { return age >= minAge && age <= maxAge; } }
      
      





このエンティティのDAOインターフェイスを作成します。

 package com.thinkinginobjects.dao; import com.thinkinginobjects.domainobject.Account; public interface AccountDAO { Account get(String userName); void create(Account account); void update(Account account); void delete(String userName); }
      
      





AccountDAOインターフェースには、さまざまなORMフレームワークを使用したり、SQLデータベースクエリを直接実行したりできる多くの実装を含めることができます。

このパターンには次の利点があります。



ただし、このパターンでは多くの質問が未回答のままになります。 特定のlastNameを持つアカウントのリストを取得する必要がある場合はどうなりますか? アカウントのメールフィールドのみを更新するメソッドを追加できますか? 識別子としてuserNameの代わりに長いidを使用したい場合はどうなりますか? DAOの責任は正確に何ですか?

問題は、DAOの責任が明確に説明されていないことです。 ほとんどの人はDAOをデータベースへのゲートウェイとして提示し、データベースと通信する新しい方法を見つけたらすぐにメソッドを追加します。 したがって、次の例のように、DAOが肥大化することがよくあります。

 package com.thinkinginobjects.dao; import java.util.List; import com.thinkinginobjects.domainobject.Account; public interface BloatAccountDAO { Account get(String userName); void create(Account account); void update(Account account); void delete(String userName); List getAccountByLastName(String lastName); List getAccountByAgeRange(int minAge, int maxAge); void updateEmailAddress(String userName, String newEmailAddress); void updateFullName(String userName, String firstName, String lastName); }
      
      





BloatAccountDAOには、さまざまなパラメーターでアカウントを検索するメソッドが追加されています。 Accountクラスにさらに多くのフィールドとクエリを作成するさまざまな方法がある場合、さらに肥大化したDAOを取得できます。 その結果は次のようになります。



色をさらに太くするために、DAOに追加の更新メソッドを追加しました。 それらは、アカウントフィールドの異なるセットを更新する2つの新しい使用シナリオの出現の直接的な結果です。 これらは無害な最適化のように見え、インターフェイスをデータウェアハウスへのゲートウェイと見なす場合、AccountDAOの概念に完全に適合します。 DAOパターンとAccountDAOクラス名はあまりにも曖昧に定義されているため、このステップから離れることができません。

その結果、肥大化したDAOインターフェースが得られました。今後、同僚がさらに多くのメソッドを追加すると確信しています。 1年以内に、20以上のメソッドを持つクラスを作成し、このパターンを選択するために自分自身を呪います。



リポジトリパターン



より良い解決策は、リポジトリパターンを使用することです。 エリック・エヴァンスは本の中で正確な説明をしました。「リポジトリは、特定のタイプのすべてのオブジェクトを概念的なセットの形で表します。 その動作は、より高度なクエリ作成機能を除き、コレクションの動作に似ています。

戻って、この定義に従ってAccountRepositoryを設計しましょう。

 package com.thinkinginobjects.repository; import java.util.List; import com.thinkinginobjects.domainobject.Account; public interface AccountRepository { void addAccount(Account account); void removeAccount(Account account); void updateAccount(Account account); // Think it as replace for set List query(AccountSpecification specification); }
      
      





追加および更新メソッドは、AccountDAOメソッドと同じように見えます。 removeメソッドは、userName(アカウント識別子)の代わりにAccountをパラメーターとして取るという点で、DAOで定義された削除メソッドとは異なります。 リポジトリをコレクションとして表現すると、認識が変わります。 アカウント識別子タイプをリポジトリに公開することは避けます。 アカウントを識別するために長く使用したい場合、これはあなたの人生を楽にします。

追加/削除/更新メソッドのコントラクトについて考えている場合は、コレクションの抽象化について考えてください。 リポジトリに別の更新メソッドを追加することを検討している場合は、コレクションに別の更新メソッドを追加することが理にかなっているかどうかを検討してください。

ただし、クエリメソッドは特別です。 コレクションクラスにこのようなメソッドが表示されることは期待していません。 彼は何をしていますか?

クエリを作成する可能性を考慮すると、リポジトリはコレクションとは異なります。 メモリー内にオブジェクトのコレクションがあるため、そのすべての要素をソートして、興味のあるインスタンスを見つけるのは非常に簡単です。 リポジトリは、多くの場合、リクエスト時にRAMの外部に配置された多数のオブジェクトで機能します。 特定のユーザーが必要な場合、すべてのアカウントをメモリにロードすることは実用的ではありません。 代わりに、1つ以上のオブジェクトを見つけることができる基準をリポジトリに渡します。 リポジトリは、データベースをバックエンドとして使用する場合、SQLクエリを生成できます。コレクションがメモリで使用される場合、列挙によって必要なオブジェクトを見つけることができます。

一般的に使用される基準実装の1つは、仕様パターン(以下、仕様)です。 仕様は、ビジネスエリアオブジェクトを取り、ブール値を返す単純な述語です。

 package com.thinkinginobjects.repository; import com.thinkinginobjects.domainobject.Account; public interface AccountSpecification { boolean specified(Account account); }
      
      





したがって、AccountRepositoryへのクエリを実行する各メソッドの実装を作成できます。

通常の仕様はインメモリリポジトリに対してはうまく機能しますが、効率が悪いためデータベースでは使用できません。

SQLデータベースを操作するAccountRepositoryの場合、仕様はSqlSpecificationインターフェイスを実装する必要があります。

 package com.thinkinginobjects.repository; public interface SqlSpecification { String toSqlClauses(); }
      
      





データベースをバックエンドとして使用するリポジトリは、このインターフェイスを使用してSQLクエリパラメータを取得できます。 Hibernateがリポジトリのバックエンドとして使用された場合、Criteriaが生成するHibernateSpecificationインターフェイスを使用します。

SQLおよびHibernateリポジトリは、指定されたメソッドを使用しません。 それにもかかわらず、すべてのクラスでこのメソッドの実装が存在することには利点があります。 このようにして、リクエストをバックエンドに直接送信する前のリポジトリのキャッシュ実装と同様に、テスト目的でAccountRepositoryのスタブを使用できます。

さらに一歩進んで、SpicificationコンポジションをConjunctionSpecificationおよびDisjunctionSpecificationとともに使用して、より複雑なクエリを実行することもできます。 この問題は記事の範囲を超えているようです。 興味のある読者は、エヴァンスので詳細と例を見つけることができます。

 package com.thinkinginobjects.specification; import org.hibernate.criterion.Criterion; import org.hibernate.criterion.Restrictions; import com.thinkinginobjects.domainobject.Account; import com.thinkinginobjects.repository.AccountSpecification; import com.thinkinginobjects.repository.HibernateSpecification; public class AccountSpecificationByUserName implements AccountSpecification, HibernateSpecification { private String desiredUserName; public AccountSpecificationByUserName(String desiredUserName) { super(); this.desiredUserName = desiredUserName; } @Override public boolean specified(Account account) { return account.hasUseName(desiredUserName); } @Override public Criterion toCriteria() { return Restrictions.eq("userName", desiredUserName); } }
      
      







 package com.thinkinginobjects.specification; import com.thinkinginobjects.domainobject.Account; import com.thinkinginobjects.repository.AccountSpecification; import com.thinkinginobjects.repository.SqlSpecification; public class AccountSpecificationByAgeRange implements AccountSpecification, SqlSpecification{ private int minAge; private int maxAge; public AccountSpecificationByAgeRange(int minAge, int maxAge) { super(); this.minAge = minAge; this.maxAge = maxAge; } @Override public boolean specified(Account account) { return account.ageBetween(minAge, maxAge); } @Override public String toSqlClauses() { return String.format("age between %s and %s", minAge, maxAge); } }
      
      







おわりに



DAOパターンは、契約の曖昧な説明を提供します。 それを使用すると、クラスの実装が誤用され肥大化する可能性があります。 リポジトリパターンは、コレクションメタファーを使用して、緊密なコントラクトを提供し、コードの理解を容易にします。



All Articles