#practical.ddd
Explore tagged Tumblr posts
phpmentors · 7 years ago
Text
DCI Tokyo 1 - Lean Architecture by James Coplien - が開催されました(後編)
後半のセッションでは、「What the system is(共通性や可変性を分析する)」を説明しつつも、「What the system does」とは別のものとして切り分けることはできないという主張から、 DDD とリーンアーキテクチャとの比較、そしてパターンに関する議論が展開されました。 @remore が前半部分をまとめてくださっていまして、そこで出て来るいくつかの用語(Form, Structure, What the system is, What the system does)を前提として私のできる範囲で行いました。
なお当日のCoplien氏によるセッション内容は、許可を得た上でYouTubeにアップロードされていますので、より深くご覧になりたい方はこちらも併せてご参照下さい。
DCI Tokyo 1 - Lean Architecture by James Coplien (Part 1 of 2)
DCI Tokyo 1 - Lean Architecture by James Coplien (Part 2 of 2)
What system is に関して
Jim Coplien さんの言うアーキテクチャ
共通性、可変性分析に関する What system is に関連している。
ほとんどのドメイン知識は、アルゴリズムで表される What system does よりも What system is の方にある。
ソ��トウェアを開発する際に重要なもの
プログラム言語自体の知識ではなく、ドメインでありビジネスである。
ほとんどのドメイン知識は、構造(Structural)であって、アルゴリズムではない。
このミートアップでは、構造(Structure)の方を主に扱う。What system is の方である。
実際のシステムを構築する際には、構造は安定されていることが多いので先に行うことが多く、それらが出来上がってから、まだ実装されていない機能を作り始める。
その機能が、人々が求めているユースケースになり、これがビジネスとなる。
What system is と What system does の関係性
しかし、What system is と What system does は別々に考えられるものではない。それは、構造と時間やアルゴリズムを全く別のものとして、分けることは不可能だからである。
リーンアーキテクチャ本では、 What system is も What system does も説明しており、夫婦で共著で書いている。 リーンアーキテクチャ本では分けることができると書いたが、実際は簡単に構造とユースケースを分けることができるわけではない。
それは、歴史が物語っている。
1960年から1970年では、ノイマンコンピュータでは、データ構造は全く注目せずに、 What system does に注力され、 CPU が大事でこの性能を良くするということが重要だった。
1970年後半にデータベースが登場し、ボトルネックはデータへのアクセス速度が重要となった。CPU のことは忘れ去れられ、What system is が注意の関心の的になり、データモデルが重要になった。
1980年にコンピューターネットワーキングが登場し、オブジェクト同士のコミュニケーションが重要になった。こちらは What system does がまた注目された。
このように、歴史を見ると、 What system is と What system does のサイクルができている。
アーキテクチャ全般に言えることだが、例えば部屋の構造では、どこにライトがあって、どこドアがあって、それが何か(What system is)を説明することができるが、どうしてそこにある(What system does)のか、とか、部屋の中をどうやって歩くかなどのプロセスにも影響している。つまり、これらは簡単に分けることはできずに互いに関係しあっている。
リーンアーキテクチャと DDD の関係性
DDD
分析に関しては扱っていない。
コードに関することを主に扱っている。
形態(Form)の社会性は扱わない。
構造(Structure)のみに注目しており、具体的な事柄から始める。
リーンアーキテクチャ
人々について、そして人々のメンタルモデルを扱う。モデルに関する議論をすることを扱っているので、社会的な活動である。
形態(Form)を扱う。
形態(Form)からスタートして、抽象的な Commonality から Variation を適用させて正しい実装をしていくことである。
アレグザンダーは、アーキテクチャは、社会的な活動であると明記している。
対称性とパターンに関して
アレグザンダーは、空間からイベント��パターンを作成している。ソフトウェアはまだ歴史が浅いため、パターンはほぼ存在しない。建築は世紀を超えてパターンを作ってきた。ソフトウェアの世界で言われているパターンは、コンテキストの上でのロジックであって、それはパターンではない。唯一、例として上げてもらったパターンは、リーキーバケットに関するものであり、イベントのパターンを説明している。
リーキーバケット
共通性と可変性は対称性を持っているが、あるとき対称性が壊れることがある。プログラム言語では対称性を表そうとするが、実際の要件では、対称性のない複雑なものを作り上げる必要がある。リーンアーキテクチャは対称性を扱っており、負の可変性(Negative Variation)は扱っていない。ここが複雑な場所である。パターンは、What system is と what system does の両方を考える必要があり、また、空間と時間を考える必要がある。
パターンに関する議論に関しては、私(ganchiku)の中で消化しきれていない場所があり、まとめることができませんでしたので、今後の課題としてアレグザンダーや Jim Coplien さんの議論を追いかけて、自分の言葉で説明できるようになっていこうと思います。
会場を提供していただいた UUUM 株式会社、また、オーガナイズしていただきました DCI Tokyo メンバーの皆様ありがとうございました。
1 note · View note
phpmentors · 8 years ago
Text
「現場で役立つシステム設計の原則」批判 (2) ~ポリモーフィズムは何のために?~
増田亨氏の「現場で役立つシステム設計の原則]」批判の第2編です。
(2)ポリモーフィズムは何のために?
オブジェクト指向の要件
本書には「変更を楽で安全にするオブジェクト指向の実践技法」というサブタイトルが付与されています。オブジェクト指向が何かという点については、論者によって違いはあるものの、以下の3つが要件とされることには、多くの人が合意するでしょう。
カプセル化
インヘリタンス(継承)
ポリモーフィズム(多態)
前回で明らかになったように、カプセル化は、オブジェクト指向とは独立にモジュール分割の指導原理としてデイビッド・パーナスにより提唱された「情報隠蔽」を敷衍したものです。オブジェクト指向の要件ではありますが、オブジェクト指向固有のアイデアではありません。
インヘリタンスは便利な機能ですが、コンポジションや移譲により代替できるので、オブジェクト指向の本質的な要件とは見做されなくなって��たかと思います。
となれば、プログラミングの歴史にオブジェクト指向が付け加えたものは、ポリモーフィズムであるということになります。オブジェクト指向のもっとも偉大な貢献は、イヌがワンと鳴き、ネコがニャーと鳴くことです!
さて、こうした視点から本書を通読すると、まず、カプセル化については、その手段に過ぎない「データと処理を一体にする」という点に囚われ過ぎて、目的である「情報隠蔽」を取り逃がしているということが言えるかと思います。これが前回の指摘でした。
本書でのポリモーフィズム
では、本書ではポリモーフィズムはどのように扱われているでしょう���。
本書でポリモーフィズムを明示的に扱っているのは、「Chapter2 場合分けのロジックを整理する」だけです。この章でのポリモーフィズムの用途は、区分値に関する条件分岐(if文/switch文)を排除することです。
ポリモーフィズムのこうした利用方法は、マーティン・ファウラーのリファクタリング本で紹介され知られるようになったかと思いますが、便利でもあり、広く使われているものでもあると思います。
ただ、こうした「区分値ポリモーフィズム」は、ポリモーフィズムを適用可能なユースケース全体の中で、かなり周辺的なものであるように私には感じられます。その理由について以下ご説明します。
ポリモーフィズム ― 典型的な用法
その前にまず、ポリモーフィズムの典型的な用法とはどのようなものでしょうか。前回ご提示した、受注(SalesOrder)における値引き計算を例にご説明しましょう:
class SalesOrder { Money unitPrice; Quantity quantity; // ... Money amount() { if (isDiscountable()) return discount(unitPrice, quantity); return unitPrice.multiply(quantity); } // ... boolean isDiscountable() { return quantity.compareTo(discountCriteria) >= 0; } }
このコードでは、開示してよい知識と隠蔽したい知識を以下のように切り分け、前者をisDiscountable()に閉じ込めました:
開示してよい知識
受注ごとにその内容に応じて値引き可否が決まるという知識。
隠蔽したい知識
注文数量・金額等にもとづく具体的な値引き決定ルール。
ここで、「隠蔽したい知識」をなぜ隠蔽したいかというと、それが変わりやすいと考えるからです。ならば、一歩進んで、変わりやすい部分は差し替え可能にしておこうという発想が生まれます:
class SalesOrder { Money unitPrice; Quantity quantity; DiscountPolicy discountPolicy; // ... boolean isDiscountable() { return discountPolicy.isDiscountable(this); } }
ここで、DiscountPolicyは、以下のようなインターフェースです:
interface DiscountPolicy { boolean isDiscountable(SalesOrder salesOrder); }
DiscountPolicyの実装のひとつとして、QuantityBasedDiscountPolicyがあります:
class QuantityBasedDiscountPolicy implements DiscountPolicy { Quantity discountCriteria = Quantity.valueof(100); boolean isDiscountable(SalesOrder salesOrder) { return salesOrder.getQuantity().compareTo(discountCriteria) >= 0; } }
QuantityBasedDiscountPolicyは、たぶん、DIコンテナなどにより、SalesOrderのdiscountPolicyに注入されるでしょう。
この例��、ポリモーフィズムは、SalesOrderに関する様々な関心事から値引き計算に関する関心事を分離するのに用いられ��います。 例えば、テストという局面をとっても、QuantityBasedDiscountPolicyはSalesOrderに関する他のテストから切り離しでテストできますし、SalesOrderの方は(スタブを用いて)値引き計算の詳細を考慮せずにテストすることができます。 さらに、このソフトウェアが成功し、他社でも使われるようになれば、値引き計算ルールをこのように簡単にカスタマイズできることは、さらなるメリットをもたらすでしょう。
DiscountPolicyのように、特定の計算/判定条件をカプセル化し差替可能にするというポリモーフィズムの利用法は、GoFのデザインパターン本で「ストラテジー(別名:ポリシー)」として知られており、エリック・エバンスのドメイン駆動設計本でも、ごく最初の方で、オーバーブッキングポリシーという例が紹介されています(p.19/位置No.1068)。高度な利用法ではなく、極めて普通な用いられ方かと思います。
前回お話しした「情報隠蔽(=カプセル化)」では、隠蔽したい知識と開示してよい知識を区分して両者を別のモジュール(クラスもモジュールです)に割り振りました。 ポリモーフィズムは、そこから一歩進んで、それら2つのモジュールを切り離し、「疎結合化」するのに役立ちます。「疎結合」というと大げさに響きますが、要するに処理の依頼側と引受側2つのモジュールがあって、依頼側が引受側を知らずとも処理を依頼できるということです。この例で、値引き可否判定を依頼する側であるSalesOrderクラスはQuantityBasedDiscountPolicyクラスを知りません。
こういった意味における疎結合化は、オブジェクト指向以前も行われていましたが(*1)、気軽に、広範に適用できるようになったのは、やはりポリモーフィズムが生み出されてからのことです。
区分値ポリモーフィズムと本書に対する評価
さて、ポリモーフィズムの典型的なユースケースをおさらいした頭で、「区分値ポリモーフィズム」を見直してみましょう。
本書が説明する通り、区分値ポリモーフィズムには、if文やswitch文を排除することで、ソースコードの読みやすさを改善する機会を提供する、という役立ちがあります。
しかし、区分値オブジェクトのメソッド(本書の例では、yen()など)を呼び出す側のプログラムは、区分値オブジェクトのクラスを「知って」いるのが通常ですから、処理の依頼側と引受側は結合したままです(*2)。 ですから、前述したように「疎結合化」をポリモーフィズムの大きな意義と捉える立場からすれば、区分値ポリモーフィズムはやや傍流的な用法に見えることがお分かり頂けるでしょう。
公平に見て、本書は、ポリモーフィズムの他のユースケースについて触れていないだけであり、それらを否定しているわけではありません。ですから、本件は、本書の問題というより、読む側が注意すべき点である、ということなのかもしれません。
ただ、「多態は、区分ごとのロジックをクラス単位に分離してコードを読みやすくするオブジェクト指向らしいしくみです(p.58/位置No.1040)」といった説明を読む読者、とりわけオブジェクト指向の経験が浅い読者が、ポリモーフィズムの主な用途は区分ごとの場合分けロジックを整理することなのだと受け止めるのは自然なことでしょう。しかし、その道の先は行き止まりなのです。
むしろ、初級者には、疎結合化というビジョンのもとで、できるだけ簡単な入り口を示してあげるのが親切というものでしょう。上述したポリシーあるいはストラテジーパターンはそのような入り口として好適な例のひとつと思います。
「現場で役立つシステム設計の原則」という格調高いタイトルと「変更を楽で安全にするオブジェクト指向の実践技法」という具体的な副題を持つ300ページ超の本の中で、オブジェクト指向の業績の中心にあるポリモーフィズムについてこのように周辺的なユースケースのみ解説されている事態は、少なくとも私には驚異的なことです。
ポリモーフィズム ― さらなる展開
今回の批判はこれで終わりですが、例として取り上げた値引き計算のケースは、ポリモーフィズムの可能性を検討する上で興味深いので、もう少し深堀りしてみましょう。 前回以来の例では値引き可否判定ロジックの扱いが焦点でしたが、実際には可否を判定するだけでなく値引き額の計算が重要でしょう。業種にもよりますが、値引きの計算は複雑で、特定品目限定の値引き、過去の取引履歴に基づく値引き、一回の総発注額に基づく値引き、キャンペーンでの値引きなど多岐にわたるかもしれません。また、そうした値引きルールは、時の経過に応じて追加され、また廃止されていきます。システムは、そうした諸々の値引きについて、理由と金額をお客様に提示する必要があるでしょう。
こうした状況にどう対応すればよいでしょうか。SalesOrderクラスは、isDiscountable()メソッドで値引き可否だけを返せばよいのではなく、値引きの詳細を返さなければなりません。例えば、以下のようなメソッドを備えることになるでしょう:
List<Discount> getDiscounts();
Discountは、値引き理由の説明と値引き額を保持するオブジェクトです:
class Discount { String description; Money discount; // ... }
ここでの焦点は、値引きルールの詳細を隠蔽することに加えて、その時々に応じて値引きルールを追加し、あるいは廃止出来るようにすることです。それを踏まえれば、getDiscounts()の詳細は以下のようになるでしょう:
class SalesOrder { // DIコンテナなどから値引きポリシーのリストを設定 List<DiscountPolicy> discountPolicies; // ... List<Discount> getDiscounts() { List<Discount> discounts = new ArrayList<Discount>(); // 値引きポリシーを適用 for (DiscountPolicy discountPolicy : discountPolicies) { Discount discount = discountPolicy.getDiscountFor(this); if (discount != null) { discounts.add(discount); } } return discounts; } }
インターフェースDiscountPolicyは以下のようになります。
interface DiscountPolicy { /** * 与えられた salesOrder に対する値引きを計算し、その結果を Discount として返す。 * 当ポリシーでの値引きが salesOrder に適用されない場合は null を返す。 */ Discount getDiscountFor(SalesOrder salesOrder); }
このようにしておけば、新しい値引き制度が出来たときには、それに対応するDiscountPolicyを作成してシステムに登録するだけで対応が完了します(*3)。
値引き計算以外に、請求/回収条件、在庫引当、配送方法などにも同じような仕組みが適用可能かもしれません。こうした手法を常に適用すべきということではありませんが、適用しようと思えば出来る、ということを理解していると、設計における選択肢の幅が顕著に広がります。
こうした例をお示ししたのは、ポリモーフィズムというものが過度に技術的なものと受け止められているのではないかと私が危惧しているからです。
イヌがワンワン、ネコがニャーニャーという説明ばかりでポリモーフィズムはわからん、という方が多いですが、一方でその方もDIコンテナは使っていたりします。DIコンテナは、技術的環境に依存するオブジェクトに関して、アプリケーションにはインターフェースだけ提示し、その実装はポリモーフィズムによって実行時に結合することで、アプリケーションと技術的環境を疎結合に保つための仕掛けです。ポリモーフィズムのメカニズムが本当にわからないのであればDIコンテナを使うことさえ出来ないはずです。
ですから、多くの人にとってわからないのは、ポリモーフィズムのメカニズムではなく、「使いどころ」なのだろうと思うのです。フレームワークなどがポリモーフィズムを使っているのはわかっている。ただ、自分たちが書いている「アプリケーション」のコードにおいてどういう局面でポリモーフィズムを使えばよいのか、わからない。結果として、ポリモーフィズムは自分たちに関係ない「技術的」な概念だと感じてしまう。そういうことではないでしょうか。
実際には、それは誤解で、アプリケーション開発においてもポリモーフィズムはカジュアルに利用できます。値引き計算の例ではそれをご理解頂きたかったわけです。ポリシー(ストラテジー)パターンは、ポリモーフィズムのユースケースのごく一部に過ぎませんが、疎結合化というポリモーフィズムの本旨に沿っており、かつ、利用価値も高いものだと思います。
杉本 啓 @sugimoto_kei 経営管理基盤ソフトウェア fusion_place の、プログラマ兼設計者 http://www.fusions.co.jp
[脚注]
[1] 昔からある「ユーザ出口ルーチン」などはこの例でしょう。 [2] 料金計算など区分値に依存するメソッドを区分値クラスに移すことと、その計算にポリモーフィズムを適用することは別問題です。例えば本書p.60/位置No.1062のFeeTypeクラスは以下のように記述することもできます(label()に関する実装は省略)。
enum FeeType { ADULT, CHILD, SENIOR; Yen yen() { switch (this){ case ADULT: return adultFee(); case CHILD: return childFee(); case SENIOR: return seniorFee(); default: throw new UnsupportedOperationException("Fee for fee type [" + this + "] not defined."); } } private Yen adultFee() { return new Yen(100); } private Yen childFee() { return new Yen(50); } private Yen seniorFee() { return new Yen(80); } }
p.60/位置No.1062のコードとこのコードを比べてみると、料金計算方法という知識がFeeTypeクラスに隠蔽されている点は同じで、大人・子供・シニアの料金計算がそれぞれ別のメソッドに分離されている点も同じです。違いは、条件分岐に switch文を使うかポリモーフィズムを使うかという点だけです。 こうしたメソッドが複数あるならば、それぞれの実装でswitchを書くより、ポリモーフィズムを用いるべきでしょう。上記の例のようにswitchが1��所だけであれば、いずれにするかは、設計の良否というより多分に好みの問題と、私は思います。
なお、このケースではそもそもポリモーフィズムを使うほどのこともなく、以下のコードで十分です。
enum FeeType { ADULT(100), CHILD(50), SENIOR(80); private final Yen yen; private FeeType(int yenAsInt) { this.yen = new Yen(yenAsInt); } Yen yen() { return yen; } }
ただ、これは、単にサンプルコードの選択に関する趣味の問題であって、区分値ポリモーフィズムの妥当性という論点には影響しません。
[3] ここで示したコード例は、値引き制度の間に相互依存関係がないという想定にもとづいています。依存関係があり得る場合(例えば上得意先向け値引きを適用したときにはキャンペーン値引きは適用しないといった条件に対応する場合)、DiscountPolicyにDiscountのリストを渡し、各ポリシーがそのリストに対してDiscountを除去したり追加したりできるようにする方がよいかもしれません。
interface DiscountPolicy { /** * 与えられた salesOrder に対する値引きを計算し、その結果を DiscountのListに追加する。 * リストから既存のDiscountを除去しても構わない。 */ void updateDiscounts(SalesOrder salesOrder, List<Discount> discounts); }
この場合、Discountに、値引き制度を識別するためのDiscountTypeなどを保持させることも必要でしょう。
値引き計算といった機能をこのように疎結合化するにあたり、疎結合化されたモジュール間の役割分担をどのように設計するかは極めて重要です。あるいはそれこそがオブジェクト指向にもとづくアプリケーション設計の核心部分かもしれません。 「オブジェクト指向」という言葉を作ったアラン・ケイは「オブジェクトよりもメッセージング」と強調したメールの中で、以下のように言っています:
The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.
(拙訳)偉大で成長可能なシステムを作る上でのカギは、個々のモジュールの内部特性や振る舞いがどうあるべきかということにではなく、むしろ、モジュールたちがどのようにコミュニケ―トするかをデザインすることにこそある。
また、「マルチパラダイムデザイン」で、ジム・コプリエンが提示した「共通性/可変性分析」も、こういった相互作用に重点を置いた設計手法です。
6 notes · View notes
phpmentors · 8 years ago
Text
「現場で役立つシステム設計の原則」批判 (1) ~何のために、「データとロジックを一体に」するのか?~
増田亨氏の「現場で役立つシステム設計の原則]」の評判が高いようです。
この本が、オブジェクト指向の初級者に受け入れられ易いことはわかります。オブジェクト指向的なプログラミングが出来ていない現場で、明日からでも出来そうなことが平易に書かれているからです。オブジェクト指向の入り口を指し示しているように見える。
一方で、私としては、この本が指し示す入り口は、入りやすいかもしれないけれど、結局はどこにも通じていないのではないかと疑っています。
本稿では、そのように私が考える理由について、3つの切り口からご説明したいと考えています:
何のために、「データとロジックを一体に」するのか?
ポリモーフィズムは何のために?
「ドメインモデル」とは何か?
長くなるので、今回は、一番目の「何のために、『データとロジックを一体に』するのか?」について。
批判 (1) 何のために、「データとロジックを一体に」するのか?
本書はデータとロジックを一体にすることを強く主張しています(p.77/位置No.1294等)。データとロジックを一体にすることは一般には「カプセル化」と呼ばれ、オブジェクト指向の特長のひとつとされていますから、こうした主張自体は突飛なものではありません。
問題は、本書の場合、カプセル化することが自己目的化していて、何のためにカプセル化するのかという視点が極めて希薄なことです。その結果、「うまいカプセル化」「まずいカプセル化」の区別がない、という状況に陥っています。
「データとロジックを一体に」している例
データとロジックを一体にするという主張を端的に実践している例として、本書p.37/位置No.759最下部に掲載されているコード例(改善後)を取り上げてみましょう:
Money amount(Money unitPrice, Quantity quantity) { if(quantity.isDiscountable()) return discount(unitPrice, quantity); return unitPrice.multiply(quantity.value()); }
これに対して改善前のコード例(p.37中ほど、位置No.738)は以下の通りです:
int amount(int unitPrice, int quantity) { if(quantity >= discountCriteria) return discount(unitPrice, quantity); return unitPrice * quantity; }
本書のこの箇所での主張は「「型」を使ってコードをわかりやすく安全にする」ということですから、改善のポイントはintで表現されていた金額、単価、数量にそれぞれ適切な型を付与したことですが、いま注目したいのはその点ではありません。
改善後のコードではQuantityクラスの導入に伴いisDiscountable()というメソッドが設けられ、そこに、値引き条件判定のロジックが移されています。これは、「データとロジックを一体に」という本書の一貫した主張に従っており、良い例として提示されていることはあきらかです。
私が提起したい論点は、これが本当に良い例なのかということです。
isDiscountable()メソッドを設けることによって、値引き可否判定ロジックは、それが用いる「数量」という変数とともに、Quantityクラスに閉じ込められます。これは確かにカプセル化ではあります。しかし本当にこれは妥当な設計なのでしょうか。どういう点でこのカプセル化は有用なのでしょうか。
情報隠蔽
その問いに答える準備として、一歩下がってカプセル化の意義について確認しておきましょう。
一般にカプセル化の目的は「情報隠蔽」にあると言われます。両者をあえて区別せず、カプセル化とは情報隠蔽のことだと考える論者もいるようです。この「情報隠蔽」という概念は1972年にデイビッド・パーナスにより提唱されました。その論文「システムをモジュールに分割する際に用いられるべき基準について」はWebで閲覧できます。
この論文でパーナスが言っている隠すべき情報とは、インスタンス変数のような具体的な「データ」のことではなく「設計上の意思決定に関する知識」です(同論文p.1056「The Criteria」)。情報隠蔽というより知識の隠蔽なのですね。
例として、スタックを表すオブジェクトを書くことを考えましょう。
スタックは、複数のデータを順に入れる(pushする)ことができ、入れた順の逆順にデータを取り出す(popする)ことが出来る、いわゆる「後入先出方式」のデータ保管容器です。
スタック内部でデータを実際に保持するためのデータ構造としては、配列や連結リストなどの選択肢があり得ますが、スタックに対する操作をpush()とpop()に限定しておけば、内部構造としてどれをとっても、あるいは、ある日思い立って配列から連結リストに切り替えても、スタックを使用している側のコードには影響を与えません。
情報隠蔽とはこのようなものです。
Quantity はどんな知識を隠蔽しているか
さて、情報隠蔽についておさらいをした目で、QuantityクラスのisDiscountable()メソッドを眺めてみましょう。これはどんな知識を隠蔽しているのでしょうか。
数量がいくつであれば値引き可となるのかという知識は隠蔽されています。元の式、quantity >= discountCriteriaでは、その点は顕わでしたが、isDiscountable()では隠されています。
一方で、このメソッドを設けたことで、「値引きは数量のみに基づいて行われる」という知識が開示されてしまっています。
値引の可否はたぶん受注入力画面上に表示したいでしょう。であるならば、その画面を表示するためのクラスのいずれかの箇所で以下のようなコードを書くことになるでしょう:
Quantity quantity = Quantity.valueOf( /* 数量フィールドの入力値 */input ); if (quantity.isDiscountable()) { // "値引き適用"と画面に表示 } else { // "値引き適用なし"と画面に表示 }
取引条件が変更され、数量だけでなく金額も含めて値引き適用可否を決める、となった場合、どうなるでしょうか。もとのamount()メソッドだけでなく、この受注画面のコードも変更しなくてはならなくなります。影響箇所の拡散です。
本書では、「データとロジックを一体にする」ことの目的として「業務ロジックを重複させない」ということを強調していますが(p.77/位置No.1294)、このケースではむしろ逆の結果をもたらしています。
開示してよい知識/隠蔽したい知識
上記のコードが問題を含んでいる原因は、このコードが「値引きは数量のみに基づいて行われる」という知識を適切に隠蔽していないことにあるわけです。
本来は、値引き判定のロジックをどのオブジェクトに配するかを決めるにあたって、どのような知識を隠蔽すべきか、あるいは裏返して言えば、どのような知識は開示して構わないかという点に思いをめぐらすべきでした。
値引き条件などというものは、ビジネス上の都合により変更されやすいものです。このケースのように注文数量だけで値引き可否が決まるというケースもあるかもしれませんが、発注金額も考慮し、あるいは発注者が上得意かどうかも判断要素に含める、というように変更されるかもしれません。一方で、注文数量・金額・発注者が誰かなども含む受注内容に応じて値引き可否が決まる、という点はたぶん変わらないだろうと考えられます。
であれば、このケースで開示してよい知識と隠蔽したい知識とは以下のようになるでしょう:
開示してよい知識
受注ごとにその内容に応じて値引き可否が決まるという知識。
隠蔽したい知識
注文数量・金額等にもとづく具体的な値引き決定ルール。
これを踏まえれば、isDiscountable()は、Quantityクラスにではなく、呼び出し元であるamount()メソッドを含むSalesOrder(受注)といったクラスのメソッドであるべきだということになります:
class SalesOrder { Money unitPrice; Quantity quantity; // ... Money amount() { if (isDiscountable()) return discount(unitPrice, quantity); return unitPrice.multiply(quantity); } // ... boolean isDiscountable() { return quantity.compareTo(discountCriteria) >= 0; } }
これに伴い、受注画面のコードは以下のようになります:
salesOrder.setQuantity(Quantity.valueOf( /* 数量フィールドの入力値 */input )); if (salesOrder.isDiscountable()) { // "値引き適用"と画面に表示 } else { // "値引き適用なし"と画面に表示 }
このコードであれば、上述のように取引条件が変更された場合も、SalesOrderクラスのisDiscountable()メソッドだけ修正すれば済むでしょう。値引き決定ルールが、SalesOrderクラスに完全に隠蔽されているからです。
ルール信奉の落とし穴
本書の帯には「トラブルを起こさないためにどう考え、何をすべきか?」と大書され、トラブルの例のひとつとして「1つの修正のために、あっちもこっちも書きなおす必要がある」という事象が挙げられています。
ここまで読まれた方には、「データとロジックを一体に」という単純なルールに従うだけでは、こうしたトラブルを避けることが出来ず、むしろ助長してしまう可能性すらあるということが、ご理解頂けたかと思います。
「データとロジックを一体に」というようなルールは白黒がつけやすく、それに従ってコードを作り替えていくことにはパズルのような楽しさがあるでしょう。でもそれはパズルであって設計ではありません。
本書で、このようなルール信奉は随所に見られます。例えば、本書の末尾近くではThoughtWorksアンソロジーの「オブジェクト指向エクササイズ」が推奨されています。このエクササイズは、いくつかの単純なルールに従ってコードを修正していくことで、オブジェクト指向らしい設計に馴染もうという試みです。私もこのような試みには意味があると思います。
しかし、それはあくまで「エクササイズ」として、なのです。原書では「Object Calisthenics」、直訳すれば「オブジェクト健康体操」とされています。本番の試合前に体を柔軟にしておこうということなのです。本番の試合中��健康体操をする人はいないでしょう。一方本書では、そうした位置づけが不明瞭で、こうした単純なルール集を、本番コードにも適用しようとしているように見えます。
オブジェクト指向への入り口という位置づけでなら、本書のようなパズル的なやり方もアリ、とお考えになる方もあるかもしれません。その方には、上述のisDiscountable()のように極めて簡単な例においてさえ、本書の推奨するアプローチが破綻していることを思い起こして頂きたい。初級者用のガイドラインとしても失敗しているのです。しかも実践者である著者ご自身、たぶん本ケースを、良くない設計の例と見ておられない。ルールに従ってパズルを解くとそれだけで達成感が得られるので、ルールを超える視点は頭の隅に追いやられてしまうという傾向が生じるのかもしれません。
設計の原則に立ち帰る
解決策は、「データとロジックを一体に」という、どちらかというとゲームのルールのような具体的で単純なルールから視点を引き上げ、「情報隠蔽(=知識隠蔽)」のような、より本質的な、目的志向的な設計原則に立ち帰って考えることです。
単純なルールを適用してコードを気軽に変更してみるのは大変よいことです。ただし、その結果が適切であったかは、必ず、より高い視点から評価されなければなりません。その視点のひとつが「情報隠蔽」です。単純なルールは、コード変更のきっかけにはなり得てもその正当性を保証しないのです。
一方、情報隠蔽といった抽象的な原則に従うには、相応の思考が要求されます。隠蔽されるべき知識が何か、というのは頭のひねりどころです。「値引って、数量だけでなく金額が関係してくる可能性もあるよなあ。お得意様なら条件が違うかも」といった良識あるいはドメイン知識も必要です。こうした知識は必ずしも「難しい」ものではありませんが、パズルのルールで代替できるものでもありません。
しかし、与件として与えられた問題を一定のルールの枠内で解くだけではなく、この場で何を隠蔽し何を開示すべきかといった新たな問いかけを生み出すことも含めて、問題と解の関係性を、描いては消し描いては消ししながら重層的に深化させていくことが、そもそも、設計という活動の本来の姿なのではないでしょうか。
杉本 啓 @sugimoto_kei 経営管理基盤ソフトウェア fusion_place の、プログラマ兼設計者 http://www.fusions.co.jp
追記:これはサンプルコードが悪いのか?
このエントリへの批判として、これは単に取り上げたサンプルコードが悪かったのでは、というものが散見されます。 たまたま出来が悪かった部分を、重箱の隅をつつくように批判してもしようがない、ということかと思います。
私は、それはあたらないと思っています。 私がこのサンプルコードを取り上げたのは、これが増田氏の目からみてもご自身の考えをよく体現したコードだろうと考えたからです。
このサンプルコードが記載されているページ(p.37/位置No.759)で増田氏が実演されているコード改善(リファクタリング)は、以下のように2ステップに分解することが出来ます:
ステップ1: isDiscountable(Quantity) というメソッドを抽出
ステップ2: 同メソッドを Quantity クラスに移動
ここで、2ステップ目を実行しなければ、私からの批判は成り立ちません。 それなのにあえてステップ2を実行されたのは、この本の以下のような主張に沿うならば���れが必然だからだと思います:
業務ロジックをデータを持つクラスに移動する(p.79)
使う側のクラスに業務ロジックを置き始めたら設計を見直す(p.80)
メソッドは必ずインスタンス変数を使う(p.81)
例えば、ステップ1の段階で止めると、isDiscountable(Quantity)は引数しか使わないことになり、上記3点目の規則に明らかに反するのです。 これとは別に、Quantityなど値オブジェクトに業務ロジックを持たせることはこの本の随所で推奨されています(例えば、p.86-87)。 これは、この本の一貫した主張である「データとロジックを一体に」の具体的な適用ケースです(p.87)
こうしたことを考えれば、このコード例が書き損じや考慮不足ではなく、増田氏の視点からみて良く練られたものであることをご理解頂けると思います。
8 notes · View notes
phpmentors · 7 years ago
Text
DCI Tokyo 1 - Lean Architecture by James Coplien - が開催されました(前編)
1月10日に六本木ヒルズにて、James Coplien氏をお招きしてLean Architectureに関する勉強会を開催しました。UUUMさんに大変素敵な会場を提供頂き、スタッフ含めて40名前後の参加者が集まりました。
Tumblr media
このブログでは、当日の翻訳担当を務めた@remoreと@ganchikuが、当日の翻訳内容から抜け落ちた部分の捕捉を含めて、内容のアウトラインを簡単に振り返れればと思います。セッションの前半部分は@remoreが、後半は@ganchikuが解説を担当します。
なお当日のCoplien氏によるセッション内容は、許可を得た上でYouTubeにアップロードされていますので、より深くご覧になりたい方はこちらも併せてご参照下さい。
DCI Tokyo 1 - Lean Architecture by James Coplien (Part 1 of 2)
DCI Tokyo 1 - Lean Architecture by James Coplien (Part 2 of 2)
Form(形態)とLean Architecture
形態は機能に従う(Form Follows Function)という言葉があります。Googleで検索してみると、元々はアメリカの建築家ルイス・サリヴァンの言葉であると出てきます1。この言葉は建築やプロダクトデザインなど様々な領域で引用されている言葉ですが、ソフトウェア・エンジニアリングの世界でもしばしば引用されているようです。今回のセッション前半では���前回の内容を簡単におさらいしつつ、この「形態」に焦点を当てた内容が主に展開されました。
前半部分のセッションの内容について、独断と偏見で要点を書き出してみました。なるべく短く抜き出してみたつもりなのですが、各トピックの内容が濃いせいか、結果的にそれなりの長さになってしまいました:
1. Form(形態)とStructure(構造)の違い
Formは抽象的であり、Structureは具体的である
複数のFormをよく見ていくと、パターン認識によって共通性と可変性という特徴を抽出することができる
ArchitectureはFormに関すること
Formは対称性を強調する
人間の脳は共通性を見つけることに秀でている
2. 現代のプログラミング言語とOOPについて
共通性と可変性のペアをパラダイムと呼ぶ
プログラミング言語はこれらのパラダイムのうちいくつか限られた数だけを言語の設計思想に取り入れているに過ぎない
例えばOverloadingといったOOPのプログラミングテクニックはパラダイムの一つと言えるし、その他の言語が持つ機構や機能(例えばC++の場合#ifdef, Template、class, 継承といった言語が提供する機能)は共通性と可変性のペアをForm(形態)において表現しているだけとも言える
これを実際に実装していくとStructure(構造)の話となるが、設計において我々がどうそれを捉えているかというと、あくまでFormの形で捉えている
C++を作ったStroustrup氏は、C++をオブジェクト指向言語とは呼ばずマルチパラダイム言語だと呼んでいる
オブジェクト指向は一つのパラダイムに過ぎないのに、「オブジェクト指向分析」で全ての問題を解決しようと考えだすからおかしくなる
C++はオブジェクト指向以上のことが実現できる言語
3. マルチパラダイムデザインがどこで始まり、Lean Architectureがどこからきたのか
Lean Architectureでは2種類の視点でFormを捉えている
What the system is(共通性や可変性を分析する)
What the system does(振る舞いや"間"で見る)
建築家は「形態は機能に従うか?(Does Form Follows Function?)」という点に関して各々の主張を持っている
建築家アドルフ・ルースは「形態は機能に従う」と唱えた
ソフトウェアアーキテクトもこれを唱えがちだし、ユースケースを分析すれば最適なアーキテクチャも見つけられると考えがちであるが、これは完全に間違いである
銀行口座送金の例を見ると、振る舞いを表現するために適切なものはオブジェクトIDでもクラス名でもなく、Roleであることが分かる
機能の形態(Form of Function)はRoleの中にある
Roleという概念が一級市民として存在しており、オブジェクトがRoleを動的に演じることができるようなプログラミング言語を想像してみよう。機能の形態をコードで表現することができ、コードを読むだけでユースケースを理解できる。これがDCI。
ここでいう"機能の形態"は、オブジェクト間のやり取りや振る舞いといった"What the system does"の部分にあたるもののこと
前半サマリは以上となります。なお、動画の54:43頃から始まる対称性や幾何学の話を起点に展開される、現代のシステム開発におけるオブジェクト指向言語のあり方と利用のされ方に対する強い批判は、要約ではとても表現しきれる内容ではなくかつオススメの内容のため敢えて上記には含めていません。動画全てを見る時間が取れない方でも、英語と内容の両方が一部難解な部分もありますが2それでも、ぜひ一度ご覧頂くことをオススメします。
To Be Continued
更に興味がある方がいらっしゃいましたら、今回のセッションの参加ブログを書いて頂いた方もいらっしゃいますので、こちらも併せてご参照頂けると理解がより深まるかと思います。@ganchikuによるセッション後半の解説記事も追ってこちらのブログで配信予定となっております。また、Twitterでは #dcitokyo というタグで本勉強会についての過去のツイートを検索できますのでこちらもオススメです。
Jimは年内にまた来日予定があるとお話されていましたので、次回日程等決まりましたらまた告知などを行っていければと考えています。その際に、最近開設されたDCITokyoの公式Twitterアカウントから各種告知等されていく予定となっていますので、今後こちらもぜひご注目ください。
当日Jimはルイスと同年代の建築家アドルフ・ロースの言葉と紹介していたた��、要出典確認 ↩︎
55:46頃にJimが解説しているオペレーショナルモデルについて当日私の方で通訳として十分に説明できなかった部分については、追ってTwitterで後追いする形で一部捕捉の説明を試みていますので、こちらもご参照ください ↩︎
0 notes
phpmentors · 8 years ago
Text
「Lean Architecture / DCI Evening」参加レポート
2017年10月18日、James Coplienさんとその奥様であるGertrud Bjørnvigさんをお招きして、「Lean Architecture / DCI Evening 」というイベントを開催しました。日本ではソフトウェアパターンやアジャイルのリーダーとして知られるJames Coplienさんは、『 マルチパラダイムデザイン 』(1998年)でドメインとドメイン間の関係を中心に据えた設計パラダイムを提唱していました。Coplienさんは2009年、MVCアーキテクチャの考案者である Trygve Reenskaug さんと共に「DCIアーキテクチャ」を発表しました。2010年、CoplienさんはGertrudさんとともに書籍『 Lean Architecture 』を上梓、トヨタ生産方式をソフトウェアアーキテクチャに適用するリーンアーキテクチャについて、DCIアーキテクチャをそのビルディングブロックと位置づけた具体的な方法を記述しました。今回、マルチパラダイムデザインの読書会を大阪で開催していた縁から、表題のイベントを開催することができました。この記事ではセッションの内容についてトークを再現する形でお伝えしたいと思います。
NOTE: (※)内は筆者によるコメントです。また、基本的に英語から修正せずに翻訳しているので、事実関係に関しては確認がとれていないものもあります。
CoplienさんによるDCIアーキテクチャの概説
Coplienさん(以下、C): 最初にクイズをします。みんな立って。DCIは、DCIと呼ぶ前はなんと呼んでいたか知っていますか?・・・DCA (Data-Collabolation-Algorithm)(※関連リンクを参照)です。知ってた人はそのまま立って。これを差し上げます。(※参加者の一人がサイン付きのLean Architectureをプレゼントされる)
C: (※DCIで開発するための言語基盤とIDEを提供する)Trygveのプログラムの拡張子は「.k」になっているのはなぜでしょう?これは哲学者のカントからとっている。
C: オブジェクト指向プログラミングができない、モダンな言語はなんでしょう?Rust?Java。OK、それだ。Javaはオブジェクト指向ではなく、クラス指向の言語だ。
C: DCIのWebサイトfulloo.infoにいろんなリソースがあります。サンプルコードとかビデオとかも入っている。
C: 今日はインフォーマルな会です。小さいDCIのチュートリアルをすることもできるし、私が最近どういう研究をやっているかを話すこともできる。DCIが実際にどうやってプログラマーの集中力を高めてエラーを取り除くことができるかということを話してもいいし、Trygve言語の実際の例もお見せすることができる。見た目はJavaみたいだけどね。そこには、コンテキストとロールも登場する。設計の話もしてもいいし、ドメイン分析の話をしてもいい。
参加者A: ドメイン分析とTrygve IDEも見たい。私たちは大阪で「マルチパラダイムデザイン」の読書会をしていました。MPDとDCIをどのように繋ぐかの話も聞きたい。
C: 今日は何人、大阪から来たの?Oh! DCIを殆ど知らないという人は?最初に小さいインタラクションをしましょうか。15分くら��。
C: マルチパラダイムデザインは、クラスベースの言語に基礎を置いて議論を展開している。DCIはリアルなビルディングブロック(構成要素)とオブジェクト群のネットワークです。そこが大きな違いです。ドメイン分析とかユースケース、ロールのアクターに関しては後ほど妻が説明してくれる。
C: まずはDCIの触り。簡単なデモを。銀行口座間の送金のケースを考えてみる。ソフトウェア上ではそれはどういう状態でしょうか。クラスを書いて、オブジェクト同士をインタラクションさせますね。クラスを書くときというのは、クラスの全てのメソッドを考慮して書��必要がある。
C: ATMの例を考えてみましょう。ATMが稼働していて、それを確認するために、コンソールにオブジェクトIDをプリントするプログラムを考えてみましょう。
(※メソッドが呼ばれるたびにオブジェクトIDがプリントされていくデモ)
C: これを見て何かパターンは見つかるでしょうか? 単に同じオブジェクトが延々と呼ばれていることがわかるだけですね!(※会場笑)呼ばれるオブジェクトの順番とかは全然重要ではなかったわけです。
C: 次にオブジェクトIDではなく、クラス名が表示されるようにしてみましょう。クラス名は重要ですよね。コンソールを見てパターンは見つかりますか?ないですね。これもまだ完璧ではないようです。クラスのうち、2つは口座に関するもの、最後の3つ目は通貨に関するものだということはわかります。しかし、アーキテクチャの振る舞いはクラスだけでは表現されていないようです。
C: さて、今度はメソッドが呼ばれる毎に、役割(※ロール)がプリントされるようにしてみますよ。・・・Ah ha!(※会場笑) 分かりましたね。お金を送金するときの構造はこういうふうになりますよね。構造は役割(※ロール)の中に入っています。
C: 1つの銀行に2つ以上の口座を持っている人はいますか?(※会場から手が)それでは、私に1つの自分の口座から他の口座に送金するユーザーストーリーについて説明して下さい。
参加者B: ある口座からある口座に送金します。妻にダイアモンドを買うために。貯蓄口座から普通口座へ。
C: もっと一般的に言うと?
参加者B: 振込元から振込先へ送金する。
C: 振込元口座を持っている人はいますか?それは、オブジェクトでしょうか?クラスでしょうか?なんでしょうかね、振込元口座みたいな役割とは?・・・人はこれを役割(ロール)として考えているわけですよね。クラスやオブジェクトとして役割(ロール)を考えてはいないですよね。
C: アラン・ケイの最初に考えていたオブジェクト指向というのはヒューマンメンタルモデルをコンピュータに適用させたものでした。そのメンタルモデルを今発見したのです!
参加者B: どうやって、口座の振込元とか振込先とかを判別するんでしょうか?
C: ピアジェという発達心理学者がいます。ピアジェに「オペレーショナルモデル理論」というのがあります。ビルディングブロック(※構成要素)を使って論理的に考える必要があります。オブジェクトやクラスではないものがあるのです。おそらく私たちは最初にオブジェクトを学び、その後にクラス・クラス化を学ぶようです。子どもが一番最初にオブジェクトを学ぶ時は、全てを自分自身として認識するようです、母親さえも自分だと思う。4-7ヶ月の間に区別をつけることができるようになっていく。小さな子どもは馬を見た時に、馬を指差して「犬!」と叫ぶ。(※あ〜!と会場共感の声)こういった認識は物事に共通性を見出していることによって成り立っています。私たちはプログラムを問題解決のために書いています。子どもは8ヶ月くらいの時に原因と結果という因果関係について学ぶ。その頃から机を押したり、上にあるものを叩いたりして何かが落ちたりするのを楽しむ。そうして最終的には、問題解決する能力を学習していく。それがオペレーショナルモデルと呼ばれるものです。
C: プログラムを通して問題解決するためにこの考えが必要ですね。プログラムを書くときにはどうやっているのでしょう?ここにこのようにオブジェクトが散らばっている。そしてオブジェクトをつなげるユースケースがある。これはまた別のユースケースですね。これはまた別のユースケース、また別のユースケース・・・(※会場笑)ユースケースはオブジェクトのシーケンスではないんですよね、こう見ると。順番があるわけではない。
C: では、ユースケースのコードをオブジェクトよりも上のレイヤーに取り出す、ということをやってみましょう。今、ユースケースのコードは、オブジェクトのクラスの中にあります。このユースケースの部分を、クラスの外側に出すのです。そうすると、オブジェクトの方は基本的なクラスのインスタンスのままなので、とても単純です。しかし、これらは実際のところユースケースの一部にはなりません。では、ユースケースの部品は何でしょうか? ロールですね。ユースケースの部品は、ロールの中にあります。開発者は、このようなロールの中からいくつかを選んでまとめます。このまとまりを、コンテキストと呼びます。 つまり、コンテキストがユースケースに相当します。そして、ユースケースを実行するには、サブジェクト(※ユースケースの入り口となるエンティティ、後述のサンプルコードのTransferMoneyContext.SOURCE_ACCOUNTに代入されるオブジェクトにあたる)にこれらのメソッド(※ロールのメソッド)をインジェクトします。このオブジェクト(※他のエンティティ、後述のサンプルコードのTransferMoneyContext.DESTINATION_ACCOUNTに代入されるオブジェクトにあたる)にもこれらのメソッドをインジェクトします。
C: オブジェクトは既にあるからユースケースを書きたい。例えばヘリコプターを飛ばすことについて考えてみましょう。オペレーショナルモデルで思考して、ロールを考えます。映画の「マトリックス」を見たことがあるますか?トリニティはヘリコプターを飛ばさなければいけなかったですよね。ヘリコプターを操縦するプログラムをダウンロードしていました。ロールのプログラムをダウンロードすることによってヘリコプターを飛ばすことができたわけです。ランタイムでオブジェクトに対してどのようにユースケース上で振る舞うのかを教えてあげる。ユースケースが終わると、オブジェクトは何をやるか、何をやったかを(※どのように振る舞うかを)忘れてしまうわけです。
参加者B: トリニティはオブジェクト(※エンティティ)、パイロットはロールということですね。
C: トリニティは、ヘリコプターを操縦するためのメソッドを持っている。手を動かしたり、足を動かしたりといった基本的なトリニティが持っているメソッドを使うことができる。コンテキストがヘリコプターを運転させる。トリニティは基本的な動作はできる。コンテキストとトリニティの間のメソッドの契約というものが必要で、トリニティは人なので人が持っているメソッドだけが使えるという契約がある。手を動かすとか。ヘリコプターを操縦するというのははコンテキストです。ロールはデータを持たない。純粋にメソッドを持っている。以上がDCIの基本的な話でした。
参加者C: ロールとクラスの違いがまだわからない。
C: クラスはデータを持ちます。あなた達はいわば人間クラスのインスタンスですよね?そこから初期化されている。(※会場笑)ヘリコプターパイロットはロールなので、ヘリコプターパイロットのインスタンスを作ることはできない。人間が必要なんです。あるいはロボット?もしくはゴリラ。
参加者C: Javaのインタフェースはロールに近い?
C: 似てるとは言えるけど、実装がないからロールを完全に表すかというと違うと思います。(※Java 8��降はインタフェースは実装を持てるようになりましたが)あと、Javaはインタフェースは静的に決まっていて、動的に決めることができないというところが異なります。クラスがインタフェースをextendするというのを全てコンパイル時に決めなければなりません。
参加者D: Swiftのプロトコルエクステンションを使うと近いことができますか?
C: Objective-Cでお腹いっぱいなので、Swiftの学習は避けているものでね(※会場笑)プロトコルエクステンションを使うと近いことができるようです。
参加者C: クラスとロールの違いについて。Java クラスとインタフェース、Scala クラスとトレイト、C++ 動的クラス、Virtualクラス これらはDCIの実装として使うことができるか。
C: Scalaのトレイトはかなり近いです。Scalaではトレイトのミックスインを使う。トレイトを使ってクラスに合成できる。その結果クラスは両方の機能のメソッドを持つことができる。C++ではどうするか?C++の場合はクラスの合成に継承を使ってしまうので難しい。どうするか?ロールをテンプレートを使って書くことができる。テンプレートを使えばDCIを実現することは可能です。ただそれはコンパイルのタイミングになってしまう。ダイナミックではない。fulloo.infoも見てくださいね。なんとなく、DCIのイメージが掴めましたか?
C: DCIのための言語として気に入ってるものの一つはRubyです。オブジェクトの拡張が簡単にできる。モジュールを使ってミックスインすることもできます。ただ、メソッドは自由に追加することはできるが、それを取り外すのが難しい。名前の衝突の問題もある。Matzはこの問題について理解してくれたが、一度インジェクトしたものを引き剥がす機能を入れることまでは、説得するには至らなかった。Rubyの仮想マシンを変更すればできるのですがね。
(※実は筆者はRubyistなのですが、refinementを使うとある程度は目的に適うのではないのかなーと思ってしまいました)
C: Trygveを使ってる人。(※一人だけ手が挙がる)オープンソースですよ!(※会場笑) お金の送金のサンプルを見せていきます。ここにクラスがあります。これはドメイン分析から抽出されたものです。 amount()(金額) などのメソッドを持っている。クラス指向のプログラミングならこんなものでしょうか?Trygveの紹介をしていきますね。
(※以下のコードはTrygve で書かれた送金のサンプル https://github.com/jcoplien/trygve/blob/master/examples/july_money_transfer.k より引用)
/* * july_money_transfer.k */ class Currency { public Currency(double amount) { amount_ = amount.clone } public Currency +(Currency amount) { assert(false) return this; } public Currency -(Currency amount) { assert(false) return this; } public Currency() { } public String name() const { assert(false); return "" } public String sign() const { assert(false); return "" } public double amountInEuro() const { assert(false); return 0.0 } public double amount() const { return amount_ } public String toString() const { return amountInEuro().toString() } public int compareTo(Currency other) { if (amount() > other.amount()) return 1 else if (amount() < other.amount()) return -1; return 0 } private double amount_ } class Euro extends Currency { public Euro(double amount) { Currency(amount) } public Euro -(Currency amount) { return new Euro(amount() - amount.amountInEuro()) } public Euro +(Currency amount) { return new Euro(amount() + amount.amountInEuro()) } public String name() const { return "Euro"; } public String sign() const { return "€"; } public double amountInEuro() const { return amount() } public String toString() const { return amount().toString() } } class Account { public Account(int acctno) { acct_ = acctno } public String accountID() const { return acct_.toString() } public Currency availableBalance() const { assert(false); return null } public void increaseBalance(Currency amount) { assert(false) } public void decreaseBalance(Currency amount) { assert(false) } public void updateLog(String message, Date dt, Currency amount) { assert(false) } private int acct_ } class CheckingAccount extends Account { public CheckingAccount() { Account(1234); availableBalance_ = new Euro(100.00) } public Currency availableBalance() const { return availableBalance_ } public void decreaseBalance(Currency c) { availableBalance_ = availableBalance_ - c } public void updateLog(String message, Date t, Currency c) const { System.out.print("account: ").print(accountID()) .print(" CheckingAccount::updateLog(\"").print(message) .print("\", ").print(t.toString()).print(", ") .print(c.toString()).print(")") .println() } public void increaseBalance(Currency c) { availableBalance_ = availableBalance_ + c } private Currency availableBalance_ } class SavingsAccount extends Account { public SavingsAccount() { Account(1234); availableBalance_ = new Euro(0.00) } public Currency availableBalance() const { return availableBalance_ } public void decreaseBalance(Currency c) { assert(c > availableBalance_); availableBalance_ = availableBalance_ - c } public void updateLog(String logMessage, Date timeOfTransaction, Currency amountForTransaction) const { assert(logMessage.length() > 0); assert(logMessage.length() < MAX_BUFFER_SIZE); // assert(new Date() < timeOfTransaction); System.out.print("account: ").print(accountID()) .print(" SavingsAccount::updateLog(\"").print(logMessage) .print("\", ").print(timeOfTransaction.toString()) .print(", ").print(amountForTransaction.toString()) .print(")").println() } public void increaseBalance(Currency c) { availableBalance_ = availableBalance_ + c } private Currency availableBalance_; private int MAX_BUFFER_SIZE = 256 } class InvestmentAccount extends Account { public InvestmentAccount() { Account(1234); availableBalance_ = new Euro(0.00) } public Currency availableBalance() const { return availableBalance_ } public void increaseBalance(Currency c) { availableBalance_ = availableBalance_ + c } public void decreaseBalance(Currency c) { availableBalance_ = availableBalance_ - c; } public void updateLog(String s, Date t, Currency c) const { System.out.print("account: ").print(accountID()) .print(" InvestmentAccount::updateLog(\"") .print(s).print("\", ").print(t.toString()) .print(", ").print(c.toString()).print(")") .println() } private Currency availableBalance_; } class Creditor { public Creditor(Account account) { account_ = account } public Account account() { return account_ } public Currency amountOwed() const { return new Currency(0.0) } private Account account_ } class ElectricCompany extends Creditor { public ElectricCompany() { Creditor(new CheckingAccount()) } public Currency amountOwed() const { return new Euro(15.0) } } class GasCompany extends Creditor { public GasCompany() { Creditor( new SavingsAccount()); account().increaseBalance(new Euro(500.00)) // start off with a balance of 500 } public Currency amountOwed() const { return new Euro(18.76) } } context TransferMoneyContext { // Roles role AMOUNT { public Currency(double amount); public Currency +(Currency amount); public Currency -(Currency amount); public String name() const; public String sign() const; public double amountInEuro() const; public double amount() const; public String toString() const; public int compareTo(Currency other) } requires { Currency(double amount); Currency +(Currency amount); Currency -(Currency amount); String name() const; String sign() const; double amountInEuro() const; double amount() const; String toString() const; int compareTo(Currency other) } role GUI { public void displayScreen(int displayCode) } requires { void displayScreen(int displayCode) } role SOURCE_ACCOUNT { public void transferTo() { // This code is reviewable and meaningfully testable with stubs! int SUCCESS_DEPOSIT_SCREEN = 10; // beginTransaction(); if (this.availableBalance() < AMOUNT) { // endTransaction(); assert(false, "Unavailable balance") } else { this.decreaseBalance(AMOUNT); DESTINATION_ACCOUNT.increaseBalance(AMOUNT); this.updateLog("Transfer Out", new Date(), AMOUNT); DESTINATION_ACCOUNT.updateLog("Transfer In", new Date(), AMOUNT); } // GUI.displayScreen(SUCCESS_DEPOSIT_SCREEN); // endTransaction() } } requires { void decreaseBalance(Currency amount); Currency availableBalance() const; void updateLog(String msg, Date time, Currency amount) } role DESTINATION_ACCOUNT { public void transferFrom() { this.increaseBalance(AMOUNT); this.updateLog("Transfer in", new Date(), AMOUNT); } public void increaseBalance(Currency amount); public void updateLog(String msg, Date time, Currency amount) } requires { void increaseBalance(Currency amount); void updateLog(String msg, Date time, Currency amount) } public TransferMoneyContext(Currency amount, Account source, Account destination) { SOURCE_ACCOUNT = source; DESTINATION_ACCOUNT = destination; AMOUNT = amount } public TransferMoneyContext() { lookupBindings() } public void doit() { SOURCE_ACCOUNT.transferTo() } private void lookupBindings() { // These are somewhat arbitrary and for illustrative // purposes. The simulate a database lookup InvestmentAccount investmentAccount = new InvestmentAccount(); investmentAccount.increaseBalance(new Euro(100.00)); // prime it with some money SOURCE_ACCOUNT = investmentAccount; DESTINATION_ACCOUNT = new SavingsAccount(); DESTINATION_ACCOUNT.increaseBalance(new Euro(500.00)); // start it off with money AMOUNT = new Euro(30.00) } } context PayBillsContext { public PayBillsContext() { lookupBindings } role [] CREDITORS { } requires { Currency amountOwed() } stageprop SOURCE_ACCOUNT { public String accountID() const; public Currency availableBalance() const; public void increaseBalance(Currency amount) unused; public void decreaseBalance(Currency amount) unused; public void updateLog(String message, Date dt, Currency amount) unused } requires { String accountID() const; Currency availableBalance() const; void increaseBalance(Currency amount); void decreaseBalance(Currency amount); void updateLog(String message, Date dt, Currency amount) } // Use case behaviours public void doit() { for (Creditor credit : CREDITORS) { // Note that here we invoke another Use Case TransferMoneyContext xfer = new TransferMoneyContext( credit.amountOwed(), SOURCE_ACCOUNT, credit.account()); xfer.doit() } } private void lookupBindings() { // These are somewhat arbitrary and for illustrative // purposes. The simulate a database lookup InvestmentAccount investmentAccount = new InvestmentAccount(); investmentAccount.increaseBalance(new Euro(100.00)); // prime it with some money SOURCE_ACCOUNT = investmentAccount; Creditor [] creditors = new Creditor [2]; creditors[0] = new ElectricCompany(); creditors[1] = new GasCompany(); CREDITORS = creditors } } { // Main TransferMoneyContext aNewUseCase = new TransferMoneyContext(); aNewUseCase.doit(); PayBillsContext anotherNewUseCase = new PayBillsContext(); anotherNewUseCase.doit() }
C: Account(口座) という別のクラスがあります。ドメイン分析から抽出されたものです。口座番号というデータを持っている。メソッドは increaseBalance()(残高増) と decreaseBalance()(残高減) 。口座番号を与えれば、このメソッドを使っていろんなことができる。いわば、ちょっと高級な Integer みたいなものです・・・。全くもって良くないですね!貯金用の口座とか、投資用の口座とかどんどん増えていってしまいます。会社の口座、お金の債権者… ここまでができの良くないクラス指向のプログラミングのやり方でした。
C: さて、それでは送金のユースケースについて見ていきましょう。Context がキーワードです。多くの点でクラスのように見えるのだけれど、クラスとは異なっていて、ほとんどの場合データを持たずにロールだけを持っている。 Amount(金額) と呼ばれるロールはとてもシンプルなロールです。これで資金移動をすることができる。
C: (※デモを見せて)これがロールのインタフェースのシグネチャです。オブジェクトがこれらのメソッド全てをサポートするように定義する必要があります。これを「必須の契約」(※the requires contract)と言います。オブジェクトがロールを表現する場合、これらのメソッドを全てサポートしなければならない。
参加者C: ここで言う契約とは、「契約による設計」の契約のことですか?
C: その通りです。バートランド・メイヤーという人が提唱した契約による設計。この人の言ってることは、だいたい合っているけど突き詰められていない。
参加者B: メイヤーの考えのどこが機能しないんでしょう?
C: クラスAがあり、そこにメソッド1があります。そこには事前条件、メソッド実行、事後条件があります。メソッド1は事前条件が真であることを要求し、事後条件が保証されていることを約束します。次にメソッド1の中からメソッド2を呼ぶケースを考えてみましょう。メソッド2にとっての事前条件を見た時、メソッド2にとってはメソッド1が状態を維持してくれているという想定をしているわけですが、メソッド1の実行中に自己隠蔽されている状態の中で条件がおかしくなった状態でメソッド2を呼ぶとメソッド2が結果保証している事後条件が壊れてしまうことがある。
C: クラス側で本体となる実装を持たないのは、ロールのプレイヤーの持っている実装を使うだけだからです。ロールを演じるオブジェクトの実装を使います。別のオブジェクトに由来するメソッドを使うわけです。これらのメソッドはパブリックインタフェースで、ロールが何をするかを知ることができます。
C: よりわかり易い例として SourceAccount(振込元口座) と呼ばれる別のロールも見ていきましょう。SourceAccount は transferTo()(〜に送金する) として呼ばれる��ソッド、責務を持っています。今からこの箇所のコメントを外して動くようにしましょう。トランザクションを開始しますよ。もし振込元口座が利用可能な残高を持っていれば、コメントを外した箇所は何になるでしょうか?
参加者B: ロールを注入されたオブジェクト
C: そうです! SourceAccount ロールを適切に演じるオブジェクトです。利用可能な残高はどこでしょうか?気をつけてください。ここ(※ロール)にはありませんよ。残高は、ロールを演じる口座オブジェクトの側、ロールプレイヤーにあります。
C: クラスみたいだけれど、ランタイムで考えられるようにしてくれます。SourceAccount ロールを演じるオブジェクトについて考えてほしいです。ロールプレイヤーはマトリックス(※コンテキストが持つロールとオブジェクトのマッピング表)によってもたらされるものです。ロールがオブジェクトに注入されると、オブジェクトが機能を持つようになって、送金ユースケースを実現できるようになります。送金メソッドは振込元の Account オブジェクトに注入されます。
C: こちらは別のロール、DestinationAccount(振込先口座) です。DestinationAccountはメソッド transferFrom()(〜から送金される) を持ちます。transferFrom() メソッドは DestinationAccount の残高を更新します。
C: コンテキストのコンストラクタを見てみましょう。TransferMoneyContext(送金コンテキスト) です。コンテキストの中で何がオブジェクトかを伝える必要あります。送金の金額、振込元、振込先、口座の種類。
C: マトリックス(※コンテキストが持つロールとオブジェクトのマッピング表)を表すオブジェクトを持っていて、ロールのメソッドが振込元の Account オブジェクトに注入されます。要はこれ(※コンテキスト)はオブジェクトだと言うことです。Account オブジェクトに対して振込元口座にあるべきメソッド群を与えていきます。送金ユースケースのためにロールプレイヤーが演じるメソッド群です。そうすれば、ランタイムでオブジェクトを組み立てることができます。
C: 他のユースケース、お金を支払うについて見ていきましょう。特別なロールがあります。そして、Amount や SavingsAccount(貯蓄口座) のようなオブジェクトが新たに一つここにあります。これらは、プラグアンドプレイをするために用意したアクターです。私が台本を書いたロールがあり、アクターは芝居においてロールを演じます。
C: これは舞台のようなもので、大道具(stage props)はアクターの持っている道具です。大道具だけでは、一切何もできません。ただデータを引き出すことはできます。
参加者B補足: 何かをすることはできないのだけど、データを引き出すことはできる。
C: このロールの全てのメソッドは const で、アクターの状態を変更することはできません。
参加者B: const というのはロールの型かなにかですか?
C: そうです。ロールの束縛、制約の種類です。
C: さて、stage props (大道具に由来する機構名) の使いどころはわかりますか?オブジェクトは2つのロールを一度に演じるかもしれません。同じオブジェクト、同じインスタンスで二役を一度にこなすことがあります。とても複雑な例として再帰実行になるケースもありえます。これが先程上がっていた問題の一つで、複数回呼び出したら事前条件が崩れることがあるという例です。このstage propsはデータを更新しないから、問題が起こらないようにすることができます。
C: ちょっと難し目の例を見てみましょう。Javaのグラフ描画のデータ構造です。ブロック崩しと言われるピンポンゲームです。変数の中をチェックするデバッガも入っています。700行くらいのプログラムでこれができます。
C: MPDからDCIへどう変遷していくかについて。よく知られているように、対象のドメインを理解することから始めます。殆どの場合、インタラクティブなプログラムはクラス指向のプログラムでできています。オブジェクト指向プログラミングとは、アラン・ケイが定義したように、オブジェクト同士の協調によるネットワークです。これをどうやって意識するかについて。
GertrudさんとCoplienさんによるユー��ーストーリー、アジャイルとリーンアーキテクチャ
Gertrud さん(以下、G): 皆さんはユーザーストーリーは知っていますか?ユースケースは?アジャイルになるためにはユーザーストーリーをたくさん作りましょう。リーンになるためには、ユースケースが必要。
G: ユーザーストーリーの形式(フォーム)として、以下のようなフォーマットで考えるといいです。
As a <USER or ROLE> 例: 口座を持っている人として I want <FEATURE> 例: ある口座から別の口座に移したいという行為 So I can <MOTIVATION> そうすればなにができるか。例えば、ダイアモンドを買ってあげることができる。
ユーザーストーリーの構成要素はこのようになっています。これをブレインストーミングなどでたくさん作っていく。 上から、Who、What、Why に対応している。
G: ユーザーロールには、口座を持っている人、銀行の従業員、(※例えば電気代を支払おうとする)市民などがいます。
G: お金の送金は、今回の場合、全てのロールで行うケースがある。しかし、それぞれのロールでやりたい事は違う。ロールやモチベーションによってコンテキストが変わってくる。実装する前に、モチベーションがわかるので、実装することができる。これは実装する前に知る必要があるんです。
G: ユーザーストーリーの洗い出しは、ブレインストーミングであげていく。するとかなり数が増えていく。そこから一般的な(※抽象度の高い)状態に近づけていく。そうすれば、ユーザーストーリーからユースケースに落とし込むことができる。
G: ユーザーストーリーは具体的で、抽象的なのがユースケース。どこまで抽象的にするべきか?コードで理解できるレベルまでです。ユースケースをリーダブルコードに落とし込めるところまで持っていくのが目標になる。
G: ユーザーストーリーはインプットなので、なるべくたくさんの実例を出したほうがいい。銀行の業務であれば、昔に使っていた小切手の話をユーザーストーリーとして抽出するくらい。
G: ユースケースの段階で要点を厳密に抽出する。これがそのままプログラムに使われるから。
(ここで休憩に入りました)
G: ユースケースで大事なことはターミノロジー(※用語)。システム分析とシステム設計で同じボキャブラリー、ターミノロジーを使ってお互いの同意を取っていくことが大事。関係者の間で合意をとるのも大事になってくる。
G: 分析と設計で、同じボキャブラリー、同じターミノロジーを使って話をするので、相互に一貫性をもたせることができる。もちろん違うチームに行くと言葉も変わっていく。
G: 銀行だったら銀行員にも話を聞かないといけない。ドメインエキスパートだけではなく、銀行員もチームに入って、彼らの使っている用語をコードに落とし込んでいく。ユースケースレベルになると、コードのステップレベルまで落とし込めるようになる。
G: コアシナリオとサテライトシナリオというのがある。コアシナリオはシナリオの中でも本道で大事な物。サテライトシナリオは、例外的だったり拡張されていたり複雑だったりするもの。それでも、システムはこういうものもカバーしなければならない。
参加者: なぜ、「サテライト」という用語にしたか?
G: オリジナルの用語は、インクルードとエクスクルードだった。その使い分けはわかりにくいので、コアシナリオとサテライトシナリオにした。また、デビエイション(※deviation: 逸脱、脱線、偏向)と一時期は呼んでい��。コアシナリオじゃないなにか、そんな感じ。
G: ユースケースという用語は昔ながらの用語で古臭い感じがするので使わない方向でというのはある
G: 感覚的にはコアシナリオがスコープ全体の80%、残りはサテライトシナリオという割合になる。
G: サテライトシナリオのほうがアジャイルっぽいやり方で進めていける。そもそも複雑だったり、変わりやすかったりするので。
G: 具体的なユーザーストーリーをユースケースに落としていく時に、どこまで一般化するのか。例えば、銀行で資金移動する時に使う一般的な用語は送金するという用語とか、そういうことを考えてちょうどいい落とし所を見つけていく。
C: 銀行口座の残高がマイナスになった時に、自動的に所有者の他の口座から振り替えてマイナスを補填するような、システム起動のユーザーが関与しない形でのシステムオペレーションを考える。MVCはエンドユーザーから見た、それに基づいたプログラミングモデル。DCIはプログラマーサイドのユーザーモデルを表現している。先程の、ユーザーがでてこないシステムオペレーションのようなシステムオペレーションも自然に扱うことができる。
G: ターミノロジーがとても大事。ターミノロジーのデータベースを会社が持っていて、緻密に定義した辞書を持っている。そう、会社に辞書がある。プログラムの中の変数やファイル名なんかもそのデータベースを見て決める。 辞書に用語がなければ、辞書に追加したり変更したりする。追加するときにはちゃんと申請しなければいけない。
G: ソースコードの中にはコメントを書いてはいけない。さきほどの辞書を使って、ソースコードが、例えば英語からドイツ語などの自然言語にそのまま翻訳されるので。ソースコード中にコメントがあるとその内容をいちいち翻訳しなければならなくなる。
G: 最初に生まれた子供と同じくらいの注意深さで名前をつけよう。
C: ポリモーフィズムがないからコードが読みやすいのかもしれない。ポリモーフィズムがあると、メソッドがどこから呼び出されるかわからないからプログラムの見通しが悪くなる。ボキャブラリーも大事だが構造も大事。ある研究では、ユースケースのいろんな箇所にコードを分散させるとエラー率が70%増えるという調査結果がある。
C: リーンアーキテクチャの良いところとして、エラーを早く見つけることができることがある。テストをしているときではなく、コードを読んでいる時にエラーに気づくことができる。
C: フォードは組み立てを実際にして、テストは最後に確認している。トヨタの場合は、組立時にその場でテストをしている。リーンのアプローチの1つとして、問題をより早く見つけるという利点がある。
C: アジャイルとリーンについて。リーンはクラスの構造が重要。リーンにコストをかけて、アジャイルで実際にどう稼いで行くかを考える。アジャイルはオブジェクトのインタラクションが重要。
C: ドメイン分析とユースケース分析も同時並行的にやる。同時並行的にやると重なる場所がある。みんなそれぞれ同時に実行していく。
C: 野中先生がトヨタにいた時に気づいた。それぞれの分析がかさなりあって進んでいく。これを、刺し身モデルという。(※ラグビーの)スクラムも刺し身。挟まり合っているでしょ。ウォーターフォールとは違って皆が全てのことをやるからスクラム。
C: リーンは長い期間のプランニングが必要で大事になってくる。アジャイルはフィードバックに応じた再調整が肝要。リーンは、深いレベルの専門家が作る。アジャイルはなんでもありみたいなところ��ある。スクラムとDCIはそのふたつのことをやる。
C: パターンも同じように両方のものを含む。これは日本の禅から来ていると思います。パターン系のガイダンスに従って、門を通ってゴールに近づくことはできるのではないか?パターンもスクラムも、ある程度のレベルではDCIも、ほとんどは日本由来のもの。道教だよね、これ。
C: リーン側のほうは、日本のトヨタの思想。山田 ひろしと言う有名なエンジニアでホンダの人がトヨタに教えた。(※原典見つけられず)
C: アジャイルのほうは、日本でよく知られた比喩で伝えるのは難しいですが… 社会的構造が違うので。デンマークは、社会がとてもフラット。日本は階層的なところがある。よりシステム化されているので、色々なルールが存在する。日本は、もうちょっとアジャイルのほうをエクササイズするといいかもしれません。
C: 実際の開発の流れ。ドメイン分析 → 共通性可変性分析 → クラス → インタフェース、API設計。最初はインタフェースしか書かない。もしかしたらスタブくらいは書くかもしれない。実装はいつ書くか?ユースケースのタイミングで。Just In Time。ユースケースが必要とするインタフェースだけを実装する。(※インターフェイスをクラスで実装するのではなく、インターフェイス自体を実装する)インタフェースは抽象的な概念で、ロールは具体的なインタラクションやアルゴリズムなので、そのタイミングで。
C: この中でプラットフォームを開発している人。プラットフォーム開発はリーンではない。あらかじめ用意しておくもの。だからリーンではない。使わないものを作る時は会社が危なくなる。これは、無駄と言います。
C: DCIアーキテクチャを利用した開発の順番としては、まずアーキテクチャを作ってそれからそれぞれデータベースとかGUIのインタフェースを作る。会社としてなにを売るかというのはいろいろあるけど、売るのはユースケース。それぞれのクラスを売っているわけではない。ユースケースは、それぞれのデータベースとかサーバーとかクライアントのユーザーインタフェースなどを少しずつ重ねたもの。
C: 生産性が一番の無駄を生むと、トヨタの大野耐一さんがおっしゃっている。
C: こういう話を、「object-composition」というメーリングリストで話をしているので、入ってくださいね。
C: 1月にまた東京に来るので、またこういう会を開催しましょう!
最後に、GertrudさんとCoplienさんと参加メンバーで集合写真を撮りました。
Tumblr media
NOTE: 当日は同時翻訳付きの英語トークでした。通訳の任を快諾してくださった、 @ganchiku さん、@remore さん、ありがとうございました。また、記事を書くにあたっては英語の下訳で @kuma_nana さんに貢献していただきました。こちらもありがとうございます。もちろん、当ポストの文責は執筆者である私にあります。
関連リンク
fulloo.info
The trygve language project
マルチパラダイムデザイン読書会
[Reenskaug 2006] Trygve Reenskaug. MVC and DCA Example program comments
object-compositionメーリングリスト
関連記事
PHP Mentors -> Beyond MVC
PHP Mentors -> Debasish Ghosh氏のブログ記事「ドメイン駆動設計:可変性の管理」を翻訳しました
PHP Mentors -> PHPカンファレス2015 PHPメンターズセミナー「モデルを設計せよ!―ドメイン駆動設計を超えて」参加レポート
0 notes
phpmentors · 9 years ago
Text
杉本 啓「2つのドメインモデル―DDDの含意」
Kei Sugimoto, "Two Domain Models - An Implication of DDD", PHP Mentors, (December 20, 2015)
 ドメイン駆動設計(Domain-Driven Design: DDD)は、オブジェクト指向、デザインパターン、リファクタリングなど、ソフトウェア設計分野における幅広い知的遺産と交差します。それゆえ、ドメイン駆動設計は、ドメイン指向のアプリケーションソフトウェア開発に馴染むような仕方でこれらの知識資産を統合したものと見ることができます。
こうした見方は妥当でかつ生産的でもありますが、DDDの業績に対しては別の見方もあり得ます。それがこの短い論考のテーマです。
この文脈において、DDDは、「ソフトウェア設計」から、私たちが特別な思い入れを込めて「情報設計」と呼ぶものへの静かな出発として考えることができるかもしれません。
ドメインモデルとは何か
とはいえ、まずは入口から始めましょう。私たちは1つの基本的な質問について再考しなければいけません:ドメインモデルとは何でしょうか?
ばかばかしい。ドメインモデルはドメインのモデルです。そうではありませんか?
具体的な例を用いて検討しましょう。バージョン管理システム(Version Control System: VCS)です。現在私たちは束にできるほどたくさんのVCSを持っていて、そのいずれもが他といくらか異なるモデルを提供しています。例えば、Subversionでは「コミット」は中央の(かつ単独の)リポジトリへのソースコードの変更の登録を意味するのに対して、Gitでは同じ言葉が分散された一群のリポジトリの特定のメンバーへのソースコードの変更の登録を意味します。「コミット」はそれぞれのVCSのユビキタス言語の語彙に含まれます。そして、そうであるなら、この語の意味のこうした違いは、その2つのVCSの基礎を成すドメインモデルの違いを反映しているのに他ならないでしょう。
この例から、これらの2つのモデル間の違いは、同一のドメインの問題を解決するためのアプローチの違いに由来することが分かります。ドメインモデルはドメイン自体のモデルではありません。そうではなく、実のところそれは、そのドメインのためのソリューションのモデルです。それぞれのVCSが誕生して初めて、それぞれのモデルが認識されるようになったのです。
このアイデアは全く単純明快です。別の例として、スプレッドシートソフトを見てみましょう。各種のスプレッドシートソフトは、シート、セル、数式、等々で構成される独特なドメインモデルを共有しています。このドメインモデルは VisiCalc とともに誕生し、その後進化してきたものです。私たちは会計の計算のためにスプレッドシートソフトを使うかもしれませんが、会計ドメインが、その歴史においてスプレッドシートの概念を持っていたわけではありません。そうではなく、スプレッドシートの概念やモデルは、幅広いドメインの問題を解決するため��設計され、そのドメインが会計の一部をたまたま含んでいた、ということなのです。
2つのドメインモデル
伝統的に、ドメインモデルは、「分析モデル」と見られる傾向にあります。すなわち、設計ではなく分析の対象です。しかしながら、ドメインモデルはソリューションのモデルであるという私たちの新しいビジョンは、この見方に異議を唱えます。「ソリューション」が何であれ、それは私たちが設計するものです。折に触れて私たちが既存のソリューションの設計を分析するにしても。
「設計」は、以前に存在しなかったものを創造するのに対して、「分析」は、それがすでに存在する物事を扱うという点で、いくぶん受動的な言葉です。伝統的なOOアプローチにおいては、分析モデルは、既に存在し、当然の帰結として設計の対象ではない現実世界の抽象として見られる一方、私たちが(エンジニアとして)構築するソフトウェアの機能と構造を決定することを「設計」と呼ぶ傾向があります。エンジニアリング視点からは、この分割はとても自然にみえます。
ここでのポイントは、ドメイン駆動設計者の視点からは、「現実世界」は2つの下位要素―物理的な要素と情報の要素―を含むであろう、ということです。
物理的な世界にあっては、人々が働き、モノが動き、お金が稼がれます。それらは私たちが「ビジネス」という言葉によって想像するものです。ビジネスを進めるために、注文、請求、在庫記録、財務諸表、等々といった情報を私たちは記録し交換します。これは現実世界のもう1つの構成要素―情報処理―です。
したがって、ドメインモデルは2つの異なったモデルに分割されます。物理世界のモデルと情報のモデルです。
このような世界観のもとでは、物理世界のモデルはたいてい単に与えられ我々にとって制御できないものであるのに対して、情報のモデルの大部分は我々が設計し得る、と私たちは考えるでしょう。
2つのドメインモデルとOOA
DDD本において、エヴァンスは初期時点での表面的なモデルと深いモデルについて繰り返し語ります。コンテナ輸送アプリケーションからの彼の事例では、表面的なモデルが船舶(Ships)やコンテナ(Containers)のような物理的な実体を含んでいるのに対して、深いモデルは本船名/航海番号(Vessel Voyages)や船荷証券(Bills of Lading)(例えば p.191 「深いモデル」)のような抽象概念を含んでいます。これらのモデルの間の区別は、少なくとも部分的には「深さ」の違いによるのではなく、「対象」の違い、すなわち、対象が物理的なものか情報か、によるのです。
エヴァンスは、伝統的なOOAがこの区別を無視する様子に関して、示唆的な話を語ります。
新しいオブジェクトモデラがプロジェクトに参加すると、決まって最初に提案するものがあった。それが、あるはずのクラス、船舶とコンテナである。彼らが聡明でなかったわけではなく、ただ、深いモデルを発見するためのプロセスを経験していなかっただけなのだ。 (p.192)
2つのドメインモデルという着想に照らして、私たちは、これは「ただ、深いモデルを発見するためのプロセスを経験していなかっただけなのだ」ということだけによるのではなく、OOAの知識体系(または少なくともその一般的に受け入れられた理解)��物理的な世界のモデルと情報のモデルの区別を欠いているからであろう、と理解することができます。
しかしながら、この区別が無視されているといって驚くには当たりません。なぜなら、この2つのモデルの区別は常に明らかであるとは限らないからです。というのも、情報は、私たちの世界のあらゆる領域に浸潤してしまっており、あたかも物理的存在であるかのような顔をしてまかり通っているからです。小切手は物理的でしょうか?あるいは情報でしょうか?私たちはどのように真に物理的なものと小切手を区別するのでしょうか?あるいは、そもそも区別しなければならないのでしょうか?この点について私は明快な答えを持っていません。多くの場合、あなたは(まるでそれらが物理的であるかのように)情報モデルから安全に小切手を除外することができますが、その一方で、(紙幣、硬貨、小切手をひっくるめて呼ぶ)「現金有高(げんきんありだか)」をモデルに含めなければならないかもしれません。しかし、これは金科玉条ではありません。私たちは自身の判断力を用いなければなりません。とはいえ、物理的なモデルと情報のモデルの間に線を引くことは、依然としてモデリングにおける洞察をもたらすことができるでしょう。なぜなら、最終的に私たちがソフトウェアによって表現しなければならないものは、物理的なビジネスのモデルではなく情報のモデルだからです。
DDDの含意
ユビキタス言語とモデル駆動設計はDDDの中核を成す2つのプラクティスです。2つのドメインモデルに照らすと、ユビキタス言語は、物理的な世界のモデルから情報のモデルを切り離すための、そして後者を分析ではなく設計の対象とするための仕掛けとして理解することができるでしょう。それは、私たち開発者の領域を広げます。そしてモデル駆動設計は、ソフトウェアの構造が情報のモデルを正確に反映している状態を保ち続けることを私たちに要請します。それは、私たちの職業がアナリストと開発者に分解することを防ぎます。
かくして、情報モデルの設計は私たちの手の内にあります。私たちの情報技術の時代以前においてさえ、情報モデルは設計されてきました。複式簿記は、それらのモデルの注目に値する実例です。列車運行図表(ダイヤグラム)や楽譜はその他の例です。これらのモデルは、「ビジネス側」の人々によって設計されてきました(その頃「IT側」は存在しなかったので)。対照的に、私たちのIT時代において、情報モデルを設計する責任は宙に浮いているかのようです。私たちのITは、ビジネス側の人々にとってとても付き合いにくいものです。それゆえ、彼らは、ITを使って実装される情報モデルの設計にしばしば苦痛を感じさせられてきました。アナリストが設計の責任を担うべきなのかもしれません。しかし、彼らが行うのは設計ではなく分析です。その上、実装がもたらすであろうフィードバックを受けることができない中で設計するのは困難です(これもまたモデル駆動設計が重視する点です)。私たち開発者は、この責任を引き受けるベストな位置にいるのです。もちろん、ビジネス側の人々との緊密な協力関係を前提として。
ならば今、私たちの職業の本質的な営為のひとつは、ソフトウェア設計に裏打ちされた情報設計であるわけです。 これがDDDの含意です。
杉本 啓 @sugimoto_kei 経営管理基盤ソフトウェア fusion_place の、プログラマ兼設計者 http://www.fusions.co.jp
11 notes · View notes
phpmentors · 9 years ago
Text
Two Domain Models - An Implication of DDD
Domain-Driven Design (DDD) intersects with a broad range of the intellectual heritage in the software design discipline including object-orientation, design patterns and refactoring. And as such, it can be viewed as a synthesis of these knowledge assets tailored towards domain-oriented application software development.
Although this view is valid and productive, there could be another way of viewing accomplishment of DDD. It is the theme of this short discussion.
In this context, DDD could be thought as a quiet departure from "software design" to something we may call "information design" with a special emotional attachment.
What is a Domain Model?
But first thing first. We must have a rethink on one basic question. What is a domain model?
It sounds ridiculous. A domain model is a model of a domain. Isn't it?
Let us discuss about this on a concrete example: version control systems (VCSs). We now have a bunch of VCSs, each of which offers a somewhat different model than another. For instance, with Subversion, "commit" means a registration of source code changes to the central (and solitary) repository, whereas, with Git, the same word means a registration of source code changes to a particular member of a set of distributed repositories. "Commit" is in the vocabulary of ubiquitous language of each VCS. And if so, this difference in the meaning should be the reflection of a difference of the underlying domain models of the two VCSs.
From this example, we can see that the difference between these two models comes from the difference in approach to solve problems in the same domain. A domain model is not a model of a domain itself. Instead, it actually is a model of a solution for the domain. It was not before each VCS was given birth that each model was recognized.
This idea is quite simple and straightforward. Let us take spreadsheet software as another example. They share a unique domain model consisting of sheets, cells, formula and so on. This domain model did not exist before the birth of VisiCalc and evolved after that. Although we may use spreadsheets for our accounting calculation, it is not that accounting domain had the spreadsheet concept in its history, but the concept and model were engineered to solve problems in a broad domain, which incidentally includes a part of accounting.
Two Domain Models
Traditionally, domain models tend to be seen as "analysis models", which are subjects of analysis, not design. However, our new vision that a domain model is a model of a solution challenges this view. A solution is something we design, even if we analyze designs of the existing solutions from time to time.
"Analysis" is rather a passive word in that it treats things which already exist, while a design creates something that did not exist before. In traditional OO approaches, an analysis model tend to be viewed as an abstraction of the real world, which already exists and naturally is not a subject of design, while a design governs the functionalities and the structures of software we (as engineers) build. From the engineering perspective, this separation looks very natural.
The point here is that from a domain-driven designer's perspective, a "real world" would consist of two subcomponents - physical and informational.
In the physical world, people work, things move, and money is earned. They are what we imagine by the word "business". To let businesses move on, we record and exchange information - orders, invoices, inventory records, financial statements, and so on. This is another component of the real world - information processing.
Accordingly, a domain model is divided into two distinct models: physical world models and informational models.
With such a world view, we would consider the physical world models as usually just given and uncontrollable for us, while we can design most parts of the informational models.
Two Domain Models and OOA
In his DDD book, Evans repeatedly speaks about initial superficial models and deep models. In his example from a container shipping application, the superficial model contains physical entities like Ships and Containers, while the deep model contains such abstract concepts as Vessel Voyages and Bills of Lading (e.g. p.189 "Deep Models"). The distinction between these models is at least partially not of the "depth" but of the "subject" of each model - physical and informational.
Evans tells a story which is suggestive on how traditional OOA ignores this distinction:
Whenever new object modelers showed up on the project, what was their first suggestion? The missing classes: ship and container. They were smart people. They just hadn't gone through the processes of discovery. (p.190)
With the idea of two domain models, we understand this would not have been only because "they just hadn't gone through the processes of discovery", but also because the body of knowledge of OOA (or at least its generally accepted understanding) has lacked the distinction of physical world models and informational models.
However, this ignorance is not surprising, because the distinction between the two models is not always apparent. It is because information has infiltrated into every corner of our world and passes as if they were physical. Are bank checks physical or informational? How do we distinguish them from truly physical objects? Or, do we have to do so at all? I have no definitive answer here. In many cases you can safely omit checks from your informational models (as if they were physical), while you might have to include "cash in hand" in your models (which refers to bank bills, coins, and checks, collectively) instead. But it is not a dogma. We have to use our judgment. Nevertheless, drawing a line between physical and informational models could still bring insights in modeling, because in the end what we have to represent with our software are informational models, not physical business models.
An Implication of DDD
Ubiquitous language and model-driven design are two practices at the heart of DDD. In the light of two domain models, ubiquitous language could be understood as a device to separate an informational model from a physical world model and make it subject of design, not analysis. It extends the realm of us, developers. And model-driven design urges us to keep software structure to be exact reflection of the informational model. It prevents our profession from dissolving into analysts and developers.
Thus, design of informational models is in our hands. Informational models had been designed even before our information technology age. Double-entry book-keeping system is a remarkable instance of those models. Train operation diagrams and music scores are other examples. These models had been designed by people on the "business-side" (because there was no IT-side). By contrast, the responsibility to design informational models seems to be floating in our IT age. Our IT has been too awkward for business people. Therefore they often have felt uncomfortable designing informational models which are to be implemented with IT. Analysts might be the people in charge. But they analyze, do not design. Furthermore, it is difficult to design if you are disconnected from feedbacks that implementation would provide (this is also what model-driven design cares). We, developers are in the best position to accept this responsibility, off course with a tight cooperation with business-side people.
Now an essential act of our profession is the information design underpinned by the software design. This is what DDD implies.
Kei Sugimoto @sugimoto_kei Programmer and Designer of fusion_place, a management infrastructure software http://www.fusions.co.jp
1 note · View note
phpmentors · 10 years ago
Text
Debasish Ghosh氏のブログ記事「ドメイン駆動設計:可変性の管理」を翻訳しました
ドメイン駆動設計が、今、世界的に盛り上がりを見せています。2016年1月には、Domain-Driven Design Europeが開催されるそうです。このイベントのスピーカー&ワークショップ講師の1人として、Jim Coplien氏の名前が載っています。Jim Coplien氏は、日本だと、「組織パターン」の著者として、また、DCIアーキテクチャの人として有名ですが、ドメインエンジニアリングの研究者でもあります。
Eric Evans氏のDDD本は、サブタイトルが、「ソフトウェアの核心にある複雑さに立ち向かう」となっています。複雑さとは何か、それを管理する技術とは何か、古くて新しい問題です。Jim Coplien氏の著作���マルチパラダイムデザイン」では、ドメインとは何か、どのように設計を行うべきかについて書かれています(残念ながら絶版です。)
Debasish Ghosh(デバシッシュ・ゴーシュ)氏のDDDとマルチパラダイムデザインを関連付けたブログ記事「Domain Driven Design : Managing Variability」を翻訳しました。以下に掲載します。(※デバシッシュ氏は、ScalaMatsuri2016に応募されています!)。
Note Debasish Ghosh氏は書籍「実践プログラミングDSL」(原著: DSLs in Action)の著者として知られています。彼は現在Functional and Reactive Domain Modelingを執筆中です。こちらは2016年春に出版予定だそうですが、MEAPを利用して一足先に読むこともできます。
ドメイン駆動設計:可変性の管理
原文URL:http://debasishg.blogspot.jp/2006/09/domain-driven-design-managing.html
2006年9月18日(月)
Springのユーザーたちが、ドメイン駆動設計について語り始めました。彼らはきちんと取り組んでおり、良い議論をしています。2006年に行われたSpringOneでは、ドメイン駆動設計に関する3つのセッションがありました。 AOPの人として知られているRamnivas Laddad氏(最近「Interface 21」メンバーに加わりました)は、DIとAOPが、DDDの最も良いところを引き出すのに有効であること―最小限のサービス層、豊かなドメイン層、流れるようなインターフェイスについて発表しました。 「Interface 21」とGrailsで知られるSteven Devijver氏は、サービス層にある複雑性を管理するためにデザインパターンを利用することについて語り、リッチなドメインモデリングに関する議論を行いました。 レイヤー化、AOP、IoC等による関心の分離のような重要なアスペクトについて彼は強調しました。 不変性は、モデルの正当性を維持するために重要であること。 つまり、クラスはイミュータブルに作り、できるだけ外部へ漏れ出さないように制御に努めること。 実装の内部をさらけ出してはならないこと―パブリックなセッターはNGであること。 要するに、ドメインモデルは、ドメインエキスパートが話せるドメインの言語にだけ公開されているべきだということです。 インターフェイスとパブリックな契約が、ユビキタス言語を語るように作ること。 ドメインモデリング技法に関する別のセッションでは、「Interface 21」のKeith Donald氏が、ドメインの蒸留、ドメインモデルの中に獲得した知識を抽象化するためのテクニックについて話しました。 全体を通して、SpringユーザたちはDDDファンのイベントを楽しんでいるようでした!
可変性!可変性!
ドメインモデルを、柔軟に、生成的(ジェネレ―ティブ)に作るということは、モデルにおいて可変性を管理することに尽きます。 そのモデルの可変部分をうまく管理することによって、より構成しやすく、カスタマイズしやすく、柔軟にすることができるようになります。 以前の記事では、ジェネレーターとしてはたらいて基盤を提供する構成の知識について書きました。 構成の知識を最大限に活用するのがこの可変性です。 設計のアスペクト、固有の実装、ライフサイクル、スコープの決まったインスタンス、アルゴリズム、ストラテジー等は、Java / C++のコードベースの中で埋没させたくはないものなのです。
ストラテジーパターンで示すと…
ここ数年、ストラテジーパターン実装の多くのバリエーションが、モデルの可変性を管理するために使われてきたと思います。 GOFは C++ で2つのバリエーションを検討しています。 1つは、実行時ポリモーフィズムと仮想関数を使うもの、もう1つは、テンプレートとコンパイル時ポリモーフィズムを使うものです。 それぞれにメリットがあり、特定のコンテキストにある特定の問題を解決するのに用いることができます。
下記スニペットに、ストラテジーデザインパターンを実行時ポリモーフィズムで適用して、繰延利息計算(accrual calculation)ドメインプロセスを外部化するやり方を示します。
class AccrualCalculation { private CalculationStrategy strategy; // セッターインジェクション public void setStrategy(CalculationStrategy strategy) { this.strategy = strategy; } // ストラテジーへの委譲 public BigDecimal calculate(...) { return strategy.calculate(...); } } interface CalculationStrategy { BigDecimal calculate(...); } class DefaultCalculationStrategy implements CalculationStrategy { public BigDecimal calculate(...) { // 実装 } }
// 構成の知識を通して可変性を外部化する <bean id="calcStrategy" class="com.x.y.DefaultCalculationStrategy"/> <bean id="accrual" class="com.x.y.AccrualCalculation"> <property name="strategy"> <ref bean="calcStrategy"/> </property> </bean>
ジェネリクスを使って実装した同じパターンと比べてみましょう。 繰延利息計算クラスがストラテジーでパラメーター化されるように設計しています。
class AccrualCalculation<S extends CalculationStrategy> { private S strategy; // ストラテジーへの委譲 public BigDecimal calculate(...) { return strategy.calculate(...); } } // 使い方 interest = new AccrualCalculation<DefaultCalculationStrategy>().calculate(...);
トレイトテクニックのelse-if-then型(この名前、いいですね!)は、C++コミュニティで使われており、拡張性を持たせつつ型レベルで可変性を管理することができます。 トレイトクラスとトレイトテンプレートは、メタプログラミング能力に秀でていただけではなくて、様々なC++アプリケーションの開発に使われてきました。 コンパイル時ストラテジーの別なバリエーションとして、Alexandrescu氏のポリシーベース設計があります。 ポリシークラスは、実装をいくつでも持つことができ、コンパイル時ストラテジーをドメインモデルにプラグインするのに最適な選択肢を提供してくれます。
ストラテジーデザインパターンは、最適な機構を提供することで、ドメインモデルにおける粗粒度の可変性をカプセル化します。 ストラテジーの実装をIoCコンテナやその他のジェネレーターへと外部化することにより、モデルがプラガブルであるようにします(構成の知識として)。
細粒度の可変性とテンプレートメソッド
テンプレートメソッドデザインパターンは、アルゴリズム、ストラテジーといった細粒度の可変性を、共通性フレームワークの範囲内でプラグインすることを目的としています。 ドメインプロセスの大きなレベルでのフローが不変である場合、モデルでこのパターンを使います。 こまごました物をカスタマイズできるように、フックをかけて外部化するわけです。 私はルールベースの大規模な金融アプリケーションをモデリングした時に、このパターンをうまく効果的に活用できました。 下記は、テンプレートメソッドデザインパターンを使って、細粒度の可変性を実装するサンプルです。 ここで、繰延利息計算アルゴリズムはfinalであり、フックの箇所は派生クラスによって追加されます。 フックは基底クラスの抽象メソッドとして管理されます。
abstract class AccrualCalculation { public final BigDecimal calculate(...) { // フック int days = calculateAccruedDays(...); if (days != 0) { // 不変のロジック } else { // フック boolean stat = isDebitAccrualAllowed(...); if (stat) { // 不変のロジック } // 不変のロジック } } protected abstract int calculateAccruedDays(...); protected abstract boolean isDebitAccrualAllowed(...); }
負の可変性
Jim Coplien氏は、設計、分析領域において、この用語を著作マルチパラダイムデザインの中で初めて紹介しました。 共通の継承階層にまとめられるメンバー群にあるうちの一部のメンバーが、他の点においては共通であるのに、一部の点でだけは基底クラスの仮定を破壊してしまうような時、これを負の可変性と呼びます。 Cope(訳注:James O. Coplien のこと)は、負の可変性は、基礎である共通性を侵害してバリエーションのルールを壊す、ルールの例外になる、と述べています。 負の可変性を実装するためのテクニックは少なくありません。 たとえば、C++ におけるテンプレートの特殊化や、条件コンパイル等です。 ブリッジデザインパターンもこの問題をうまく扱えることが知られています。 Coplien の「マルチパラダイムデザイン」に、負の可変性について詳しく書かれています。 この師範の深い洞察をぜひ読んでみて下さい。 実務の場で、ドメインモデルが負の可変性の振る舞いを示すことはよくあります。 そのようなとき、適したパターンやイディオムを使って、正しく取り扱う必要があります。
参考文献
マルチパラダイムデザイン(金澤典子, オブジェクトの広場2002年2月号)
Multi-Paradigm Design(James O. Coplien, July 8, 2000)
関連記事
Pieceの中のSymfony #3: Dependency Injectionコン��ーネント―構成の知識とDIコンテナ
「ドメインモデリングにおける関数型パターン―仕様パターン」を翻訳しました
3 notes · View notes
phpmentors · 11 years ago
Text
Practical DDD #4: ユビキタス言語とモデル
ドメイン駆動設計のユビキタス言語とモデルの関係性についての、個人的なメモです。
思考を図にする
書籍「エリック・エヴァンスのドメイン駆動設計」の第2章で、ユビキタス言語の語彙について説明した段落があります。
ユビキタス言語の語彙には、クラスや主要な操作の名前が含まれている。また、モデルの中で明示されたルールについて議論するための用語も含まれている。この言語は、モデルが従うべき高次の構成原理(コンテキストマップや大規模な構造など、第14章と第16章で説明するもの)に由来する用語によって補完される。そして最後は、ドメインモデルに対して��般に適用されるパターンの名前によって、この言語は強化される。 エリック・エヴァンスのドメイン駆動設計 第2章 コミュニケーションと言語の使い方 ユビキタス言語 (p. 25)
この文章が言わんとしていることを私の理解で図にしてみました。
この図は、上に挙げた段落の意味を、パターンランゲージのナビゲーションマップの図法(DDD本の見開きにあるもの)をイメージして描いたものです。あくまで上の段落からの意味だけなので、ドメイン駆動設計のある1つの側面を表しているにすぎません。ただ、こうやって図にしてみることで、私には、書き手の思考がより伝わってくるように感じました。
これは概念モデリング?
先ほど私がやったこと、それは、ドメイン駆動設計という本に書かれた内容を解釈し、それが意味していると考えられる内容を、言葉と言葉というよりもむしろ、概念と概念の関係性としてナビゲーションマップ形式の図に表しました。要するにこれは、概念モデリングそのものではないでしょうか。
(Googleで検索してみると、同じようなことをされている方はすでにいらっしゃいます ex 「読書ツールとしての概念モデリング - 反言子」)
哲学者フォーダーによれば、
文は思考を、語は概念を表わす。(Fooder 1998) 信原 幸弘、太田 紘史 編『シリーズ 新・心の哲学I 認知篇』勁草書房 2014年 第1章 概念の構造とカテゴリー化 5-1 情報原子論と文法的現象 p. 61
というように、文章で書かれたものと、その文章から概念モデリングした図とは、その根底にある思考は共通しています。
で。。。。?
ドメイン駆動設計を軸にしたソフトウェアの分析・設計では、このように見出した概念の意味にこだわります。エヴァンスの本を読んでいると、まるで、言語体系をつくり上げること自体が設計の中心であるかのように錯覚することが私にはあります(私はまだ、そういった境地ではありません)。
ただ、ソフトウェアを設計するのに、そのような意味論にこだわりすぎるのが良い結果を生むのかどうか。今の私は結論を持っていません。
ドメイン駆動設計を読んでいる皆さんが、今回挙げた私の図を見て感じた違和感でも何でも、コメントいただけると幸いです。
参考
モデリングのスタイル:意味論と統辞論: 設計者の発言
1 note · View note
phpmentors · 11 years ago
Text
Practical DDD #3: モデルの深さ
ドメイン駆動設計の「モデルの深さ」などについての考察、前回の続きです。
Practical DDD #2: 責務のレイヤーとPolicy-Control-Operation
前回の記事では、エリック・エヴァンスのドメイン駆動設計で書かれているモデルの深さというのは分かりづらいので、別の尺度としてモデルに表れる概念群の凝集度を考えてみてはどうか、ということに触れました。また、ユビキタス言語にあらわれる概念と概念が互いに関連しあい、述語でつながっていれば、パターンランゲージのようにドメインの知識を豊かに伝えることができるでしょう。
今回はドメイン駆動設計第1章の最後のストーリーで到達した「船荷証券(ふなにしょうけん)」というものから、モデルの深さについて考えてみます。
そもそも海運ドメインについて
私には海運ドメインについての知識がありません。エヴァンス氏自身がそうだったのかはさておき、ドメイン駆動設計のストーリーとしても、プロジェクトの初期において開発者には海運ドメインについての深い知識はないという想定になっています。しかし、ドメインモデリングを行うには対象ドメインの知識が必要です。エリック・エヴァンスのドメイン駆動設計では、ドメイン知識を得る方法としてドメインエキスパートとのコミュニケーションが中心的に取り上げられていますが、対象ドメインについての知識を得る方法は他にもたくさんあります。『ドメイン分析・モデリング―これからのソフトウェア開発・再利用基幹技術』には、ドメイン分析に使う情報として、次の項目が例示されています。
目的業務
業務マニュアル
そのドメインの教科書、パンフレット、用語集
ドメインエキスパートへのインタビュー
ビジネスの現状
ドメインの境界、スコープ、近隣ドメイン
使用可能なテクノロジ
D-AMEの適用範囲
開発事例
既存のシステム(パッケージ)
既存のシステムの開発担当者へのインタビュー
既存の仕様書、マニュアル
実現プラットフォーム
ひな形アーキテクチャ
法規
コスト制約
(D-AMEというのは、「ドメイン分析・モデリング」で紹介されているドメイン中心の分析・設計・開発スタイルの名称です。Domain Analysis, Modeling and Engineeringの頭字語)
ドメインエキスパートへ素朴にヒアリングするわけではなく、事前に様々な知識を揃えておけということで、これは当たり前のことですね。このリストを見ると、自社サービスやいわゆる業務システム以外のシステムを開発する場合も例外ではなく、ドメイン知識を得るための材料はたくさんあるということが分かります。最近ではWebで検索することでかなり手軽に仕入れられる情報も増えていますね。
海運 - Wikipedia
船荷証券 - Wikipedia
JETRO(日本貿易振興機構)のページのFAQで「船荷証券」について調べると49件もありました。
検索結果 - 貿易・投資相談Q&A - 国・地域別情報(J-FILE) - 海外ビジネス情報 - ジェトロ
また、実際に海運関係のお仕事をされていた方のホームページも見つかり、次の図のように端的に海運業を表すモデルも示されていました。
港のお仕事 - われら海族
B/L(Bill of lading) 船荷証券
L/C(Letter of Credit) 信用状
L/G(Letter of Guaranty) 保証状
S/O(Shipping Order) 船積指図書
M/R(Mate Receipt) 本船受領書
D/O(Delivery Order) 荷渡指図書
われら海族のページには、上の図が基本の流れと説明されています。
これらの情報から推察すると、少なくとも船荷証券という知識は、ドメインエキスパートへのインタビュー初日から出てきそうなくらい、海運ドメインでは基本中の基本になっているように思えます。
船荷証券は「深い概念」なのか?
ドメイン駆動設計の第1章の最後に、ドメイン駆動設計のハイライトである深いモデルの話があります。
結局、何ヶ月も知識をかみ砕いた後で我々が気づいたのは、貨物の取り扱い、すなわち物理的な荷積みと荷下ろしや、ある場所から別の場所への移動などは、ほとんど下請け会社や社内の業務担当者によって行われているということだった。輸送業エキスパートから見れば、関係者の間で連続して責任が移動するということだったのだ。(中略)重要視されるようになったのは、輸送日程の手配よりも、船荷証券などの法律書類や支払い免除につながるプロセスだったのだ。 エリック・エヴァンスのドメイン駆動設計 第1章 知識をかみ砕く 深いモデル (p. 21)
この文章からは「船荷証券などの法律書類や支払い免除につながるプロセス」が深いモデルである、ということのように読めてしまう気がしますが、果たしてそういうことなのでしょうか?
船荷証券自体は海運ドメインでは当たり前の知識で、実際に業務担当者は「現物」として印刷された船荷証券をやりとりしているかもしれません。モデルを蒸留し、考えに考えて絞り出された抽象、という類のものではなく、素朴に現実として存在し使われているものです。つまり、船荷証券自体は深い概念ではないというのが現在の私の解釈です。 (この考えに至る過程で、IT勉強宴会の方々からのアドバイスが大変参考になりました。ありがとうございます。)
では、ドメイン駆動設計で語られているこのストーリーは間違っているのかというと、そういうことでもありません。このストーリーの「続き」が書籍のあちこちに出てきますが、まさに「深いモデル」そのものについて説明されている第15章 蒸留のうち「隔離されたコア」にある例15.4 (p. 436〜) などからは、深さを見て取れます。(p. 440 にある 図 15.4 隔離されたコアの完成後に非コアサブドメインの有意義なモジュール化が続く、から抜粋しています)
ここには船荷証券などとともに「顧客合意」という抽象要素が追加されています。この要素によって、配送ドメインから顧客にまつわる関心事が分離され、配送ドメインがより凝集されていることが分かります。このパッケージの中を眺めると、顧客合意のみ抽象度が高く、抽象度という軸では統一されていませんが、この点は問題ではないということですね。蒸留された深いモデル、そしてそれを表すモジュール/パッケージというのは、抽象度の高い要素のみで構成されているわけではなく、パッケージの目的にふさわしい抽象度で、過不足無く要素がつまったものなんでしょうね。この意味では、パッケージ分割のために適切な抽象要素を見つけることと同じか、あるいはそれ以上に、適切なパッケージ名を見つけることもとても重要なのでしょう(15章の例では、輸送から、配送/顧客/物流に分割されています。)。
凝集度の高さがモデルの深さと1対1に対応するわけではありませんが、凝集度を高めることは、深いモデルであるための必要条件とはいえそうです。
船荷証券にもっと早く気付くには
ドメイン駆動設計の蒸留で説明されているような深さのあるモデルは、確かに一朝一夕には辿り着けないかもしれません。「顧客合意」のようにパッケージを分割できるような役に立つ抽象は、ドメインエキスパートも明確には概念化していないでしょう。開発者がモデルの凝集度を高めることを考えていく過程で、見つかるものというのも納得できます。
一方、船荷証券のような業務で基本的に扱っているモノについては、気づくまでに何ヶ月もかけていては話になりません。船荷証券に着目していなかったモデリングは、対象ドメインという大枠ではズレていなかったとしても、やはり何かズレた対象/異なる層をモデリングしてしまっていたと言わざるを得ません。ここを見誤らないようにするためには、管理過程から業務過程を見る、または帳簿組織に着目する��いう視点が役に立ちそうです。これらについては、また次回。
追記
@sugimoto_kei氏より次のご指摘を頂きました。
船荷証券(B/L)を含む貿易取引の仕組み自体は「深い」ですよ。歴史の中で洗練されてきたモデルだし。でも、取引で用いるそうした記録のモデルと、貨物や船舶といった「現実のモノ」を対象にしたモデルの違いは、「深さ」とは別の軸上にあるのですね。
本文で「船荷証券は深い概念ではない」と書きましたが、これは「対象をモデリングした結果として表れるもの」という括りで見れば、ということです。杉本さんの仰るとおり、この海運ドメインは歴史があり、それ自体が深く洗練されているのですね。しかしそのことと、開発するシステムにおいて「深く蒸留されたモデル」を作り出すこととは、別の軸の話であるということです。
5 notes · View notes
phpmentors · 11 years ago
Text
Practical DDD #2: 責務のレイヤーとPolicy-Control-Operation
2014年4月6日に大阪で開催された第4回ドメイン駆動設計読書会@大阪に参加しました。読書会の内容のまとめなどはWikiの方をご参照ください。今回は第1章の知識のかみ砕き、深いモデルと第2章ユビキタス言語の前半を読み、ディスカッションしました。ディスカッションで得られた気付きとこれまでに私が考えていたこと、ドメイン駆動設計の後半に出てくる「責務のレイヤー」などとのつながりについて、考察してみます。
モデルの「深さ」とは何か
モデルに対して深いのか浅いのかについて、ドメイン駆動設計で何か客観的な指標が示されているわけではありません。あくまでエヴァンス氏の主観でしかないと言えますし、開発者が取り組んでいるドメインやコンテキストに依存するものでもあります。ドメイン駆動設計においては、モデルの深さを探求していくことが大きな目標の1つとされており、「深いモデル」という言葉が開発者同士の共通語になってはいます。しかし、もう少しブレの無い言葉で表現しておいた方が良いように思います。ここで、ソフトウェア工学で使われてきた「凝集度」という言葉を使えば、「概念群の凝集度」という視点で置き換えられるのではないかと私は考えます。この捉え方はすでに増田亨さんのブログでも触れられています。ドメイン駆動設計の後半では、蒸留カテゴリの中に凝集は繰り返し現れます。
凝集度を高める:クラスもパッケージもコンテキストも「ドメインの視点」で凝集すべし | システム設計日記
ユビキタス言語で凝集度を判定する
内容の順序と前後しますが、ユビキタス言語の章では2つのシナリオが例示さています。分かりやすい箇所を抜粋します。
シナリオ 1:
開発者:わかりました。該当する貨物IDを持ったすべての行を輸送テーブルから削除してから、荷受け地と荷出し地、新しい通関地点を経路選択サービスに渡せば、テーブルにデータが再登録されます。貨物にブール値を持たせて、貨物テーブルにデータがあることがわかるようにしましょう。
シナリオ 2:
開発者:わかりました。経路仕様にある属性をどれか1つでも変更したら、古い輸送日程を削除した上で、経路選択サービスに対して、新しい経路仕様に基づいた新しい輸送日程を生成するよう依頼します。
シナリオ 1 では、ユビキタス言語由来の単語は2つですが、シナリオ 2 では3つになっています(この箇所では貨物は出てこなくなっています)。そして、伝えたい意図を表現するための語彙のセットに変化しています。シナリオ 1 では、概念凝集度の低いモデルの表象としてのユビキタス言語をベースに開発者が意図を話そうとしているわけですが、この発言をしながら開発者自身「語彙が足りない」ことに気づくのではないでしょうか。言うならば、初めての海外旅行先で英語を話そうとしても、言いたいことが言えないという感覚と同じです。みなさんもモデリングセッションの中で少なからず体験されていることと思います。
ユビキタス言語が凝集されていない状態:
Tumblr media
凝集度の高いユビキタス言語:
Tumblr media
ところで、次に出てくる疑問は、どのようにしてユビキタス言語を構成しうる知識をかき集め、ドメインモデルを成長させていくのか、という点でしょう。これをエヴァンスは「知識のかみ砕き」や「蒸留」、「隠された概念を引き出す」のように表現しています。
オーバーブッキングポリシー
「隠された概念を引き出す」例として、オーバーブッキングポリシー���話が示されています。貨物輸送ドメインにおいては「オーバーブッキングの仕方」は重要な概念なので、概念として明示的にドメインモデルに登場させます。それと同時に実装コード側も、メソッド本体の if 文に埋もれた状態から1つの独立したクラスに切り出されています。
この例では、業務知識からの要請によってオーバーブッキングポリシーという概念を得ています。この程度のものであれば、業務知識がなかったとしても、コードのリファクタリングから発見できた可能性もあるでしょう。ドメイン駆動設計では実装からのフィードバックも重視されており、実装とコードのリファクタリングから何か得られるかもしれない、という心構えは常に必要です。
一方で、あらゆる対象ドメインをゼロから実直にモデリングして実装し、リファクタリングを繰り返さなければ深いモデルにたどりつけないのかという疑問が出てきます。手早く確実に「オーバーブッキングポリシー」という概念を得る方法はないのでしょうか。業務知識を豊富に持っていれば、もちろん役立つでしょう。しかし、あらゆるドメインの個別知識を持つことなどできません。ある程度汎用性のある知識、つまり知識の体系、それも一段階抽象度の高いメタレベルの知識体系が、このような問題解決に役立ちます。このようなメタレベルの知識体系としてドメイン駆動設計で取り上げられているものの1つが、第16章「大規模な構造」の中の「責務のレイヤー」です。
責務のレイヤー
責務のレイヤーがどのようなものなのかの説明は書籍を読んでいただくとして、オーバーブッキングポリシーの例を責務のレイヤーに割り当ててみると、私の理解では次のようになります(例では航海が積載量を保持していますが、能力レイヤーに該当する要素としてうっすらと船・顧客の存在を示してみました。こちらに積載量を持たせてみています)。
Tumblr media
ただし、この責務のレイヤーというのは文字通り「責務」のレイヤーであり、下から上方向への依存がないような知識の体系ではありますが、それでもドメインのある1つの側面を取り出してレイヤー構造に表したにすぎません。実際に貨物の運送予約という業務のユースケースを想定した場合、意思決定支援/ポリシーレイヤーにある知識が当然業務で使われなければなりませんから、この段階(コンテキスト)ではレイヤーが異なるという意識は不要になります。
Tumblr media
ここで示したように、責務のレイヤーという知識体系によって、オーバーブッキングポリシーがどのような種類の情報なのかが明確になりました。とても自然に、それぞれの情報が各レイヤーに当てはまっているように見えます。逆に、責務のレイヤー(能力/業務/意思決定支援・ポリシー)という情報��分類を開発者が最初から知っていれば、オーバーブッキングポリシーという概念に相当早くたどり着けるように思われます。
「それなら、なぜドメイン駆動設計の最初に、責務のレイヤーを説明しないんだ!」という声が聞こえてきます。私もそう思いました。
責務のレイヤーとドメイン駆動設計の関係
責務のレイヤーを最初から知っていれば、深いモデルに早くたどり着ける。これはつまり、責務のレイヤーという概念は、ドメインの知識体系そのものの一部分であるということです。エヴァンスが「ドメイン駆動設計」として本にまとめて伝えようとしているエッセンスは、ドメインの知識体系にどうアプローチしていくのかということです。ですから 、責務のレイヤーはより早く深いモデルに到達するための既存知識体系の1つ、として紹介されているだけなんでしょうね。同じように、アナリシスパターンなどが知識体系のショートカットとして紹介されています。このようなメタレベルの知識体系で役立つものは他にいくつもあります。
ところでこの責務のレイヤーという知識体系について、元をたどるとPolicy-Control-Operationという体系があることを、2月の関西IT勉強宴会に登壇された中西先生から教わりました。
Policy-Control-Operation
経営、管理、業務という3つのタイプで、これは企業や何らかの組織がある目的を持って活動する時、つまり経営活動が行なわれる時に、さまざまなレベルで表れる構造です。
Tumblr media
この構造のうち、特に情報の種類について言及されている例が、Milt Bryce and Tim Bryce著「IRMー情報資源管理のエンジニアリング」第2章(p. 37)の表「情報が果たす役割」として挙げられています。
Tumblr media
ドメイン駆動設計読書会の最初の回でも紹介されていたTM(T字形ER手法)の佐藤氏のページでも、事業過程・管理過程・組織過程という業務の階層の考え方が解説されており、モデリングの際の視点の決め方として重要とされています(これは遡ると20世紀初頭の管理原則の父アンリ・ファヨールの管理過程論などからの系譜です)。
長くなりましたので、続きの「船荷証券」の話は、また次回 (Practical DDD #3: モデルの深さ)。
6 notes · View notes
phpmentors · 12 years ago
Text
Practical DDD #1: Specificationパターンの例
あるエンティティに対して、何らかの条件を満たすものをグループとして扱いたいことがよくあります。安直な実装としては、条件を加味してエンティティを抽出するようなメソッドをリポジトリに追加する方法をとってしまうかもしれません。
このようにリポジトリにメソッドを持たせてしまうと、条件が集合操作の中に埋もれてしまい、再利用しづらくなります。そこでDDDではSpecification(仕様)としてこういった条件をくくり出すパターンが紹介されています。『エリック・エヴァンスのドメイン駆動設計』p.229「仕様の適用と実装」では、次のように書かれています。
仕様の価値の多くは、全く異なるように見えるアプリケーションの機能を統一することにある。以下に挙げる3つの目的のうち、1つでも当てはまれば、オブジェクトの状態を(筆者注:仕様として)定義する必要があるだろう。
オブジェクトを検証して、何らかの要求を満たしているか、何らかの目的のための用意ができているかを調べる。
コレクションからオブジェクトを選択する(期限が超過した請求書を問い合わせる場合など)。
何かの要求に適合する新しいオブジェクトの生成を定義する。
検証、選択、要求に応じた構築という、これら3つの用途は、概念レベルでは同じものである。仕様のようなパターンがなければ、同一のルールがさまざまな外見で、場合によっては矛盾した形式で出てきてしまうかもしれない。それにより、概念の統一性が失われかねない。仕様パターンを適用することで、実装が異なってしまう場合でも、一貫性のあるモデルを使用できるようになる。
『エリック・エヴァンスのドメイン駆動設計』第9章 暗黙的な概念を明示的にする p.229
具体例で見てみましょう。キャンペーンの開催情報を管理するシステムがあり、キャンペーンエンティティには開催開始日付が設定されています。この日付とシステムの現在日時を比較することで、キャンペーンが開催中かどうかを判定します。この条件を満たすキャンペーンエンティティを「開催中キャンペーン」というグループで扱いたいとします。「開催中かどうか」の条件は、キャンペーンエンティティが満たすべき仕様となります。この例では単なる日時の比較なので単純ですが、もっと複雑な条件の場合なども想像してください。また、開発・運用中にこのような仕様が変更される可能性もあるでしょう。そういった場合に、仕様そのものが1箇所で表現されているととてもメンテナンスしやすくなります。
DDD本では、仕様オブジェクトには次のような責務を持たせています。
単一のエンティティに対して、仕様を満たすかどうかを判定する
リポジトリから仕様を満たすエンティティの集合を取得する
1.については、対象のエンティティを引数にとるisSatisfiedBy()メソッドを実装します。
2.については、対象エンティティのリポジトリオブジェクトを引数にとるsatisfyingElementsFrom()メソッドとして実装します。このメソッドでは、リポジトリから一旦汎用的な条件でエンティティの集合を取得し、集合の各エンティティに対してisSatisfiedBy()を使って仕様を満たすかどうかをチェックするような実装にします。
このように実装したサンプルをGitHubにあげてあります。
phpmentors-jp/phpmentors-example-campaign
OpenCampaignSpecificationクラスは次のようになっています。
phpmentors-example-campaign / src / Example / CampaignBundle / Domain / Specification / OpenCampaignSpecification.php
class OpenCampaignSpecification { /** * @var Clock */ protected $clock; /** * @param Clock $clock */ public function __construct(Clock $clock) { $this->clock = $clock; } /** * @param Campaign $campaign * @return bool */ public function isSatisfiedBy(Campaign $campaign) { if ( ($campaign->getStartDate() <= clock->getCurrentDateTime()) && ($campaign->getEndDate() > $this->clock->getCurrentDateTime()) ) { return true; } return false; } /** * @param CampaignRepository $campaignRepository * @return ArrayCollection */ public function satisfyingElementsFrom(CampaignRepository $campaignRepository) { $campaignList = $campaignRepository->findAll(); $campaignList = $campaignList->filter(function($campaign) { return $this->isSatisfiedBy($campaign); }); return $campaignList; } }
Tumblr media
仕様をくくり出すことで、リポジトリがさまざまな条件のファインダメソッドで膨れ上がることを避けられます。分離した後に、必要であれば最適なクエリーを仕様とリポジトリとで協調して生成する例などもDDD本には紹介されています。他に、ファクトリーオブジェクトと協調して、最初から仕様を満たすオブジェクトを生成するといった使い方もあります。
参考
『エリック・エヴァンスのドメイン駆動設計』第9章 暗黙的な概念を明示的にする - 「仕様(Specification)」(p. 226〜)
仕様オブジェクトとリポジトリとで協調動作する際の実装として、ダブルディスパッチが使われています。一般的には仕様はバリエーションを持ちますが、それに対するリポジトリは単一なので、2次元の可変性確保という目的とは合致しません。関心・責務の分離の観点でダブルディスパッチについて解説した記事も参照してください。
4 notes · View notes
phpmentors · 12 years ago
Text
DDDのリポジトリの外部依存とオブジェクト指向の原則の適用について
DDDのリポジトリがORMコンポーネントへ依存することの是非について、オブジェクト指向の原則の面から解説します。
リポジトリ(repository)とは、収納場所・倉庫・貯蔵庫を表す言葉です。
DDD(ドメイン駆動設計)では、リポジトリはモデル駆動設計でドメインをモデリングする際のビルディングブロックの1つになっています。ビルディングブロックとは基本構成要素のことで、ドメインをモデリングする際の基本部品として使います。
DDDのリポジトリの役目は、ドメインレイヤーのオブジェクトから永続化レイヤーを隠蔽することです。リポジトリ="エンティティの貯蔵庫"という抽象化されたオブジェクトを持ち込み、ドメインレイヤーの内部では貯蔵庫からエンティティを取り出すように設計・実装します。
構築するシステム(ここでは何か1つのシステムのみをイメージしてください)においてアーキテクチャが決定すると、その段階では永続化レイヤーに使われるコンポーネントが1つ選定されているでしょう。DDDのリポジトリは、ドメインレイヤーの中で永続化コンポーネントとのやり取りを担当します。根本的には、リポジトリは何らかの永続化コンポーネントに依存することになります。
依存関係逆転の原則
ドメインレイヤーはピュアに保つ。これがドメイン駆動設計における基本ポリシーです。しかし、外部とのやり取りを責務として持つオブジェクトについては、根本的に外部への依存を無くすことはできません。次の図のように、何らかのORMコンポーネントを利用した実装になるのは必然で、つまり依存が持ち込まれます。例えばSymfony Standard Editionの構成であれば、ORMコンポーネントにDoctrineが使われます。リポジトリは、Doctrineの提供するリポジトリ用の基底クラスであるEntityRepositoryを継承した実装になります。ドメインレイヤーの実装をドメインパッケージ、ORMはORMパッケージとしてまとめられていることを想定しています。
Tumblr media
このように外部への依存を持ってしまった部分に対して、オブジェクト指向のクラス設計原則(SOLID原則)の1つである依存関係逆転の原則(DIP:Dependency Inversion Principle)を適用するとどうなるでしょうか。
依存関係逆転の原則では、直接依存しあう2つのクラスについて、間にインターフェイスを設け、2つのクラスそれぞれがインターフェイスに依存するように変更します。リポジトリの例では、ドメインパッケージには特定のリポジトリのインターフェイス(例:UserRepositoryInterface)のみを配置するようにします。リポジトリの実装は、ドメインパッケージの外に配置した実クラス(UserRepository)にて行います。このクラスが、ドメインパッケージにあるインターフェイスを実装し、ORMコンポーネントの基底クラスを継承します。こうすることで、リポジトリの実装をドメインパッケージの外に出すことはできました。
それでは、ドメインパッケージの外に出たリポジトリの実装は、どこに属するのでしょうか?
リポジトリの実装は、リポジトリパッケージに配置することにします。このパッケージはドメインパッケージから依存関係の理由で分割したというだけであり、依然としてドメインレイヤーに属するものだということに注意してください。リポジトリパッケージを切り離したドメインパッケージは、外部依存を持たなくなり、凝集度が高くなっています。
Tumblr media
再利用・リリース等価の原則
しかし、ドメインレイヤーのオブジェクトは、それぞれ変更されていく可能性が高いものです。リポジトリの持つメソッドを追加したいということも出てくるでしょう。この場合、ドメインパッケージに属するリポジトリのインターフェイスと、リポジトリパッケージに属するリポジトリ実クラスの両方を修正しなくてはなりません。つまり、個別のリポジトリのインターフェイスというのは不安定(=変更が発生しやすい)なものだったわけです。不安定なものを抽象化することは、無理やり安定させようとするため矛盾・ひずみを生みます。結局ドメインの問題についての変更要求でドメインパッケージとリポジトリパッケージの両方に変更を加え、両方を同時にリリースするようになるでしょう。したがって、再利用・リリース等価の原則(REP:Reuse-Release Equivalency Principle)と照らしあわせても、これらは1つのパッケージにある方が自然と言えます。
また、現実のプロジェクトでは、リポジトリの操作には特定の汎用知識(問い合わせ方法)が必要になるでしょう。この用語をリポジトリごと・プロジェクトごとに再定義するのは手間であり、また、多くの場合この問い合わせ言語はドメインの本質部分ではありません。ORMの提供する問い合わせ用メソッドを「問い合わせドメインの言語」として自ドメインに輸入してしまうのが現実的です。
Doctrineの提供するリポジトリ基底クラス(EntityRepository)のインターフェイスが安定しているということも前提になります。
まとめ
DDDのリポジトリとORMコンポーネントの関係に着目して依存について解説しました。依存の排除に固執しすぎると、本来注力すべき部分から外れてしまう場合があります。パッケージの安定度を見極め、それに合わせた抽象化にとどめておくことで、「変更しやすさ」を維持していくことが肝要です。
参考
パッケージの安定度・不安定度といった尺度については、Robert C. Martin著『アジャイルソフトウェア開発の奥義 第2版』20.5.1「安定性」、20.5.2「安定性の尺度(目安)」を参照してください。
Robert C. Martin - Dependency Inversion Principle(PDF)
(山口 大輔氏による翻訳)
DIP:Dependency Inversion Principle - BEAR Blog
PHP ソフトウェアメトリック - BEAR Blog
PHPメンターズ流 設計と実装の型
2 notes · View notes