DDDがOOPを満たしている:オブジェクト指向リポジトリを実装する方法は?


12

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"));
    }
}

これを達成するための最良のアプローチは何ですか?オブジェクト指向のリポジトリを実装することは可能ですか?


6
OOPは、Productオブジェクトはそれ自体を保存する方法を知っているべきだと言います -それが本当に正しいかどうかはわかりません... -useが入ります)
-jleach

1
OOPのコンテキストでは、オブジェクトについて話していることに注意してください。データの永続性ではなく、オブジェクトのみ。あなたの声明は、オブジェクトの状態を外部で管理するべきではないことを示していますが、私はそれに同意します。リポジトリーは、永続レイヤー(OOPの領域外)からのロード/保存を担当します。クラスのプロパティとメソッドは、独自の整合性を維持する必要があります(はい)。ただし、これは、別のオブジェクトが状態の永続化を担当できないことを意味するものではありません。また、ゲッターとセッターは、オブジェクトの受信/送信データの整合性を確保するためのものです。
-jleach

1
「これは、別のオブジェクトが状態を維持する責任を負えないという意味ではありません。」-言わなかった。重要なステートメントは、オブジェクトがアクティブでなければならないということです。これは、オブジェクト(および他の誰も)がこの操作を別のオブジェクトに委任できることを意味しますが、他の方法では委任できません。 。上記のスニペットでこのアプローチを実装しようとしました。
トゥルカ

1
@jleachそうです、OOPに対する私たちの未理解は異なります。ゲッターとセッターはまったくOOPではありません。ともあれ、ありがとう!:-)
トゥルカ

1
ここに私のポイントについての記事は以下のとおりです。martinfowler.com/bliki/AnemicDomainModel.html私は例えば、それは関数型プログラミングのための良い戦略である、すべてのケースで貧血モデルagaintありませんよ。OOPではありません。
トゥルカ

回答:


7

あなたが書いた

一方、OOPはProductオブジェクトが自分自身を保存する方法を知っている必要があると言います

そしてコメントで。

...それで行われるすべての操作に責任を負うべき

これはよくある誤解です。Productそれはのために責任を負わなければならないので、ドメインオブジェクトであるドメイン伴う操作単一のためにそれほど間違いではない-これ以上、劣らず、製品のオブジェクトをすべての操作。通常、永続性はドメイン操作とは見なされません。まったく逆に、エンタープライズアプリケーションでは、ドメインモデルで永続性の無知を(少なくともある程度まで)達成しようとすることは珍しくなく、永続性の仕組みを別のリポジトリクラスに保持することがこのための一般的なソリューションです。「DDD」は、この種のアプリケーションを目的とした手法です。

それでは、Aにとって賢明なドメイン操作はProduct何でしょうか?これは、実際にはアプリケーションシステムのドメインコンテキストに依存します。システムが小さく、CRUD操作のみをサポートしているProduct場合、実際には、例のようにかなり「貧弱」な状態が続く可能性があります。この種のアプリケーションでは、データベース操作を別のレポクラスに入れるか、DDDを使用するのが面倒な場合は議論の余地があります。

