DDDリポジトリの一般的な実装は、オブジェクト指向ではありません。たとえば、save()
メソッド:
package com.example.domain;
public class Product { /* public attributes for brevity */
public String name;
public Double price;
}
public interface ProductRepo {
void save(Product product);
}
インフラ部分:
package com.example.infrastructure;
// imports...
public class JdbcProductRepo implements ProductRepo {
private JdbcTemplate = ...
public void save(Product product) {
JdbcTemplate.update("INSERT INTO product (name, price) VALUES (?, ?)",
product.name, product.price);
}
}
そのようなインターフェースは、少なくともゲッターに関してはProduct
が貧血モデルであることを期待しています。
一方、OOPは、Product
オブジェクトがそれ自体を保存する方法を知っている必要があることを示しています。
package com.example.domain;
public class Product {
private String name;
private Double price;
void save() {
// save the product
// ???
}
}
問題は、Product
が自分自身を保存する方法を知っている場合、インフラストラクチャコードがドメインコードから分離されていないことを意味します。
たぶん、保存を別のオブジェクトに委任できます。
package com.example.domain;
public class Product {
private String name;
private Double price;
void save(Storage storage) {
storage
.with("name", this.name)
.with("price", this.price)
.save();
}
}
public interface Storage {
Storage with(String name, Object value);
void save();
}
インフラ部分:
package com.example.infrastructure;
// imports...
public class JdbcProductRepo implements ProductRepo {
public void save(Product product) {
product.save(new JdbcStorage());
}
}
class JdbcStorage implements Storage {
private final JdbcTemplate = ...
private final Map<String, Object> attrs = new HashMap<>();
private final String tableName;
public JdbcStorage(String tableName) {
this.tableName = tableName;
}
public Storage with(String name, Object value) {
attrs.put(name, value);
}
public void save() {
JdbcTemplate.update("INSERT INTO " + tableName + " (name, price) VALUES (?, ?)",
attrs.get("name"), attrs.get("price"));
}
}
これを達成するための最良のアプローチは何ですか?オブジェクト指向のリポジトリを実装することは可能ですか?
あなたが書いた
一方、OOPは、Productオブジェクトはそれ自体を保存する方法を知っている必要があると述べています
そしてコメントで。
...それを使って行われるすべての操作を担当する必要があります
これはよくある誤解です。 Product
はドメインオブジェクトであるため、domain操作に責任があり、singleproduct object、no less、no more-so not for foralloperations。通常、永続化はドメイン操作とは見なされません。まったく反対に、エンタープライズアプリケーションでは、ドメインモデルで永続性の無知を(少なくともある程度)達成しようとすることは珍しくなく、永続化メカニズムを別のリポジトリクラスに保持することは、このための一般的なソリューションです。 「DDD」はこのような用途を目指した技術です。
では、Product
の賢明なドメイン操作は何でしょうか?これは、実際にはアプリケーションシステムのドメインコンテキストに依存します。システムが小さく、CRUD操作のみをサポートしている場合、実際には、Product
は、例のようにかなり「貧弱」なままになる可能性があります。このような種類のアプリケーションでは、データベース操作を別のrepoクラスに配置するか、DDDを使用するだけでも面倒な場合は、議論の余地があるかもしれません。
ただし、アプリケーションが製品の売買、在庫の管理、税金の計算などの実際のビジネスオペレーションをサポートするとすぐに、Product
クラスに適切に配置できるオペレーションを見つけ始めるのはごく一般的なことです。 。たとえば、ボリュームディスカウントを考慮に入れると、特定の製品の「n個のアイテム」の価格を計算する操作CalcTotalPrice(int noOfItems)
がある場合があります。
つまり、要するに、クラスを設計するときは、コンテキストについて考える必要があります Joel Spolskyの5つの世界 であり、システムに十分なドメインロジックが含まれている場合、DDDは有益です。答えが「はい」の場合、永続性のメカニズムをドメインクラスから除外しているというだけの理由で、貧血モデルに終わる可能性はほとんどありません。
理論を切り捨てる練習。
経験から、Product.Save()は多くの問題を引き起こすことがわかっています。これらの問題を回避するために、リポジトリパターンを考案しました。
確かに、OOP商品データを非表示にするルールに違反していますが、うまく機能します。
例外のある一般的なルールを作成するよりも、すべてをカバーする一貫したルールのセットを作成する方がはるかに困難です。
DDDはOOPを満たしています
これらの2つのアイデアの間に緊張を意図するものではないことに注意してください。値オブジェクト、集計、リポジトリは、使用されるパターンの配列ですOOP正しく行われている。
一方、OOPは、Productオブジェクトがそれ自体を保存する方法を知っている必要があることを示しています。
そうではありません。オブジェクトは、独自のデータ構造をカプセル化します。製品のメモリ内の表現は、製品の動作(それらが何であれ)を示す責任があります。しかし、永続的なストレージはそこ(リポジトリの背後)にあり、独自の作業を行う必要があります。
データベースのメモリ内表現と永続化されたメモリの間でdataをコピーする方法が必要です。 境界で 、物事はかなり原始的になる傾向があります。
基本的に、書き込み専用データベースは特に有用ではなく、同等のメモリ内メモリは「永続的」ソートよりも有用ではありません。情報を取り出そうとしないのであれば、Product
オブジェクトに情報を入れても意味がありません。必ずしも「ゲッター」を使用する必要はありません。製品データ構造を共有しようとしているのではなく、製品の内部表現への可変アクセスを共有してはいけません。
たぶん、保存を別のオブジェクトに委任できます。
それは確かに機能します-永続ストレージは事実上コールバックになります。私はおそらくインターフェースをより単純にするでしょう:
interface ProductStorage {
onProduct(String name, double price);
}
goingは、メモリ内の表現とストレージメカニズムを結びつけるものです。これは、情報がここからそこに(そして再び)戻る必要があるためです。共有する情報を変更すると、会話の両端に影響が及びます。したがって、可能な場合は明示的にすることもできます。
このアプローチ-コールバックを介したデータの受け渡しは TDDのモック の開発に重要な役割を果たしました。
情報をコールバックに渡すことは、クエリから情報を返すことと同じ制限をすべて持っていることに注意してください-データ構造の可変コピーを渡してはいけません。
このアプローチは、EvansがBlue Bookで説明しているものとは少し異なります。クエリを介してデータを返すことが通常の方法であり、ドメインオブジェクトは「永続性の懸念」が混ざらないように特別に設計されています。
私はDDDをOOPテクニックとして理解しているので、矛盾しているように見えることを完全に理解したいと思います。
覚えておくべきことの1つ-ブルーブックは15年前にJava 1.4が地球を歩き回ったときに書かれました。特に、この本はJavagenerics-エバンスが彼のアイデアを開発していた当時、私たちは今より多くのテクニックを利用できます。
たぶん、保存を別のオブジェクトに委任できます
分野の知識を不必要に広めないでください。個々のフィールドについて知っていることが多いほど、フィールドの追加や削除が難しくなります。
public class Product {
private String name;
private Double price;
void save(Storage storage) {
storage.save( toString() );
}
}
ここでは、ログファイルまたはデータベース、あるいはその両方に保存しているのかどうかはわかりません。ここで、saveメソッドは、4または40のフィールドがあるかどうかはわかりません。それは疎結合です。それは良いことです。
もちろん、これはこの目標を達成する方法の1つの例にすぎません。 DTOとして使用する文字列の作成と解析が嫌いな場合は、コレクションを使用することもできます。 LinkedHashMap
は、順序を維持し、toString()がログファイルで見栄えが良いので、昔からのお気に入りです。
どんなことをしても、分野の知識を広めないでください。これは、遅くなるまで人々がしばしば無視する結合の形式です。オブジェクトが持つフィールドの数をできるだけ静的に把握したいのですが。この方法では、フィールドを追加しても、多くの場所で多くの編集を行う必要がありません。
非常に良い観察です。私はそれらについて完全に同意します。これがまさにこの主題に関する私の訂正です(訂正:スライドのみ) オブジェクト指向ドメイン駆動設計 。
短い答え:いいえ。純粋に技術的であり、ドメインに関連性のないオブジェクトをアプリケーションに含めることはできません。これは、会計アプリケーションにロギングフレームワークを実装するようなものです。
Storage
インターフェースの例は、Storage
が外部フレームワークと見なされていると仮定して、優れたものです。
また、オブジェクト内のsave()
は、それがドメイン(「言語」)の一部である場合にのみ許可する必要があります。たとえば、transfer(amount)
を呼び出した後、Account
を明示的に「保存」する必要はありません。私はビジネス関数transfer()
が私の転送を持続することを期待するべきです。
全体として、DDDのアイデアは良いものだと思います。ユビキタス言語を使用して、会話、境界コンテキストなどでドメインを行使します。ただし、ビルディングブロックは、オブジェクト指向と互換性がある場合、深刻なオーバーホールが必要です。詳細については、リンクされたデッキを参照してください。
すでに述べたパターンに代わるものがあります。 Mementoパターンは、ドメインオブジェクトの内部状態をカプセル化するのに最適です。 mementoオブジェクトは、ドメインオブジェクトのパブリック状態のスナップショットを表します。ドメインオブジェクトは、内部状態からこのパブリック状態を作成する方法、およびその逆の方法を知っています。リポジトリは、状態の公開表現でのみ機能します。これにより、内部実装は永続性の詳細から切り離され、パブリックコントラクトを維持するだけで済みます。また、ドメインオブジェクトは、実際に少し貧弱になるゲッターを公開する必要はありません。
このトピックの詳細については、スコットミレットとニックチューンによる「ドメイン駆動設計のパターン、原則、および実践」というすばらしい本をお勧めします。