ただし、アプリケーションが製品の売買、在庫の保持と管理、または税金の計算などの実際のビジネスオペレーションをサポートするとすぐに、Productクラスに適切に配置できるオペレーションを発見することは非常に一般的です。たとえば、CalcTotalPrice(int noOfItems)ボリュームディスカウントを考慮に入れるときに、特定の製品の `nアイテムの価格を計算する操作があるかもしれません。

要するに、クラスを設計するときは、コンテキスト、Joel Spolskyの5つの世界のどれであるか、そしてシステムに十分なドメインロジックが含まれていればDDDが有益であるかを考える必要があります。答えが「はい」の場合、永続性の仕組みをドメインクラスに入れないという理由だけで、貧弱なモデルになることはほとんどありません。


あなたの主張は私にとって非常に賢明なことです。そのため、貧血データ構造(データベース)のコンテキストの境界を越えると、製品は貧血データ構造になり、リポジトリはゲートウェイになります。しかし、これは依然として、ゲッターとセッターを介してオブジェクトの内部構造へのアクセスを提供する必要があることを意味します。ゲッターとセッターは、APIの一部となり、永続性とは関係のない他のコードによって簡単に悪用される可能性があります。これを回避する良い方法はありますか?ありがとうございました!
トゥルカ

「しかし、これはまだゲッターとセッターを介してオブジェクトの内部構造へのアクセスを提供する必要があることを意味します」 -ありそうもない。永続性を無視するドメインオブジェクトの内部状態は、通常、ドメイン関連の属性のセットによってのみ与えられます。これらの属性については、ゲッターとセッター(またはコンストラクターの初期化)が存在する必要があります。存在しない場合、「興味深い」ドメイン操作はできません。いくつかのフレームワークでは、リフレクションによってプライベート属性を永続化できる永続化機能も利用できるため、カプセル化は「他のコード」ではなく、このメカニズムでのみ破損します。
Doc Brown

1
通常、永続性はドメイン操作の一部ではないことに同意しますが、永続性はそれを必要とするオブジェクト内の「実際の」ドメイン操作の一部である必要があります。たとえばAccount.transfer(amount)、転送を永続化する必要があります。それがどのように行われるかは、オブジェクトの責任であり、外部エンティティの責任ではありません。一方、オブジェクトの表示通常、ドメイン操作です!要件は通常、物がどのように見えるべきかを詳細に説明します。これは、ビジネスやその他のプロジェクトメンバーの言語の一部です。
ロバートブラウティガ

@RobertBräutigam:クラシックAccount.transferは通常、2つのアカウントオブジェクトと作業単位オブジェクトを含みます。トランザクションの永続化操作は、(関連するリポジトリへの呼び出しとともに)後者の一部になる可能性があるため、「転送」メソッドから除外されます。そのようにして、Account永続性を無視することができます。私はこれがあなたの想定した解決策よりも必ずしも良いとは言っていませんが、あなたの解決策はいくつかの可能なアプローチの1つにすぎません。
Doc Brown

1
@RobertBräutigamオブジェクトとテーブルの関係について考えすぎていることは間違いありません。オブジェクトは、それ自体の状態をすべてメモリ内に持っていると考えてください。アカウントオブジェクトで転送を実行すると、新しい状態のオブジェクトが残ります。それはあなたが持続させたいものであり、幸いにもアカウントオブジェクトはそれらの状態についてあなたに知らせる方法を提供します。それは、それらの状態がデータベース内のテーブルと等しくなければならないという意味ではありません。つまり、転送される金額は、生の金額と通貨を含むマネーオブジェクトである可能性があります。
スティーブチャマイラード

5

実践は理論に勝ります。

経験から、Product.Save()は多くの問題を引き起こすことがわかります。これらの問題を回避するために、リポジトリパターンを考案しました。

確かに、製品データを隠すというOOPルールを破ります。しかし、それはうまく機能します。

例外のある一般的な良いルールを作成するよりも、すべてを網羅する一貫したルールのセットを作成するのがはるかに困難です。


3

DDDがOOPを満たしている

これら2つのアイデアの間に緊張を意図するものではないことを覚えておくと役立ちます。バリューオブジェクト、集計、リポジトリは、使用されるパターンの配列であり、OOPが正しく行われると考える人もいます。

一方、OOPはProductオブジェクトが自身の保存方法を知っているべきだと言っています。

そうではない。オブジェクトは独自のデータ構造をカプセル化します。製品のメモリ内表現は、製品の動作(それらが何であれ)を示す責任があります。しかし、永続ストレージはそこ(リポジトリの背後)にあり、独自の作業が必要です。

データベースのメモリ内の表現とその永続的な記憶との間でデータをコピーする何らかの方法が必要です。 境界では、物事はかなり原始的になる傾向があります。

基本的に、書き込み専用データベースは特に有用というわけではなく、メモリ内の同等のデータベースは「永続的な」ソートよりも有用ではありません。その情報をProduct決して取り出さないのであれば、情報をオブジェクトに入れることは意味がありません。必ずしも「getter」を使用するわけではありません。製品のデータ構造を共有しようとしているわけではなく、製品の内部表現への変更可能なアクセスを共有すべきではありません。

保存を別のオブジェクトに委任することもできます。

それは確かに機能します-永続ストレージは事実上コールバックになります。私はおそらくインターフェイスをよりシンプルにするでしょう:

interface ProductStorage {
    onProduct(String name, double price);
}

そこにはされていく情報はこちらから(そして再び)そこに到達するために必要があるため、メモリ表現とストレージメカニズムに間を結合させます。共有する情報を変更すると、会話の両端に影響が及びます。そのため、可能な場合はそのことを明示することもできます。

このアプローチ-コールバックを介してデータを渡すことは、TDDモックの開発に重要な役割を果たしました。

情報をコールバックに渡すことには、クエリから情報を返すことと同じ制限がすべてあることに注意してください。データ構造の変更可能なコピーを渡すことはできません。

このアプローチは、エヴァンスがブルーブックで説明したものとは少し反対であり、クエリを介してデータを返すことが物事を進める通常の方法であり、ドメインオブジェクトは「永続性の懸念」の混在を避けるように特別に設計されていました。

私はDDDをOOPテクニックとして理解しているので、その一見矛盾を完全に理解したいと思います。

念頭に置いておくべきこと-Blue Bookは、Java 1.4が地球を歩き回った15年前に書かれました。特に、この本はJava ジェネリックよりも前のことです。エヴァンスがアイデアを開発していたときから、さらに多くの技術を利用できるようになりました。


2
また、「それ自体を保存する」には、常に他のオブジェクト(ファイルシステムオブジェクト、データベース、またはリモートWebサービスのいずれか、これらの一部はさらにアクセス制御のためにセッションを確立する必要がある)との対話が必要です。したがって、そのようなオブジェクトは自立型ではなく、独立したものではありません。したがって、OOPはオブジェクトをカプセル化し、結合を減らすことを目的としているため、これを必要とすることはできません。
クリストフ

すばらしい回答をありがとう。最初に、Storageあなたが行ったのと同じ方法でインターフェースを設計し、次に高結合を考慮して変更しました。しかし、あなたは正しい、とにかく避けられない結合があるので、なぜそれをより明確にしないのか。
トゥルカ

1
「このアプローチは、エヴァンスがブルーブックで説明したものとは少し反対です」 -結局、多少の緊張があります:-)それが実際に私の質問のポイントでした。私はDDDをOOPテクニックとして理解しています。その一見矛盾を完全に理解する。
トゥルカ

1
私の経験では、これらのこと(OOP全般、DDD、TDD、頭字語を選ぶ)はすべて、それ自体が素晴らしく、素晴らしい音に聞こえますが、「現実の」実装に関しては、常にいくらかのトレードオフまたはそれが機能するために必要な理想よりも少ない。
-jleach

永続性(およびプレゼンテーション)が何らかの形で「特別」であるという概念には同意しません。ではない。これらは、要件の要求を拡張するためのモデリングの一部である必要があります。反対の実際の要件がない限り、アプリケーション内に人為的な(データベースの)境界がある必要はありません。
ロバートブラウティガ

1

非常に良い観察、それらについてあなたに完全に同意します。まさにこのテーマについての私の話です(訂正:スライドのみ):オブジェクト指向ドメイン駆動設計

短い答え:いいえ。純粋に技術的なものであり、ドメイン関連性のないオブジェクトをアプリケーションに含めることはできません。これは、会計アプリケーションにロギングフレームワークを実装するようなものです。

たとえあなたがそれを書いたとしても、それが何らかの外部フレームワークと見なされるとStorage仮定するStorageと、あなたのインターフェースの例は素晴らしいものです。

また、save()オブジェクト内では、それがドメイン(「言語」)の一部である場合にのみ許可する必要があります。たとえばAccount、を呼び出した後に明示的に「保存」する必要はありませんtransfer(amount)。私は、ビジネス機能transfer()が私の移転を持続することを当然期待する必要があります。

全体として、DDDのアイデアは良いものだと思います。ユビキタス言語の使用、会話、境界付きコンテキストなどを使用したドメインの実行。ただし、ビルディングブロックは、オブジェクト指向と互換性がある場合、深刻なオーバーホールが必要です。詳細については、リンクされたデッキを参照してください。


あなたの話はどこか見たいですか?(リンクの下にあるのはスライドだけです)。ありがとう!
トゥルカ

ドイツ語での講演の録音はここにあります。javadevguy.wordpress.com
ロバートブラウティガ

素晴らしい話です!(幸いなことに、私はドイツ語を話します)。あなたのブログ全体を読む価値があると思います...あなたの仕事に感謝します!
トゥルカ

非常に洞察力に富んだスライダーRobert。私はそれを非常に例示的であると感じましたが、最後に、カプセル化とLoDを壊さないことに対処したソリューションの多くは、ドメインオブジェクトに多くの責任を与えることに基づいていると感じました:印刷、シリアル化、UIフォーマットなどドメインと技術的(実装の詳細)との結合を強化しますか?たとえば、Apache Wicket APIと組み合わせたAccountNumber。または、Jsonオブジェクトが何であるかを説明しますか?それは価値のあるカップリングだと思いますか?
ライヴ

@Laivあなたの質問の文法は、ビジネス機能を実装するためにテクノロジーを使用することに何か問題があることを示唆していますか?このようにしましょう。問題は、ドメインとテクノロジーの結合ではなく、異なる抽象化レベル間の結合です。たとえば、として表現できることを知っているAccountNumber 必要がありTextFieldます。他の人(「ビュー」など)がこれを知っている場合、それは存在してはならないカップリングです。なぜなら、そのコンポーネントは何AccountNumberから構成されるか、つまり内部を知る必要があるからです。
ロバートブラウティガム

1

保存を別のオブジェクトに委任することもできます

不必要にフィールドの知識を広めることは避けてください。個々のフィールドについて知っていることが多いほど、フィールドの追加または削除が難しくなります。

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage.save( toString() );
    }
}

ここでは、ログファイル、データベース、またはその両方に保存するかどうかは製品にはわかりません。ここでは、フィールドが4つまたは40ある場合、saveメソッドはわかりません。それは疎結合です。よかったです。

もちろん、これはこの目標を達成する方法の一例にすぎません。DTOとして使用する文字列を構築および解析するのが嫌な場合は、コレクションを使用することもできます。LinkedHashMap順序を保持し、ログファイルでtoString()が適切に表示されるため、私のお気に入りです。

どんなことをしても、周りの分野の知識を広めないでください。これは、人々がしばしば遅れるまで無視する結合の形式です。オブジェクトにできる限り多くのフィールドがあることを静的に知ることはできるだけ少なくしたい。この方法でフィールドを追加すると、多くの場所で多くの編集が必要になりません。


これは実際、質問に投稿したコードですよね?私が使用したMap、あなたは、Stringまたはを提案しますList。しかし、@ VoiceOfUnreasonが彼の答えで言及したように、カップリングはまだそこにあり、明示的ではありません。少なくともオブジェクトとして読み戻される場合、データベースまたはログファイルの両方に保存するために、製品のデータ構造を知る必要はありません。
トゥルカ

保存方法を変更しましたが、そうでない場合はほぼ同じです。違いは、カップリングが静的ではなくなり、ストレージシステムへのコード変更を強制せずに新しいフィールドを追加できることです。これにより、ストレージシステムはさまざまな製品で再利用可能になります。ダブルをストリングに変えてダブルに戻すなど、少し不自然なことをするように強制します。しかし、それが本当に問題であれば、それも回避できます。
candied_orange

ジョシュブロッホの異様なコレクション
-candied_orange

しかし、私が言ったように、静的ではない(明示的)だけではコンパイラによってチェックできないという欠点が生じるため、より多くのエラーが発生する可能性があるため、カップリングはまだ(解析によって)表示されます。Storageドメインの一部である(同様に、リポジトリインターフェイスであるように)、そのような永続性APIを作ります。変更された場合、コンパイル時にクライアントに通知することをお勧めします。これは、クライアントが実行時に破損しないように反応する必要があるためです。
トゥルカ

それは誤解です。コンパイラは、ログファイルまたはDBをチェックできません。チェックしているのは、あるコードファイルが別のコードファイルと一貫性があるかどうかであり、これもログファイルまたはDBとの一貫性が保証されていません。
candied_orange

0

すでに述べたパターンに代わるものがあります。Mementoパターンは、ドメインオブジェクトの内部状態をカプセル化するのに最適です。mementoオブジェクトは、ドメインオブジェクトのパブリック状態のスナップショットを表します。ドメインオブジェクトは、内部状態からこのパブリック状態を作成する方法を認識しています。リポジトリは、状態のパブリック表現でのみ機能します。これにより、内部実装は永続性の仕様から切り離され、公開契約を維持するだけで済みます。また、ドメインオブジェクトは、実際に少し貧弱になるゲッターを公開する必要はありません。

このトピックの詳細については、スコット・ミレットとニック・チューンによる「ドメイン駆動設計のパターン、原則、および実践」という素晴らしい本をお勧めします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.