Quantcast
Browsing Latest Articles All 24 Live
Mark channel Not-Safe-For-Work? (0 votes)
Are you the publisher? or about this channel.
No ratings yet.
Articles:

「DDD」にまつわる諸課題の整理

DDD的なことを今後進めていく上で、自分として課題としている論点をまとめてみた。あくまで私の現時点での理解度を前提に、そこでの個人的課題感を取り纏めてみたものなので、不足や誤りや過剰が多々あるだろうがご容赦を。そして、アドベントカレンダーの初日、続く議論の礎となり、3人の賢者24人の荒ぶる者によってベツレヘムの星が見出されることをただ願うのである。

    ◇

あらまし

  1. DDDのいわゆる戦略の言う“広義のドメイン”と、戦術パターンにおける“狭義のドメイン”。この二つの用語は、実のところ「異なる境界付けられたコンテキストに属する語彙」として扱った方がよいのではないか?

  2. 参照系と更新系は、その本質として非対称性があって、参照系向けの新たな戦術パターンが必要なのではないか?

  3. 「コンテキスト」は、強く分離する上位階層から緩く分離する下位層まで、多段階の階層構造に在ると捉えるべきではないか?

  4. (「3.」を前提に、)「コンテキスト」と「(狭義の)ドメイン」は多対多の関係にあり、ドメインはコンテキストが定めるスコープの配下に整理できる、というものでは無いのではないか?

  5. Evans氏がDDD本を上梓した頃と現代では、それこそ前提となるコンテキストが相当に異なってきていることは常々念頭に置いた方がよいのではないか?

    ◇

以下それぞれについて述べる。

1. 「広義のドメイン」と「狭義のドメイン」は使い分けないとならないのでは?

まず、DDDのいわゆる“戦略”を重視しているか、目下の“戦術パターン”に取り組んでいるか、そういった立ち位置の違いから、「ドメイン」という用語へ込める意味合いに差異が出ている場面がある様に思う。

広義のドメイン
ほぼドメイン工学のいう「ドメイン」であり、何らかの問題領域に対するスコープの認識を表している。よって、、「何だって対象になる。どんな問題領域にも、その問題領域としてのモデルは描ける。ただし、当該ドメインのモデル化技法自体、そのドメインに深くダイブすることでのみ初めて見出される。」、、といった主張となる。文脈を規定するにも、語彙を収集するにも、それ以前に、ドメインの“先住民”の風習の観察から始まる、こととなる。(※「ドメインの“先住民”」は「ドメインエキスパート」を再定義する用語としてとても良い気がしてきた。)
「広義のドメイン」を用いる立場からみると、「狭義のドメイン」を用いている人たちが、プレゼン層やインフラ層をモデルの外部として、それこそドメイン外=定義域外として、実質的に取扱対象外としているような様子が片手落ちに見える。確かにアプリ層やドメイン層には、例えばEntityといったモデル化技法が有用だとしても、プレゼン層(≒UI)には(自分はよく知らない領域だが、)それとしてのモデル技法があるはずだし、インフラ層には、(これはだいたい知っているが、)RDBのERモデルがよく使われている。WebベースUIにはWebベースUIの、RDBにはRDBの、もちろんOO言語によるロジック記述にはそれとしての、それぞれの“ドメイン”にそれぞれのモデル化技法があり、それら全てを高次の階層で統合してこそのシステム・アーキテクチャー、という話になる。
狭義のドメイン
粗い理解としては、、MVC改めの「プレゼン層-ビジネスロジック層-データ層」なる論理3層アーキテクチャーの改善系として、論理4層を提唱しているところのものである。プレゼン層とデータ層(=DDD的にはRepository以下インフラ層)はいいとして、ビジネスロジック層が単層ではうまくない場面があって、上層のアプリ層と下層のドメイン層に分けたらより良かった、、という発展のストーリーとして位置付けることができる。このとき、個々のユースケース(個別の要求)によって揺らぐことのより少ない下層側を、アプリケーションから基本的には独立したドメイン、と捉える。

この二つの「ドメイン」は、正直、“異なる境界づけられたコンテキストに属した、異なるユビキタス言語の語彙”になっていると思う。だから、同じ「DDD」について語っているようでも、人によって「ドメイン」という語で何を言い表そうとしているか、実際よくずれている。

ということは、本記事における「ドメイン」も、どちらのコンテキストに於けるものか明示する必要がある。本記事の以降で言及する「ドメイン」は、(明示的に修飾されてない限り、)全て「狭義のドメイン」である。

2. 参照系と更新系は、その本質として非対称なのではないか?(参照系向けの新たな戦術パターンが必要なのではないか?)

CQRSによって、特に高トラフィックの場合に、参照系(Read系)を別サブシステムに分離するという方式が一般に定着した。このとき、概念的に同一のデータ種(※例えば、「ECにおける商品カタログ」、といった意味で同一)であっても、参照系(Read系)と更新系(Write系)とで、異なるService/Entity/Repository設計となるであろう。というか異なる設計や実装とできるように分離しているわけだ。

イベント伝播で連携させる本格的なCQRSを導入するまでもなくとも、単一RDBベースであっても、厳格にService/Entity/Repositoryを導入するのが、参照系ロジックでは(※例えば、「商品一覧」といったユースケースで)過剰に感じたり、逆にSQL JOINするのに集約跨ぎについての設計上の折り合いをいちいち付けなければならないことが不自由に感じたり、してないだろうか。

たぶん、ここには何か本質的なテーマがあると思う。

(CQRSといった大掛かりなアーキテクチャーを必要としない場面でも、)そもそも本質的に、更新系ロジックは厳格でいたいのだが、参照系ロジックはもっと自由でいたいのだ。たぶん、Service/Entity/Repositoryといった厳格なパターンは、更新系には相応しいかもしれないが、参照系には何かもっと別のやり方が必要、という要請が現に存在するのだ。

SQL JOINするのはDDD的にどうなのであろうか?では、GraphQLはどうなのか?「JOINを含む複雑なSQL SELECT」とGraphQLとは、意味論的に何か差異があるのだろうか?(分散データソースを跨いでの"JOIN"なり"Mash-up"なりができるという付加価値はあるにしても、)もし意味論的にほとんど同じなのだとしたら、違いは何か?違いはその処理が置かれるレイヤーである。参照系ロジックにおける"Mash-up層"が、サーバサイドあるいはバックエンドの奥深くにあるのが嫌なのだ。やっぱりBFF(Backend-for-Frontend)くらいに置きたいのだ。なぜか?参照系ロジックに対する要件は、基本的にデータ利用側(=フロントサイド側)から「これこれこのようにデータを見たい」という形で発せられ、対して、更新系ロジックに対する要件は、基本的にデータ提供側(=永続化データオーナー側)から「このようにのみ更新可能とさせたい」という形で発せられる。要件のオーナーシップが、参照系ではフロントサイドにあり、更新系ではサーバーサイドにあるのだ。参照系ロジックは相対的にフロント側に責務があり、更新系ロジックは相対的にサーバーサイドに責務がある。、、たぶんこういうことになっているのではないかと考えている。

3. 「コンテキスト」は階層的に在ると捉えるべきではないか?

もはやローカル定義に無しに「マイクロサービス」と云うのはただ混乱の元ではあるが、まあ構わず進めてみる。何かアプリケーションをマイクロサービスに分割した時、それぞれのマイクロサービスは異なる「(境界付けられた)コンテキスト」を構成するのであろうか?まー、だいたい「Yes」というのが相場かと思う。

ただ、ちょっと「境界付けられたコンテキスト」の戦略的な意味を思い出すべきである。「境界付けられたコンテキスト」とは、ある一式のユビキタス言語語彙セットが通用できるスコープのことのはず。そこにある二つのマイクロサービスは、ユビキタス言語セットとしても確かに分断されているという程に分断されているのだろうか?

コンウェイの法則または逆コンウェイの法則として、マイクロサービス分割とチーム構成は相似形であるべき、とされる。そうだとして、複数のマイクロサービスそれぞれを担当する複数のチームは、ユビキタス語彙セットを共有していない/できない、というほどに独立しているのだろうか?まー、程度の問題なのだとは思う。

そう、程度の問題なのだと思う。だとしたら、コンテキスト分割の程度も程度の問題、として扱うべきではないか。

EC事業でAmazonなどのモールや決済などで外部サービスと連携する場合や、大企業グループ内システムで他部門の明らかに独立に運営されているシステムとデータ連携するような場合は、自分たちのシステムと彼らのとの間には、明確に「境界付けられたコンテキスト」の境界が引かれている/引くべきである、という考えで妥当であろう。しかし、隣のチームのマイクロサービスとの間には、そこまで明確な境界はないであろう。仮に共有カーネルするとしたら、例えば実は50%くらい共有することになってないか。それって、(境界付けられた)コンテキストとして異なっている、と云う必要がある/云うべき/云うのが妥当、なのだろうか?

結局のところ、“自分たち”のシステムを最大粒度で捉えたときに最低一つの明確な境界付けられたコンテキストは必ず存在するが、その内部構造としては、自分たちのチーム編成に対応して開発・運用されるようなマイクロサービス構成などについては、よりゆるく分割されている“サブ(部分)・コンテキスト”といった概念で捉えるのが妥当ではないだろうか。

このようなコンテキストの階層的理解を導入すれば、CQRSなどでの参照系と更新系の分離も比較的位置付けやすくなるであろう。即ち、「概念的に同一のデータ種であり、大粒度の境界付けられたコンテキストとしては同一であり、共通の語彙セットの元にある。ただ、参照系と更新系で小粒度のサブ・コンテキストは異なり、コードの共有はされない(もしくは共有の程度が少ない)、としている」と。

加えて、一度階層の存在を容認するならば、それは2階層であると規定する理由は無く、理屈の上では多段階に展開し得るとする。

4. 「コンテキスト」と「ドメイン」のインピーダンス・ミスマッチ、"Beyond DDD"としてのDCIもしくはサブジェクト

結論からいうと、個人的には『「ドメイン」は一つの「コンテキスト」に収まっている』という考えに疑問を持っている。つまり、『「コンテキスト」を跨いで横断的に存在し得るような「ドメイン」』、というものを積極的に考慮しなければならないのではないか、と考えている。

「共有カーネル」という考えは、既に「境界付けられたコンテキスト」の概念とコンフリクトしていると思うのである。「境界付けられたコンテキスト」が異なれば定義上、語彙セットのシェアは出来ないはずである。が、語彙のサブセットをシェアした方が“現実的”なとき、共有してしまえ、という。「共有カーネル」というパターン自体は現実に当然に必要なアプローチではあるが、「共有カーネル」が必要と思われた時点で、当初想定のコンテキストを横断してドメインが存在していることを示唆しているのではないか。

もちろん、単なる言い方、捉え方の問題である。「分断されたコンテキストが原則で、例外的にシェアもある」と捉えるのか、「一般にドメインはコンテキスト横断的である。ただ、コンテキストを跨がないように統制することができるならその方がシンプル。」と捉えるのか。

重要な補足となるが、本節での「ドメインはコンテキストを跨ぐ」という考えは、前節での「コンテキストは階層的に在ると捉える」という考えの導入を前提とする。コンテキストが階層的で無いとするならば、比較的大粒度の唯一の境界付けられたコンテキストだけが存在するだろうから、確かに自ずとドメインはコンテキストに収まる。しかし、マイクロサービスという現実などを見ると、たぶん“サブ・コンテキスト”といった緩い分割や、さらにその階層が多段階に展開されるだろうことを扱った方がよいと思える。だとしたら、ドメインはそのようなサブ・コンテキストやサブ・サブ・コンテキストを跨いで横断的に存在することになるだろう、という主張である。

    ・

この考えにはネタ元がある。次の萩原正義氏による2005年の連載である。

@IT、「次世代開発基盤技術“Software Factories”詳解」

この連載の中のこちらのページの中(※ページ中程の「(5)要求モデルとソフトウェア資産モデルのインピーダンス・ミスマッチ」の節)に言及があるところの「何をしたいかを表す要求モデル」と「どうあるべきかを表す資産モデル」である。端的に、DDDのコンテキストは要求分析あるいはロードマップから得られたスコープであり、DDDのドメインは、まさに対象システムがどうあるべきかの構造であろう。上記萩原氏の記事における「要求モデル」と「資産モデル」が、DDDのコンテキストとドメインに対応すると見ることが妥当で、かつ萩原氏の「インピーダンス・ミスマッチ(=上位層から下位層への接続がツリー構造とならない)」認識が妥当だとしたら、DDDのコンテキストとドメインにもインピーダンス・ミスマッチは存在するはずだろう、となる。

    ・

もう一つのネタ元は、Coplien氏らのDCIである。

https://en.wikipedia.org/wiki/Data,_context_and_interaction
https://sites.google.com/a/gertrudandcope.com/www/thedciarchitecture
https://klevas.mif.vu.lt/~donatas/Vadovavimas/Temos/DCI/2009%20The%20DCI%20Architecture%20-%20A%20New%20Vision%20of%20OOP.pdf

Coplien氏は、おそらく世界で一番(※もしかしたらEvans氏自身よりも)DDDのエッセンスを理解していて、そして、コンテキストとドメインのインピーダンス・ミスマッチ問題(および、DDDの戦略と戦術パターンの話の間の論理ギャップ)に対して、おそらく世界で最も強烈にマサカリを投げている。いくつかリンクを貼っておく。

https://dddeurope.com/2016/jim-coplien.html
https://togetter.com/li/877728
https://togetter.com/li/927916
https://togetter.com/li/1062864

DCIとDDDの共通点は、、DCIの「コンテキスト」は、DDDのコンテキストにほぼ対応する。ただし、「境界付けられたコンテキスト」と云うほどの大粒度さはなく、本記事中での“サブ・コンテキスト”程度の粒度感。また、DCIの「データ」は、DDDのドメインにほぼ対応する。そして違いは、、DDDでは、「コンテキスト内にドメインが収まる」とするが、DCIでは、コンテキストとデータは直結せず「ロール」を介する。この「ロール」がインピーダンス・ミスマッチ問題への解となっている。

    ・

私は、全てのDDD実践者に、同時にDCIに取り組むことを強く薦めたい。(あるいは、荻原氏の記事でのサブジェクト指向でも良い。)

DDDは、従前のMVC改めの「プレゼン層-ビジネスロジック層-データ層」なる論理3層アーキテクチャーを改善し、ビジネスロジック層を単層から、アプリ層とドメイン層の二つに分離した。この垂直分離については、荻原氏の記事では「ユースケース-ドメインオブジェクト」として、DCIでは「インタラクション-データ」として扱っており、これら三つはほぼ対応している。水平分割についてはどうか?荻原氏の記事では、ユースケースとドメインにはインピーダンス・ミスマッチ(=水平分割におけるトレーサビリティ・ギャップ)があるとして、そのズレを仲裁する「サブジェクト」を導入している。DCIでは、同様にインタラクションとデータを仲裁するものとして「ロール」を導入している。しかし、DDDは、水平分割について何も言って無い。

5. "DDD isn't done"?

Evans氏のDDD本が出版されたのは2003年。日本で最初に一般に広まったのはたぶん'05年〜'06年からくらいで、当時オンライン記事としてはオージス総研社の解説記事くらいが頼りだった。

オブジェクトの広場、「DDD難民に捧げる Domain-Driven Designのエッセンス」

当時と今とでは前提環境が相当に異なる。当時DDDへの関心を持っていた者は、エンタープライズ系のシーンにいる者たちであり、ほとんど単一RDB前提で、スケーラビリティへの考慮はそれほどは不要で、コンテキストが交錯する状況も少なかった。現代のDDD実践者は、DDDの戦術パターンはそういう時代に形作られたという歴史的背景は念頭に置いたらよいと思う。もちろん、プラクティスの基礎としては依然有用である。しかし、CQRSやMSAの考えをDDDにリンクさせての実践手法が試みられている現代、DDD本の戦術パターンを教条的にやってるだけでは上手くいかないような場面が多くなった。一方、DCIやサブジェクト指向といった、DDDの戦術に大きく欠けていると云える部品を補った論は既に存在している。(※DCIは2009年発表、前出の萩原氏の記事は2005年のものである。)

Evans氏自身、"DDD isn't done"(≒未完了である)としてある種不完全さを認める発言をしているようである。(※単に、進化は止まってないぞ、というニュアンスかも知れないが、いずれにせよ防御的な発言であろう。)

https://www.infoq.com/news/2018/09/ddd-not-done (原文)
https://www.infoq.com/jp/news/2018/10/ddd-not-done (日本語訳)

(DCIやサブジェクト指向が何を言おうが、)実のところ、DDDの最大価値は、本記事冒頭で示した「広義のドメイン」に基づくところの戦略論にある。DDDの戦術パターンは、一つのメタモデルを目指したと言えるが、DDDの戦略論に基づくなら、特定の具体(メタ)モデルを提示した段階で、既に特定の(広義の)ドメインを念頭に置いている、こととなってしまう。DDDの示す戦術パターンは、ある種の業務システム構築上のプラクティスとしては有用だが(※本記事もほとんど戦術パターンの話だし)、最大価値である「対象領域を対象領域自身の言葉で表現するのだ」という行動原理の価値を薄めてしまったかもしれない。このへんは、上記InfoQの記事によると、Evans氏自身考えあぐねているようである。

ぐるぐるDDDで気をつけてること

External article

集約の境界と整合性の維持の仕方に悩んで2ヶ月ぐらい結論を出せていない話

External article

Microservices と DDD

External article

エンティティの同一性を表現するためにequalsをオーバーライドすべきか否か

External article

ドメインオブジェクトとユースケースの関係について

External article

DDDと私

External article

ドメイン駆動設計を勉強するときのオススメ資料

この記事は、ドメイン駆動設計 #1 Advent Calendar 2018の9日目です。
明日は@kmdsbngさんです。

今回は、ドメイン駆動設計(以下DDD)を学ぼうとする人に対して参考になる資料をまとめます。

DDD関連資料のオススメ

まずはDDDの青い本、エリック・エヴァンスのドメイン駆動設計から手を出したいところですが、500ページ超えで分厚く、初学者の人とっては解説される内容が抽象度が高く、理解するのに苦労すると思います。

ですのでこれから紹介するSTEPの順番から読んでいくのことをオススメします。

STEP1

まずはDDDの概念から理解していくことから始めましょう。下記の本がオススメです。

わかる!ドメイン駆動設計 ~もちこちゃんの大冒険~

わかる!ドメイン駆動設計.jpg
https://booth.pm/en/items/392260

この本はストーリー形式でDDDを解説されていますので比較的理解しやすいと思います。
技術同人誌らしく、可愛らしいイラストもあって心理的ハードルを下げてくれます。

現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法

61d7GWbXVJL.jpg
https://www.amazon.co.jp/dp/477419087X/

この本は、DDDの構成要素の一つであるオブジェクト指向を解説しながら
設計について学べる本です。
DDDについてあまり言及はしていませんが、内容的にはDDDに沿っていますので、イメージしやすくなると思います。
また、著者の増田さんはDDD実践者の第一人者の一人です。
SlideshereでDDDについて多くの解説がありますので理解の助けになると思います。

STEP2

ここからより深くDDDについて理解していくための資料を紹介します。

Domain Driven Design(ドメイン駆動設計) Quickly 日本語版

frontcover_ja_small.png

https://www.infoq.com/jp/minibooks/domain-driven-design-quickly

エリック・エヴァンスのドメイン駆動設計の書籍の内容を要約したものです。
エリック本を読み始める前に読むといいと思います。

エリック・エヴァンスのドメイン駆動設計

51rgnUkmlzL.jpg
https://www.amazon.co.jp/dp/4798121967/

ここでやっとエリック本を紹介します。
DDDの原典ですので、DDDのすべてが書かれています。
ただ2003年に刊行されてますので、一部古くなっている記載がありますが
それを踏まえても名著だと思います。

またDDD難民に捧げるDomain-Driven Designのエッセンスを並行して読むのをオススメです。
DDDの用語集として理解の助けになると思います。

STEP3

ここからより具体的に実践と実装方法について学べる資料を紹介します。

実践ドメイン駆動設計

91aTKucFSKL.jpg
https://www.amazon.co.jp/dp/479813161X/

通称IDDD本
具体的なコードを元に、実践的なDDDのやり方が書かれています。
技術的な内容になってますので、人によってはエリック本より先に読むのもありかなと思います。
要約として「実践ドメイン駆動設計」 から理解するDDD (2018年11月)というスライドがあります。
読み終わったあとに内容を整理するのに助けになると思います。
↑書籍版が出てます。
image.png

https://www.amazon.co.jp/dp/4798161500/

.NETのエンタープライズアプリケーションアーキテクチャ第2版

51c52CsHWTL.jpg
https://www.amazon.co.jp/dp/B00ZQZ8JNE/

.NETとありますが、コードサンプルが.NETベースなだけで他の言語でも適用可能な内容になってます。 DDD、CQRS(Command/Query Responsibility Segregation)、イベントソーシングを中心にアーキテクチャ設計の原則の実践と実装方法について書かれています。
この本の特徴としてCQRSとイベントソーシングの具体的なソース例があるところです。
私はまだ読んでる途中ですが、CQRSとイベントソーシングを採用しようとしたときに参考になりそうだと思いました。

その他オススメ資料

ここまで紹介してきた資料を読破できたなら、DDDについて大分理解ができる人になってると思います。
ただそれでも分からないことが出でくると思いますのでコミュニティに頼るのも手です。
DiscordにDDDのコミュニティがありますので参加してみてはいかがでしょうか!


以上、ここまで読んでくださった方々の助けになれば幸いです。

ユビキタス言語についての知見を共有します

この記事は、ドメイン駆動設計 #1 Advent Calendar 2018の10日目です。

ユビキタス言語は大事

DDDは分類手法の一つという側面があります。

分類の道具は境界づけられたコンテキストと、ユビキタス言語です。
境界づけられたコンテキストで、システムの対象業務を分類し、境界づけられたコンテキスト内部ではユビキタス言語で言葉を分類します。

個人的に、ユビキタス言語はDDDを実践する上でとても大事なテクニックだと思うのですが、何を指すものなのか、どういうメリットがあるものなのかわかりにくいために、初心者にとってはわかりにくいものかもなー、とも感じます。
そこでこの記事では、ユビキタス言語についての私の知見を書いてみます。ユビキタス言語についての学習の助けになれば幸いです。

ユビキタス言語とは何か?

境界づけられたコンテキストとは「共通の用語が通じる業務の範囲」のことです。
DDDは全体で同じ用語を使うことは無理と考えますが、ある範囲であれば共通の用語を使えると考えます。その範囲が境界づけられたコンテキストです。

境界づけられたコンテキストは名前空間として働きます。ユビキタス言語とはコンテキストの内部だけで有効な用語集です。コンテキストを越えると同じ名前の用語であってもユビキタス言語としては通じず、別の概念になります。

例えば発送処理と請求処理でコンテキストを分けているとして、それぞれのコンテキストで「顧客」というユビキタス言語で定義された用語を使っていても、それらの用語は別の概念だとみなさないといけません。

ユビキタス言語はどのような用語集になるでしょうか?例えば発送処理のユビキタス言語は「発送」「発送者」「郵送先」「日付指定」「配達業者」「運転手」のような言葉を使うかもしれません。
大事なことは、ドメインのユーザーが使う言葉とプログラムを構成する言葉を一致させることです。プログラムの中で使われているクラス名、メソッド名をユーザーが見れば意味が通じないといけませんし、ユーザーが使っている言葉がついたクラスは、実際の言葉が指し示すものと同じように振る舞わなければいけません。

ユーザーが使っている言葉でプログラムを検索すれば、該当する概念の挙動を表しているクラスに行き着くようになってないといけませんし、そうなっている状態が保守しやすいプログラムであると言えます。ユーザーの言葉からプログラムの言葉に翻訳しないといけなかったり、一つの概念が複数のファイルに分割されている状況は可能な限り避けないといけません。

なぜ技術用語でなくドメインの言葉で分類するのか?

同じ言葉を使ってユーザー、ドメインエキスパート、開発者などの関係者が会話することで、ドメインを効率的に学習するため。
また、そうすることでうまく分類でき、理解しやすく保守しやすいシステムにすることを期待します。
保守しやすいプログラムにすることも目的です。

ドメインの言葉を使えば適切に名前付けできるのか?

この点は私がDDDを学び始めた時の疑問の一つでした。
適切に名前付けするとは、以下の条件を満たしていること、と言えるでしょう。

  • 名前でドメインの概念を識別できること
  • 理解しやすい名前付けができること

それぞれについて考えてみます。

ユーザーが使っている言葉を使って、適切に識別できる名前付けができるのか?

たぶんできる。
ユーザーは混乱の無いように業務を回しているというのがその根拠です。
曖昧な言葉を使っていたら、効率的に業務を行うことはできない。なので曖昧な用語は使わないように業務で使う言葉が洗練されている、と期待できます。

ユーザーが使っている言葉を使って、理解しやすい名前付けができるのか?

これもたぶんできる。けど、一見わかりにくい言葉を使っていることはありうる。
業務を回すには、曖昧な言葉を使うのはNGだが、関係者だけに通じる言葉が使われているかもしれません。
外部の目から見て不必要にわかりにくい言葉が使われているなら、それを理解しやすい言葉に置き換えてもいいかもしれないし、すでに共通言語として使われているメリットを活かすなら、そのまま使い続ける判断もあるでしょう。

ユビキタス言語を作る時の注意点

ユビキタス言語は、ユーザーが使っている用語を使うだけでなく、開発者も含めて共通に使う言葉を育てていくのだそうです。
ユーザーも業務で実際に使う必要があるため、以下の点に気をつけて言葉を選んでいかないといけないでしょう。

  • 同一コンテキストの中では、たとえモジュール(パッケージ)が別れていたとしても、同じ名前を異なる概念に対してつけてはいけない。パッケージ名を取り除いた名前だけで識別できないといけない。 普段の会話の中で、パッケージ名を付けて呼んだりはしないので。
  • 誤解を生じにくい言葉を選ばないといけない。そうしないとコミュニケーションが滞ってしまう。

「重複している」「曖昧である」と感じたなら、それは適切な名前付けができてないサインです。
そういうコミュニケーションのわだかまりを学習の機会と捉えて、言葉を整理して、ユビキタス言語を育てていかないといけません。

以上、ユビキタス言語の知見の共有でした。ご参考まで。

ドメイン駆動設計における2つの『不変』

この記事は
ドメイン駆動設計 advent calendar 11日目
の記事です。

日本語版だとわかりずらい「不変(不変条件)」

エヴァンスのドメイン本では、頻繁に「不変(不変条件)」という言葉が出てきます。Kindleで検索してみたところ、83件でした。

分類してみると、主に2箇所でよく使われています。
1つは「ValueObject(値オブジェクト)」の項。もう一つは「Aggregate(集約)」の項です。
Factoryでも多く使われていますが、こちらはAggregateの不変条件に関連する内容ですね。

さて、実は「ValueObject」と「Aggregate」で使われている「不変(不変条件)」の意味って全然違う意味なんです。
弊社でドメイン駆動設計の読書会をしているなかで、話題になり目から鱗が出てしまったので紹介しますね。

「ValueObject」と「Aggregate」の項をそれぞれよく読むと、「ValueObject」では「不変」、「Aggregate」では「不変条件」という言葉で表現されています。

そこで、エヴァンズ本の索引を見てみます。

索引を読もう

索引を読むと、「不変」と「不変条件」の項目は別れて解説されています。

  • 不変(immutable) 生成された後、目に見える状態が変化しないという性質
  • 不変条件(invariant) 何らかの設計要素についての表明で、常に真でなければならないもの。ただし、メソッドの実行中やまだコミットされていないデータベーストランザクション中といった特殊な過渡的状況を除く

なんと!!!元の英語の用語からして違う言葉!!!「不変」は「immutable」、「不変条件」は「invariant」という用語でした。

原本を見てみよう

ということで原本を見てみましょう。

  • ValueObject・・・When you care only about the attributes of an element of the model, classify it as a VALUE OBJECT. Make it express the meaning of the attributes it conveys and give it related functionally. Treat the VALUE OBJCET as immutable.

  • Aggregate・・・The root ENTITY has grobal identity and is ultimately responsible for checking invariants.

それぞれの要素の説明の一部を抜粋しましたが、やはり使い分けられてますね。

英和辞典(英辞郎より)を見てみよう

さらに英和辞典で両方の用語を調べてみました。

  • immutable

【形】
〔法則・規則・決定事項・運命などが〕変わらない、変化しない、不変の、不易の、変えることのできない、変更不可能な

  • invariant

【名】
《数学》不変式、不変量
《コ》不変条件
【形】
不変の、変わらない

うーん、いまいち。。。invariantの方は数学的な意味合いがある感じでしょうか?

WikiDiffなるもので調べてみた

WikiDiffなるサイトがあり、ここでimmutableとinvariantのDiffを取ってみました。
https://wikidiff.com/immutable/invariant

  • immutable
    • Unable to be changed without exception. The government has enacted an immutable law.
    • (programming, of a variable) Not able to be altered in the memory after its value is set initially, such as a constant.

なんとなく、変更不可という意味合い。一度決まったら例外なく変更してはいけないという意味合いでしょうか。

  • invariant
    • not varying; constant
    • (mathematics) Unaffected by a specified operation (especially by a transformation)
    • (computing, programming) Neither covariant nor contravariant.

なんとなく、変更不可といった意味ではなく、変化しない、他から影響されては変化しない。といった意味合いに思えます。

もう一度、Aggregateの不変条件とは

もう一度、Aggregateの不変条件について考えてみます。実は不変条件とは何かということについては、書籍内で明確に説明されています。

不変条件(原書ではInvariant)とは、データが変更される時は常に維持されなければならない一貫性のルールで、集約のメンバ間の関係も含んでいる。複数の集約にまたがるルールはどれも、常に最新の状態にあるということが期待できない。イベント処理やバッチ処理、その他の更新の仕組みを通じて、他の依存関係は一定の時間内に解消できる。しかし、1つの集約内で適用される不変条件は、各トランザクションの完了によって強制される。

要するに・・・

invariantについては、書籍内でその意味が再定義されているということですね。
なので、immutableとは明確に意味が違うので、用語も分けているというわけでした。

これが何の役に立つかと言われると微妙なところですが、「不変」という用語を複数の意味で使われていると思っていた方にとっては少しは意味が明確になるのではないかと思い記事にしてみました。。。

アプリケーションサービスの凝集度を高めたい

ドメイン駆動設計 #1 Advent Calendar 2018の 12 日目担当記事です。
11 日目 は @YasuhiroKimesawa さんのドメイン駆動設計における2つの『不変』です。
13 日目は @dskst さんのDDDで学ぶAPI設計の勘所です。

数年前 IDDD のソースを読んでいたときに考えていたことを言語化してみました。
戦術的設計に関する記事になります。よろしければお付き合いください。

はじめに

アプリケーションサービスをご存知でしょうか。
まず大前提としてアプリケーションサービスの知識がないと、この記事は全く意味がないのでアプリケーションサービスについての簡単な解説をします。
既にご存知の方はこの章は読み飛ばして構いません。

アプリケーションサービスはエンティティや値オブジェクトなどのドメインオブジェクトを協調させて処理を行う、スクリプトのような振る舞いを持つオブジェクトです。
エンティティや値オブジェクトはそのまま利用するには粒度が細かすぎる場合があり、それらをまとめあげるための「サービス」です。
より身近な表現をすればアプリケーションの API と表現するとわかりやすいでしょうか。

たとえば MVC フレームワークを利用した Web システムを例にしてみましょう。
まずはアプリケーションサービスを利用しないパターンです。

public class UserController : Controller {
  private readonly IUserRepository userRepository;

  public UserController(IUserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public IActionResult CreateUser(CreateUserRequestModel request) {
    using (var transaction = new TransactionScope()) {
      var userName = request.UserName;

      var found = userRepository.FindByUserName(userName);
      if (found != null) {
          throw new Exception("duplicated");
      }

      var user = new User(userName);
      userRepository.Save(user);
      transaction.Complete();
    }

    return View();
  }
}

コード自体に問題はありません。
しかし、もしもフレームワークを変更することになった場合にはどうなるでしょうか。

UserController というクラスは MVC フレームワークに依存したクラスです。
移植先のフレームワークが同じプログラミング言語のフレームワークであったとしても、そのまま移植するわけにはいきません。

恐らくそういったコードはいたるところに記述されているでしょう。
このようなコードを現行のフレームワークから引きはがして別のフレームワークに移植するのは並々ならぬ労力が必要です。

さて、今度はアプリケーションサービスを使った場合を見てみましょう。

public class UserController : Controller {
  private readonly UserApplicationService userApplicationService;

  public UserController(UserApplicationService userApplicationService) {
    this.userApplicationService = userApplicationService;
  }

  public IActionResult CreateUser(CreateUserRequestModel request) {
    var userName = request.UserName;
    userApplicationService.CreateUser(userName);

    return View();
  }
}
public class UserApplicationService {
  private readonly IUserRepository userRepository;

  public UserApplicationService (IUserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public void CreateUser(string userName) {
    using (var transaction = new TransactionScope()) {
      var user = userRepository.FindByUserName(userName);
      if (user != null) {
          throw new Exception("duplicated");
      }

      var newUser = new User(userName);
      userRepository.Save(newUser);
      transaction.Complete();
    }
  }
}

UserControllerUserApplicationServiceというクラスに処理を移譲するようになりました。
UserApplicationServiceには MVC フレームワーク特有のコードは現れておらず、MVC フレームワークに依存していないといえる状態にあります。
このような形になっていれば、もしもフレームワークを変更することになったとしてもUserApplicationServiceをそのまま流用することができるのでそれほど問題は起きないでしょう。

既にクラス名からしてお気づきでしょうがUserApplicationServiceがアプリケーションサービスです。

いよいよ本題

アプリケーションサービスについてイメージがついたところで本題に入ります。

次のサークル機能(ユーザ同士のグループを作る機能)を実現するアプリケーションサービスをご覧ください。

public class CircleApplicationService {
  private readonly ICircleRepository circleRepository;

  public CircleApplicationService(ICircleRepository circleRepository) {
    this.circleRepository = circleRepository;
  }

  public void CreateCicle(string circleName) {
    using (var transaction = new TransactionScope()) {
      var circle = circleRepository.FindByCircleName(circleName);
      if (circle != null) {
        throw new Exception($"duplicated (CircleName:{circleName})");
      }

      var newCircle = new Circle(circleName);
      circleRepository.Save(newCircle);

      transaction.Complete();
    }
  }
}

CircleApplicationService はサークル機能に関するアプリケーションサービスです。

現在のCircleApplicationServiceは未完成です。
なぜならCircleApplicationServiceはサークルを作ることができても「サークルにユーザを所属させる」ことができません。
ユーザを所属させるためには次のようなメソッドを追加する必要があるでしょう。

public class CircleApplicationService {
  private readonly ICircleRepository circleRepository;
  private readonly IUserRepository userRepository;

  public CircleApplicationService(ICircleRepository circleRepository, IUserRepository userRepository) {
    this.circleRepository = circleRepository;
    this.userRepository = userRepository;
  }

  public void CreateCicle(string circleName) {
    using (var transaction = new TransactionScope()) {
      var circle = circleRepository.FindByCircleName(circleName);
      if (circle != null) {
        throw new Exception($"duplicated (CircleName:{circleName})");
      }

      var newCircle = new Circle(circleName);
      circleRepository.Save(newCircle);

      transaction.Complete();
    }
  }

  public void Join(string circleId, string userId) {
    using (var transaction = new TransactionScope()) {
      var circle = circleRepository.Find(circleId);
      if (circle == null) {
        throw new Exception($"circle not found(id:{circleId})");
      }

      var user = userRepository.Find(userId);
      if (user == null) {
        throw new Exception($"user not found (id:{userId})");
      }

      circle.Join(user);
      circleRepository.Save(circle);

      transaction.Complete();
    }
  }
}

これでユーザをサークルに所属させることができるようになり、サークル機能を無事に完成させることができました。

このクラスはきっとうまくやっていくと思います。
しかし、最初にこのようなコードを見たときに疑問を感じました。
その疑問というのがタイトルに記載されている凝集度についてです。

凝集度

コードが望ましいものであるか、という指標の一つに凝集度というものがあります。

凝集度はクラスの責任範囲がどれだけ集中しているかを測る尺度です。
クラスの責任範囲が狭まるほど、一つの事柄に特化することになるので、凝集度は高い方が望ましいとされています。

さてこの凝集度を測るには LCOM(Lack of Cohesion in Methods)という計算式があります。
これはメンバー変数とそれが利用されているメソッドの数で計算される値なのですが、その計算式の内容は「メンバー変数はすべてのメソッドで利用される方がよい」といったものです。

凝集度がどういうものかは計算式を見るよりも例を見た方がわかりやすいでしょう。
たとえば次のコードは凝集度が低いコードです。

public class Sample {
  private int member1;
  private int member2;
  private int member3;
  private int member4;

  public int CalculateA() {
    return member1 + member2;
  }

  public int CalculateB() {
    return member3 + member4;
  }
}

member1member2CalculateAでしか使われておらず、member3member4CalculateBでしか使われていません。
つまりメンバー変数がすべてのメソッドで利用されていません。
凝集度としては「メンバー変数はすべてのメソッドで利用される方がよい」ので、Sampleは凝集度が低いモジュールになっています。

もしも凝集度を高めたい場合は、次のようにクラスを分割することで高めることができます。

public class SampleA {
  private int member1;
  private int member2;

  public int Calculate() {
    return member1 + member2;
  }
}

public class SampleB {
  private int member3;
  private int member4;

  public int Calculate() {
    return member3 + member4;
  }
}

どちらのクラスもすべてのメンバ変数がすべてのメソッドで利用されています。
凝集度の観点からすると、本来はこのように分かれてしかるべきクラスであったのです。

これらのクラスは凝集度が高いモジュールといえるでしょう。

もちろん必ずしも凝集度が高いということが正解ではありません。
高い方が好ましいというだけであって、そのコードを取り巻く環境によっては敢えて凝集度を下げることが正解となることも有り得ます。
あくまでも凝集度は望ましいコードかどうかを判断する一つの尺度に過ぎません。

アプリケーションサービスの凝集度

凝集度について理解したところでサークル機能のアプリケーションサービスを見てみましょう。

public class CircleApplicationService {
  private readonly ICircleRepository circleRepository;
  private readonly IUserRepository userRepository;

  public CircleApplicationService(ICircleRepository circleRepository, IUserRepository userReporitory) {
    this.circleRepository = circleRepository;
    this.userRepository = userRepository;
  }

  public void CreateCicle(string circleName) {
    using (var transaction = new TransactionScope()) {
      var circle = circleRepository.FindByCircleName(circleName);
      if (circle != null) {
        throw new Exception($"duplicated (CircleName:{circleName})");
      }

      var newCircle = new Circle(circleName);
      circleRepository.Save(newCircle);

      transaction.Complete();
    }
  }

  public void Join(string circleId, string userId) {
    using (var transaction = new TransactionScope()) {
      var circle = circleRepository.Find(circleId);
      if (circle == null) {
        throw new Exception($"circle not found(id:{circleId})");
      }

      var user = userRepository.Find(userId);
      if (user == null) {
        throw new Exception($"user not found (id:{userId})");
      }

      circle.Join(user);
      circleRepository.Save(circle);

      transaction.Complete();
    }
  }
}

このクラスではcircleRepository変数は全てのメソッドで利用されていますが、userRepository変数はJoinメソッドでは利用されているもののCreateCircleメソッドでは利用されていません。
凝集度という観点では、最高とはいえる状況ではなさそうです。

では、これは悪いコードなのでしょうか。

そうではないでしょう。

このモジュールは凝集度が最も高い値ではないというだけのことです。
サークルの機能が一つのクラスにまとまっているのはわかりやすいでしょう。

とはいえ、何かを問われたら「0か1か」で答えたくなってしまうのがプログラマの性分です。
もしもサークルアプリケーションサービスにおいて、最高の凝集度を追い求めると、どのような変化がコードに表れるのでしょうか。

凝集度を高めてみる

現在のところサークルに関する処理をまとめたアプリケーションサービスには「サークルを作る処理」と「サークルに所属する処理」という二つの処理が存在します。
これら処理はサークルというデータを扱うということで同じクラスに同居していますが、それぞれを独立して動作させても問題ありません。

凝集度を高めるためにそれぞれの処理をクラスに分割してみましょう。

処理を分割

public class CircleCreateService {
  private readonly ICircleRepository circleRepository;

  public CircleCreateService(ICircleRepository circleRepository) {
    this.circleRepository = circleRepository;
  }

  public void Handle(string circleName) {
    using (var transaction = new TransactionScope()) {
      var circle = circleRepository.FindByCircleName(circleName);
      if (circle != null) {
        throw new Exception($"duplicated (CircleName:{circleName})");
      }

      var newCircle = new Circle(circleName);
      circleRepository.Save(newCircle);

      transaction.Complete();
    }
  }
}
public class CircleJoinService {
  private readonly ICircleRepository circleRepository;
  private readonly IUserRepository userRepository;

  public CircleJoinService (ICircleRepository circleRepository, IUserRepository userRepository) {
    this.circleRepository = circleRepository;
    this.userRepository = userRepository;
  }

  public void Handle(string circleId, string userId) {
    using (var transaction = new TransactionScope()) {
      var circle = circleRepository.Find(circleId);
      if (circle == null) {
        throw new Exception($"circle not found(id:{circleId})");
      }

      var user = userRepository.Find(userId);
      if (user == null) {
        throw new Exception($"user not found (id:{userId})");
      }

      circle.Join(user);
      circleRepository.Save(circle);

      transaction.Complete();
    }
  }
}

二つのメソッドは二つのクラスになり、それぞれすべてのメンバ変数がすべてのメソッドで利用されている凝集度が高い状態となっています。
使いまわしていたメンバ変数の定義やクラスの定義文を記述する必要があるため、全体としてはわずかにコード量が増えています。

もちろん、変化は単純なコード量の増加だけではありません。
このクラスを利用した場合の違いを比較してみます。

// before
var circleRepository = new InMemoryCircleRepository();
var userRepository = new InMemoryUserRepository(); // IUserRepository は利用されないが CircleApplicationService をインスタンス化するために用意しなくてはいけない
var circleService = new CircleApplicationService(circleRepository, userRepository); // userRepository は触れられないことがわかっているので null を渡してもいいかもしれない
circleService.CreateCircle("TestCicle");

// after
var circleRepository = new InMemoryCircleRepository();
var circleService = new CircleCreateService(circleRepository); // サークルを作るだけの処理なので userRepository は不要
circleService.Handle("TestCircle");

処理内容としてサークルを作るだけであれば、本来ユーザに纏わるアレコレは不要です。
しかし、分割する前のCircleApplicationServiceではコンストラクタがIUserRepositoryを要求しているため、何かしらのインスタンス(もしくは null )を引き渡す必要があります。

対してクラスを分割した場合には、そもそもコンストラクタで不要なオブジェクトを受け取らないように変更されます。
null を取り扱ったり、使いもしないインスタンスを作らずに済むのはメリットではないでしょうか。

これだけであれば何も考えずとも分割すればよいのですが、大抵の場合メリットにはデメリットが付き物です。
分割した場合のデメリットとして最も気になりそうなのは、やはり処理の関連性を示唆できなくなってしまったことです。

クラスに分割する前のCircleApplicationServiceのときは「サークルに関わる処理」が同じクラスの中にまとまっていました。
それに比べて、クラスを分割したCircleCreateServiceCircleJoinServiceの間には、かろうじて「 Circle という名前が接頭語としてついている」程度の関係性しかありません。
これでは処理を探すときや新しい処理を追加するときに、どこに記述すればよいか迷ってしまいそうです。

名前空間による関係性の示唆

関係した処理をまとめておき、それがまとまっていることを示すのはとても重要です。

そのまとまり方が周知されていれば、プログラマはある程度のアタリをつけて探すことができます。
探すことができるということは仕様の確認なども簡単に行うことができます。
またモジュールの再利用を促すことにも繋がり、結果として重複の排除につながります。

そう考えると関連性を示すことができなくなってしまったというのは許容しがたいデメリットです。
凝集度を高めるために関連した処理を分割し、結果としてモジュールが探せなくなるようでは開発に支障が出るでしょう。

とはいえここで諦めてしまっては話が終わってしまいますので解決策を考えます。
今回のようにクラスを分けた場合は、そのまとまりを示す手段として名前空間を利用するのがよいでしょう。

Application.Circle.CicleCreateService
Application.Circle.CicleJoinService

一般的に名前空間はそのままディレクトリ構造に反映されます。
ソースファイルの配置は次のようになります。

directory_1.JPG

これによりサークルに関係する処理は Circle フォルダにまとめることができます。
開発者はサークルに関わる処理は Circle ディレクトリを、ユーザに関わる処理は User ディレクトリを参照するようになるでしょう。

コントローラへの影響

アプリケーションサービスのメソッドがそれぞれクラスになった結果、最も大きな影響を受けるのはそれを利用する箇所、つまりコントローラです。
まずは分割する前のコントローラをご覧ください。

public class CircleController : Controller {
  private readonly CircleApplicationService circleApplicationService;

  public CircleController(CircleApplicationService circleApplicationService) {
    this.circleApplicationService = circleApplicationService;
  }

  [HttpPost]
  public IActionResult Create(CircleCreateRequestViewModel model) {
    var name = model.Name;

    circleApplicationService.CreateCicle(name);

    return View();
  }

  [HttpPost]
  public IActionResult Join(CircleJoinRequestViewModel model) {
    var circleId = model.CircleId;
    var joinUserId = model.UserId;

    circleApplicationService.Join(circleId, joinUserId);

    return View();
  }
}

コントローラはCircleApplicationServiceを利用しています。
CircleApplicationServiceのメソッド、CreateCircleJoinが使われているので、メソッドごとにクラスに分割してみましょう。

public class CircleController : Controller {
  private readonly CircleCreateService createService;
  private readonly CircleJoinService joinService;

  public CircleController(CircleCreateService createService, CircleJoinService joinService) {
    this.createService = createService;
    this.joinService = joinService;
  }

  [HttpPost]
  public IActionResult Create(CircleCreateRequestViewModel model) {
    var name = model.Name;

    createService.Handle(name);

    return View();
  }

  [HttpPost]
  public IActionResult Join(CircleJoinRequestViewModel model) {
    var circleId = model.CircleId;
    var joinUserId = model.UserId;

    joinService.Handle(circleId, joinUserId);

    return View();
  }
}

コントローラは分割されたクラスをすべてメンバ変数として保持するようになりました。

今度はコントローラの凝集度が下がってしまっているのにお気づきでしょうか。
また、今段階ではアクションが二つしかないので大した問題には見えませんが、今後新たなアクションが追加されるたびにコントローラのメンバ変数が増えるのが想像に難くありません。
将来的に一体どれだけのクラスを保持するようになるのか見当がつきません。

これは問題に思えますので対処したいところです。
この問題に対する方策としてメッセージバスを採用する方法があります。
メッセージバスを採用した場合のコントローラを見てみましょう。

public class CircleController : Controller {
  private readonly MessageBus bus;

  public CircleController(MessageBus bus) {
    this.bus = bus;
  }

  [HttpPost]
  public IActionResult Create(CircleCreateRequestViewModel model) {
    var name = model.Name;
    var request = new CircleCreateRequest(name);

    bus.Handle(request);

    return View();
  }

  [HttpPost]
  public IActionResult Join(CircleJoinRequestViewModel model) {
    var circleId = model.CircleId;
    var joinUserId = model.UserId;

    var request = new CircleJoinRequest(circleId, joinUserId);

    bus.Handle(request);

    return View();
  }
}

コントローラのメソッドではクライアントから受け取ったデータを元にコマンドオブジェクトを生成し、メッセージバスにそれを引き渡します。
メッセージバスにはその背後にある処理の中から、引き渡されたコマンドに適した処理を起動します。

こうして考えると、コマンドを作るということはそれに対応した処理を期待する行為です。
つまりコマンドは実行したいユースケースのシリアライズ化したオブジェクトと捉えることができます。

では、コマンドに対する処理はどのように決まるのかというと、次のように事前に登録しておきます。

var bus = new MessageBus();
bus.Register<CircleCreateRequest, CircleCreateService>();
bus.Register<CircleJoinRequest, CircleJoinService>();

CircleCreateRequest というコマンドのオブジェクトを受け取った場合はCircleCreateServiceに処理を移譲し、CircleJoinRequestの場合はCircleJoinServiceに処理を移譲するという設定を行っています。

コマンドとそれに対応する処理の登録はこういったスクリプトでも構いませんし、ファイルから読み込んで設定するのでも構いません。
※メッセージバスの実際の実装例が気になる方は以下の URL を参照してください(UseCaseBus という名前になっています)
https://github.com/nrslib/ClArc-CSharp

メッセージバスを利用すればコントローラの凝集度も高まり、メンバ変数もユースケースが増えるたびに増えることもなく、とてもよさそうに見えるのですが問題があります。

一番の問題は、コマンドに対して処理をするオブジェクトを登録しておかないと実行時の例外になることでしょう。

コマンドと処理系を作ったはいいけど登録を忘れていた、ということは慣れてくれば慣れてくるほど起きそうな話です。
実際は動作確認を行うでしょうから、ほとんど問題にはならないように思えますが、なるべくなら機械的に解決したい部分であります。

これに対する解決方法には次のようなアプローチが挙げられます。

  • チェックスクリプト
  • スキャフォールディングツール

チェックスクリプトは「定義(及び利用)されているコマンドに対しての処理が登録されていなかった場合はエラーとする」という処理です。
ソフトウェアの実行前イベントにしてエラー時には起動できないようにしたりするとよいでしょう。

スキャフォールディングツールは「簡単な定義を入力するとコマンドやその処理を行うクラス定義を自動で生成するツールを作り、そのとき同時に登録を行う」という方法です。
もちろんスキャフォールディングした後に設定部分を削除してしまえば正常に動作しなくなりますが、通常の開発フローであれば問題なく運用できるでしょう。

モチベーション

フォルダ構成を変更したり、ツールを作ったりと、これだけ大げさなことを行って得られるのは凝集度を高めたという事実だけのみです。なんだか少し物足りなく感じますよね。

もちろん凝集度を高めるということはモジュールの堅牢性や可読性などを高めてくれるので、それがそのまま利点です。
ですので、「凝集度を高めたその事実が素晴らしいことだ」と押し切ることもできなくもないのですが、これだけ大掛かりなことをするのですから、何か後押しが欲しいところです。

凝集度を高める以外に何かモチベーションになりそうなものとして挙げるのであれば、たとえば「迷わなくて済む」というのはどうでしょう。

プログラミングは迷いの連続です。
どう書けばよいのかという迷い。どこに書けばよいのかという迷い。
なるほど迷いというのは大きな障害となりうるでしょう。
逆に迷いを取り除くことができれば、それだけ早く開発を行うことが出来ます。

アプリケーションサービスにおいても迷うことはあります。

たとえば、どこのアプリケーションサービスに所属させるべきか考えたときに、迷う処理が現れることがあります。

次のユーザとサークルのアプリケーションサービスをご覧ください。

public class UserApplicationService {
  private readonly IUserRepository userRepository;

  public UserApplicationService(IUserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public void CreateUser(string userName) {
    var user = new User(userName);
    userRepository.Save(user);
  }

  public User GetUser(string userId) {
    return userRepository.Find(userId);
  }
}
public class CircleApplicationService {
  private readonly ICircleRepository circleRepository;

  public CircleApplicationService(ICircleRepositorycircleRepository) {
    this.circleRepository= circleRepository;
  }

  public void CreateCircle(string circleName) {
    var circle = new Circle(circleName);
    circleRepository.Save(circle);
  }

  public Circle GetCircle(string circleId) {
    return circleRepository.Find(circleId);
  }
}

コードをシンプルにするためここではドメインオブジェクトを公開する方針にしています。
いずれも単純な作成処理と取得処理を持つ単純なオブジェクトです。

さて、プレゼンテーションの要求で「サークルに所属しているユーザを取得する処理」という如何にも必要になりそうな要求があったとしましょう。
その処理はどちらに記述されるべきでしょうか。

まずはユーザに関係する処理ということでUserApplicationServiceに記述してみましょう。

public class UserApplicationService {
  private readonly IUserRepository userRepository;
  private readonly ICircleRepository circleRepository;

  public UserApplicationService(IUserRepository userRepository, ICircleRepository circleRepository) {
    this.userRepository = userRepository;
    this.circleRepository = circleRepository;
  }

  public void CreateUser(string userName) {
    var user = new User(userName);
    userRepository.Save(user);
  }

  public User GetUser(string userId) {
    return userRepository.Find(userId);
  }

  public List<User> GetCircleUsers(string circleId) {
    var circle = circleRepository.Find(circleId);
    if(circle == null) {
      return new List<User>();
    }

    return userRepository.FindUsers(circle.Users); // Circle.Users は UserId のコレクション
  }
}

処理自体に納得感はあります。
しかし、一つのメソッドのためだけにICircleRepositoryを受け取るようになるのは致し方ないとはいえ、若干の気後れを感じるのではないでしょうか。

ではCircleApplicationServiceに実装した場合はどうなるでしょう。

public class CircleApplicationService {
  private readonly ICircleRepository circleRepository;
  private readonly IUserRepository userRepository;

  public CircleApplicationService(ICircleRepositorycircleRepository, IUserRepository userRepository) {
    this.circleRepository= circleRepository;
    this.userRepository = userRepository;
  }

  public void CreateCircle(string circleName) {
    var circle = new Circle(circleName);
    circleRepository.Save(circle);
  }

  public Circle GetCircle(string circleId) {
    return circleRepository.Find(circleId);
  }

  public List<User> GetUsers(string circleId) {
    var circle = circleRepository.Find(circleId);
    if(circle == null) {
      return new List<User>();
    }

    return userRepository.FindUsers(circle.Users);
  }
}

この場合もUserApplicationServiceと同様の問題を抱えています。
サークルのことなのに、ユーザ集約を返却することにより違和感を感じる方もいるかもしれません。

こうなってくると非常に迷いが生まれる部分であると思います。

UserApplicationService に記述するべきかCircleApplicationServiceに記述するべきか、それとも新しく作るのか。

しかし、もしもすべてのユースケースがそれぞれクラスになっているようなシステムであったのならば、何も考えずにクラス化をするでしょう。

public class CircleGetUsersService {
  private readonly ICircleRepository circleRepository;
  private readonly IUserRepository userRepository;

  public CircleGetUsersService(ICircleRepositorycircleRepository, IUserRepository userRepository) {
    this.circleRepository= circleRepository;
    this.userRepository = userRepository;
  }

  public List<User> Handle(CircleGetUsersRequest request) {
    var circle = circleRepository.Find(circleId);
    if(circle == null) {
      return new List<User>();
    }

    return userRepository.FindUsers(circle.Users);
  }
}

このオブジェクト単体では殆ど違和感を感じずに済むのではないでしょうか。

まとめ

凝集度に固執すると今回のように仕掛けが必要になってしまうことがあります。

こういった仕掛けは対象となるシステムの規模が小さい場合には、とても大掛かりに感じることもあるでしょう。
反対にシステムの規模が巨大になっていくと、ユースケース毎にクラスを分割する戦略はその力を発揮してきます。

たとえば本当に小さな変化ですが、処理の検索がしやすくなります。
多くの処理群を内包するシステムでは具体的な処理を見つけ出すのも苦労したりするものです。
そういったシステムにおいて、クラス名で検索できるのは比較的検索しやすい部類になります。
もちろんメソッド名であっても検索することができるのですが、コードに対する検索ではメソッド名よりもクラス名が優先されて表示される IDE が多いように思います(もしもそうでなかったらごめんなさい)。

それ以外にも、今回の例で言えばテストをする際には利用しないリポジトリなどの余計なオブジェクトを作成する必要がなくなりました。
モックを利用してロジックのテストする際に、準備しやすいのはメリットです。

他にも処理毎にクラスが分かれているので、特定の処理だけにスタブを刺し込むというのが容易くなるでしょう。
メインのコードを変更せずともそれが行えると最高です。

また、そもそも凝集度が高まるということ自体がメリットに感じます。
凝集度を高めることは堅牢性、信頼性、再利用性、可読性の向上に繋がります。
オブジェクトの責務の量とその取扱いに必要とされる慎重さは比例するので、高い凝集度のモジュールはその責務が必然的に少なくなり、容易に扱えるようになるのです。

凝集度はあくまで指標です。
その値が最高でなくとも多くは問題が起きません。
場合によっては少し凝集度が低い方が最適であることもあります。

凝集度は今以上に高めた方がよいのか。
高めた場合に弊害は発生するか。
その弊害に対する対抗手段はあるか。
対抗手段を講じた際の影響範囲(開発者への負担も含む)はどのようになるか。

これらを考察し、そのメリットとデメリットを双方をふまえて享受すべきと考えたのであれば、その戦術を採用すべきでしょう。

悪いコードを断罪するための武器としてではなく、よりよいコードを目指すための手がかりとして、凝集度が活用されることを期待します。

おまけ

実はこれを前面に押し出して実装したのが 実践クリーンアーキテクチャ です。
もしこの記事を読んで、ご興味沸いたようでしたらご参照ください。

DDDで学ぶAPI設計の勘所

External article

DDDとコードとしての正しさ

External article

DDDをやって良かったと思ったこと

この記事は、 ドメイン駆動設計 #1 Advent Calendar 2018 の15日目です。

完全に主観ですが、ドメイン駆動設計をプロダクトの設計手法として取り入れたうえで、実際に開発プロジェクトを推進していくなかで感じた、「あ、これ良いな!!」「これが恩恵だな!!」って感想を持ったあたりを、いくつかピックアップして書きたいと思います。

もし、DDDをこれから導入されようと検討されている方の少しでも参考になれば…

コンテキストマップで全体像が把握できる・説明できる

DDDの文脈で、戦略的設計と戦術的設計があり、コンテキストマップは戦略的設計として挙げられている。

コンテキストマップを実際に描いたことがあり、かつ、それをプロジェクトに適用したり、システムアーキテクチャの定義に加えてプロジェクトを運用した経験のある方ってどれだけいらっしゃるのかな…。

私の経験では、1回(といってもそのプロジェクトは3年以上続くプロジェクトでしたが…)でしたが、そのプロジェクトの中で、常に人が流動的に入ったり抜けたりしていました。

そういった新しく入ってこられる方に、このシステムは、何です!!って説明するのに、コンテキストマップがとても役に立ちました。

まさしく、システム地図だなーと感じました。

そのときのコンテキストマップは、概念レベルでモデル化しており、ユビキタス言語としてモデルに名付けをしていて、関連の誘導可能性と、多重度のみで構成していました。

主に、以下のシーンで活躍しました。

  • 新しくメンバーが入ったときには、コンテキストマップを出して、このシステムはこういったものですといった説明をしたり…
  • 要件が追加されたときに、その概念を扱うコンテキストがどこになるのかといったことを、プロダクトオーナーを混じえて話したり…
  • その要件に出てくるその「言葉」はどのコンテキストにおけるユビキタス言語なのか、新なコンテキストとして言っているのかと、常にコンテキストマップを地図にして、コミュニケーションの中で出てくる「言葉」の位置付けについて、確認していくことができたり…
  • ステークホルダーにどういったシステムを開発しているのか説明するのに使ったり…
  • インターンシップに来た学生さんに説明したり…
  • 会社見学に来社いただいた方にどういったシステムを開発しているのか説明したり…

といった、本当に多様なシーンにおいて、「ソフトウェアシステム」という実体があるようで、見えないモノについて、説明するのに役立ちました。

規模の大小関わらず、コンテキストマップを描いてシステム地図として使うのは、とても便利だと思いました。

その他にもきっと、レガシーシステムのリプレースをするときの最初の分析としてコンテキストマップで表現するとかに、かなり有効だと思います。
現行のシステムを表現していくと、きっと、その中でこの関連とかどうなってるの?とか、ここ関連させてたらおかしいよね…とか、そういった気付きも産まれてきて、整理したくなると思います。
そこで整理してあるべきコンテキストマップと、今はこうするしかないよねってあたりと、現状と、その3点のシステムのあるべきコンテキストマップが描ければ、リプレースが現実的かつ、将来的な姿を、その場に関係する人たちの共通認識の形として表現できると思います。

このあたりは、今後、トライしていきたいなと思う部分です。

共通認識のアーキテクチャがあるから迷わない

DDDだからというわけではないのかなーと思いますが、とはいえ、DDDを学ぶと必ずアーキテクチャについて学ぶ機会があります。

レイヤー化アーキテクチャ、ヘキサゴナルアーキテクチャ、Clean Architectureといった、アーキテクチャについて、読んだり構想したり、実際に組んでみたり…

これらのアーキテチャについて、例えば、基本的なレイヤー化アーキテクチャを学ぶこととかを通して、ドメインを隔離することを目的としたテクニックを学びます。

その過程をある程度、経たエンジニアの方にとって、「こちらは、DDDで作られたソフトウェアです」と紹介されれば、多少個人差はあれど、全体的な処理の流れを把握するのに、そこまで苦労はしません。

中にはもちろん、あらゆる工夫を凝らした部分もあるかと思いますが、ドメインの隔離と、そのコードを理解することにおいては、シンプルに構成されているはずで、ドメイン貧血症になっているかもしれませんが、その意図を理解することは、それらのアーキテクチャで構成されていないモノと比較すると、それなりに容易に理解できるかと思います。

もちろん、私が出会ったことが少ないだけで、なんちゃってアーキテクチャでやたらと本質からズレたコードもあるのかもしれませんし、逆にカオスを生み出している小宇宙のようなコードもあるのかも…

ですが、向かう方角は指針は共通認識であり、ドメインを如何に隔離して、依存を排除するのかといったあたりですから、そこから大きく外れることはないと思います。

ドメインを躊躇せずに変更する

どこかで、「ドメインは、全ての中心に位置するので変更すると影響範囲が大きくなるから、できるだけしたくない」といったお話を聞いたり、はじめて取り組んだプロジェクトでは、そう思っていたところが私もありました。

それは確かにそうで、私が、そう思ったときは、PythonのDjangoでレイヤー化アーキテクチャでWebアプリをVIMとかで作っていたときでした。

実際、ドメインを躊躇せずに、どんどん変更していける状態っていうのを作ろうと思うと、IDEやコンパイラの力がとても重要で、Unitテストを書き続けて、リファクタリングをどんどん躊躇せずにできる状態を保てていることが前提です。

ただ、その状態が保てているのであれば、どんどんと躊躇せずがっしがし変更していくべきだと考えます。

ドメインは、そのときの要求を実現するのに注力するべきで、将来を見越して、こうなるかもといった曖昧で無駄にリッチな構造を作っておくと、結果的に進化することなく負債となることも考えられます。

また、ドメイン駆動なので、何か要求を実現するときも、ユースケースとドメインモデルを動的、静的な観点からラフに描き実現し、動かし検証しながら進めていき、要求に変更が出たときも、それをラフにどんどん変更しながら、ユースケースとテストを書きながらドメインに反映していきます。

すると、その他のドメインに依存している層にその影響が染み出していきます。

その染み出した影響は、コンパイラーやユニットテストが検出してくれて、その検出内容をつぶさにチェックしながら、1つずつ丁寧に対応していくことで、自然と要求の変更が仕組みとして組込まれ、なんの問題もなく、安全に全体に染み出すことが可能です。

そういったことを実現していくのには、私の場合、Scalaがやはりシックリきています。

強力なコンパイラー、静的型付け言語、オブジェクト指向、依存性を排除したり、依存の方向性を強制したり、IntelliJ IDEAのリファクタリング機能などなど…

このあたりを相性よく強制化し安全かつ簡単に実施していけるノリがあって、そのあたりが気に入っています。

やはり、効率良く安定して高度なレベルで継続して取り組んでいくには、思想だけでなく周辺のテクニカルな部分をきちんと抑えていくのは必然となるのだなーと思います。

まとめ

私が過去、DDDを実践したときに感じた、「あ、これDDDやってるから得られるメリットじゃね!?」という感覚を言語化してみました。

ざっくりと思いつくままにまとめましたので、あまり体系立ててまとめられておりませんし、語りに過不足あるかと思いますが、こういった観点もあるよねとか追加していければと思います。

以上です。

ドメイン駆動設計アンチパターン「利口なUI」

この記事は、ドメイン駆動設計 Advent Calendar #1 の16日目の記事です。

Eric Evansのドメイン駆動設計(以後Evans本)第2部の第4章で触れられている
「利口なUI」についてまとめています。
なぜこれをピックアップしたかと言うと、アンチパターンから入ったことで、
ドメイン駆動設計というものがだいぶしっくりくるようになったためです。

利口なUI

利口なUIは、ユーザーインターフェイスにビジネスロジックが入っているパターンで
Smart UIとも呼ばれており、Evans本には唯一アンチパターンという言葉が添えられて紹介されています(もちろん他にもアンチパターンはいくらでもあります)。
利口なUIと言う呼ばれ方がされているのは、UI層が責務を持ちすぎている意味が込められています。
Eric Evansは「アンチパターンと紹介することに対して皮肉を効かせている。」
と言っていますが、この言葉自体がかなり皮肉染みていると思います。
ここでのUIViewやテンプレートエンジン、フロントエンドやスマホアプリなどで、
この外部アクターとしては、人だけではなく該当するインターフェースを利用した他のサービスだったりすることもあります。

例.勤怠ソフトの場合

勤怠ソフトで利口なUIの極端な例を見ていきます。

とある勤怠ソフト

以下のような要件のソフトがあったとします。

- 従業員が1日の勤怠を入力すると、勤怠をフロントエンドにてリアルタイムでひと月の合計時間を計算、表示する。
- 従業員の操作により保存ボタンを押してサーバーサイドに勤怠データを送り、保存する。

利口なUIではデータベースはただの共有リポジトリ

以下の図の通り、ビジネスロジックはUIで行われます。
Controllerと呼んで良いのか微妙なところですが、
Controllerがデータの保存などを担う1レイヤーなシステムです。
この時、Controllerはただのデータの架け橋で、データベースは画面間のただの共有リポジトリになります。
もしこれが複雑な画面ではなく単純な画面のシステムの場合、
ひたすらControllerUIを量産して、それぞれの画面の担当者がデータベースに正しく値が入るように実装していくケースでは、もしかしたら問題なくプロジェクトを達成できるかもしれません。

Untitled New Diagram   Cacoo (10).png

ロジックの複製

ここで新しく給与画面を追加したとします。
勤怠と給与の世界はやっかいです。
給与計算になると1週間ごとの勤怠が登場します。
(1週間40時間もしくは44時間を超える場合は、時間外労働の割増計算が必要になるため。)

勤怠画面はひと月の勤怠実績の集計を確認したいだけなので、1週間ごとの勤怠は勤怠画面に必要ないとします。
その場合、給与画面で1週間ごとの勤怠を集計するために再び勤怠の集計が必要になってきてしまいます。

Untitled New Diagram   Cacoo (11).png

勤怠画面と給与画面をつなぐ共有できる勤怠集計のロジックはなく、データベース上の1日ごとの勤怠レコードのみが2つの画面をつないでいます。

スケールに限界が出てくる

サービスが拡大し、「スマホアプリ版を作りましょう。」となることは珍しくありません。
もともとそんな想定じゃなくても、ユーザーの要望や市場、ビジネスモデルの変化などでサービスは常にスケールするしコアドメイン自体も勤怠から給与に変わるかもしれません。
勤怠や給与の場合は法律・制度だって変わっていきます。

スマホアプリ版を作ることになったとします。
この場合、スマホアプリにほとんど同じロジックを複製しなければなりません。
スマホアプリでもiOSAndroidでさらにロジックの複製が増えるかもしれません。

Untitled New Diagram   Cacoo (12).png

同じようなロジックを各クライアントやview、テンプレートに作り始めプロダクトをイテレートしていくには限界が出てきます。

「利口なUI」のメリット、デメリットについて

Evans本からメリット・デメリットを抜粋させていただきます。
Eric Evans. エリック・エヴァンスのドメイン駆動設計. 翔泳社.)

メリット

  • 単純なアプリケーションの場合、生産性が高く、すぐに作れる。
  • それほど有能でない開発者でも、この方法なら、ほとんど訓練しないで仕事ができる。
  • 要求分析が不足していても、プロトタイプをユーザに公開し、その要望を満たすように製品を変更することで、問題を克服できる。
  • アプリケーションが互いに分離しているので、小さなモジュールの納品スケジュールは比較的正確に計画できる。
  • 単純なふるまいをつけ加えるようなシステムの拡張であれば、容易に対応できるだろう。
  • 関係データベースはうまく機能し、データレベルでの統合が実現される。
  • 4GLツールが実にうまく機能する。
  • アプリケーションが引き継がれた場合、保守プログラマは自分が理解できない部分を素早く作り替えられる。変更による影響が、それぞれ特定のユーザインタフェースに限定されるからだ。

解釈次第ですが、メリットを見ていても正直「これ、本当にメリットか?」というものばかりです。
まとめてみるとデータベース自体はただの共有リポジトリでしかないため、ロジックを変更しても他の画面に影響することはないので開発自体は横展開しやすい。
生産性が高く、技術者も低レベルでOK、要求分析不足しても大丈夫。
画面数が多く単純なアプリケーションで、ウォータフォールかつ上流工程から下流工程まで階層的に分かれている開発体制に向いているかもしれません。
プロダクトのイテレートが無いことを願うしかありません。

デメリット

  • アプリケーションの統合は困難で、データベースを経由させるしかない。
  • ふるまいが再利用されることも、ビジネスの問題が抽象化されることもない。ビジネスルールは、適用先の操作それぞれで複製されることになる。
  • 迅速なプロトタイピングやイテレーションを行おうとしても、自然と限界に行き当たる。抽象化が欠けているために、リファクタリングの選択肢が制限されるからだ。
  • 複雑さによってすぐに覆い尽くされてしまうので、成長しようとしても、単純なアプリケーションを追加することしかできない。
  • より豊かなふるまいが実現できるようになるといった、優雅な道は存在しない。

データベースが唯一の共有リポジトリなので画面間つなぐのはデータベースになってしまうため、ロジックなどの共有はできず、イテレーションがし辛い。
どう見ても冗長的です。

DDDでの解決手段

レイヤーを増やすことによりドメインを隔離する

Evans本に紹介されている4レイヤーアーキテクチャーや最近話題のクリーンアーキテクチャー、増田さんがよく紹介されている3レイヤー+ドメインモデルアーキテクチャーのように層を増やすことでEvans本第4章本題のドメインを隔離する話につながってきます。

3レイヤー+ドメインモデルアーキテクチャーの場合

ドメイン層を切り離し、ドメイン知識を集約していくことで高凝集・低結合になりました。
責務がそれぞれ明確になり、リファクタリングのポイントも見つけやすくなります。
他の4レイヤーアーキテクチャーやクリーンアーキテクチャーでもそれぞれ特性はあるものの、同じようなことが言えるかと思います。

Untitled New Diagram   Cacoo (13).png

その他の解決手段

トランザクションスクリプト

UIをアプリケーションから切り離すことでUIが行っていた一連の流れをアプリケーション側で手続き的に処理します。
ただ、オブジェクトモデルは持たないので、いろいろな箇所にビジネスロジックが散らばってしまったり、Fat ControllerFat Modelが誕生したりサービスが全てを振る舞うようになってしまいドメインモデル貧血症につながってきます。

Universal Javascript

UIでもプレゼンテーション層とドメイン層の2層に分け、フロントエンドとサーバーサイドでドメイン層を共有できればうまくいく解決するかもしれません。
現状だとnode環境が必要になってくるので、動作環境に依存してしまっている感じはあります。

最後に

今回は極端な例でしたが、ifの判断条件やループのリスト取得にビジネスロジックがテンプレートエンジンに入ってくることはある程度成熟したプロダクトでもよく見かけます。

Evans本では利口なUIを選択するかMDDを選択するかという感じで書かれていますが、大抵の場合はアプリケーションの負債としてすでに利口なUIが存在している場合がほとんどだと思います。
一部のスタンドアローンな業務アプリケーションに適用はできるかもしてませんが、イマドキのWebアプリケーションやスマホアプリの時代にはあまり適している場面は多くないはずです。

UIに責務が寄り過ぎてしまっていると言うだけで、これはUIに限った話ではありません。
ちょうどアドベントカレンダーで@mtoyoshiさんが書いていただいた記事(ドメインモデルにView固有の事情が入ってくることの対策)は今回とは逆のViewの責務がドメインモデルに寄ってしまう問題でした。
結局はどこにどの責務をもたせるべきか、どのようにすれば高凝集、低結合な状態を作り出せるかということが重要なんだと思います。
こうした怪しいパターン身につけることで問題や負債に対する嗅覚を上げていきたいです。

次回のドメイン駆動設計 Advent Calendar #1@k-okinaさんです。

ドメインモデルをモデリングする際に役立つルールや原則

はじめに

ドメインモデルをモデリングする上で、これってこのドメインに入れればいいんだっけ?このドメインってこれでいいんだっけ。あいつどのドメインにいるんだっけ?ってなると思います。
そこで自分用にドメインモデリングする際に必要な思考を纏めて、言語化してみます。

また筆者自身は現在DDDを学習中の身であり、まだまだ未熟ですのでもし内容が間違っている、補足、追記がある場合はコメント又は編集リクエストを頂けたらと思います!

ドメインとは

実践DDDの9章でコード上でドメインと呼ぶか、ドメインモデルと呼ぶかの考察の部分から抜粋

ドメインは、今取り組んでいる業務のノウハウの一面をとらえたものだ。私達が設計・実装するのは、ドメイン(業務)ではなく、ドメインのモデル(業務の仕組み、構造)である。つまり、結論としては、モデルをとりまとめるコンポーネント名(※言い換えてます)としては、ドメインと呼ぶよりドメインモデルのほうが適している。しかし、最終的に何と呼ぶかは、チームで決めることだ。

これはこうとも言いかえれます。

ドメインは、今取り組んでいる業務のノウハウの一面をとらえたものだ。私達が設計・実装するのは、業務ではなく、業務の仕組み、構造である。つまり、結論としては、モデルをとりまとめるコンポーネント名(※言い換えてます)としては、ドメインと呼ぶよりドメインモデルのほうが適している。しかし、最終的に何と呼ぶかは、チームで決めることだ。

まとめると、俺たちが議論してるのは特定ドメイン内のモデルの話だから、それはドメインモデルって名前で表現した方が正しいんじゃない?まぁ最終的にはチーム全体の意思が一番大切だから気に入らなかったら好きにしてくれ。

補足

業務とは、必ずしもソフトウェア開発者だけで考えるものではない。受託開発の場合はクライアントの場合だったり、また、会社の誰かがこんなのあったらもっと良いんじゃない?と新たな業務を提案する場合があります。そこから色々と議論がなされ、じゃあこうしよう!と最終的に新たな業務が誕生します。その業務が、ドメインです。そしてその業務の仕組みがドメインモデルです。
我々はその業務の仕組みをコードにそのまま反映させるだけです。(ユキビタス言語についてはあえてここでは紹介しない
反映させる時は永続化装置はどうするのかとか、そういった話はでません。そのようなインフラとかと繋がる部分は全てインターフェイスで表現することにより、余計な思考を頭から追い出し、より正確に業務内容・構造をコードで表現することができます。

1ドメインモデル、1リポジトリ・集約以下の原則

一応実践DDDの表9-1には、以下のように助言されている

モジュールは、モデリングの概念にフィットするように設計すべし

その理由
通常は、ひとつあるいはごく少数の凝集した集約ごとにひとるのモジュールを用意する。

しかし、複数の集約を1つのドメインで取り扱うとCCPとCRPに違反しがちになる。本当に違反しない凄い繊細な設計でも、メンテ大変そう。
なので、複数の集約を単一ドメインで持つのはやめとういたほうがいんじゃないかな。

ここから下の内容について

クリーンアーキテクチャのコンポーネントの原則に関する内容を纏めます。
このコンポーネントの原則は、DDDのドメインモデルをソフトウェアとして構築するのに役立ちます。

また、クリーンアーキテクチャのコンポーネントの原則のコンポーネントって何?どのコンポーネントの事?って思ってましたが、恐らくソフトウェアコンポーネントの事を指しているでしょう。コンポーネント指向で登場するコンポーネントと特徴が類似しています。

この前提知識の上に読み進めて頂けたらと思います。

閉鎖性共通の原則 (CCP)

これは単一責任の原則 (SRP)をコンポーネント向けに言い変えたものです。

クリーンアーキテクチャ本13章から抜粋

同じ理由、同じタイミングで変更されるクラスをコンポーネントにまとめること。変更の理由やタイミングが異なるクラスは、別のコンポーネントに分けること。

もう1つ抜粋

コンポーネントを変更する理由が複数あるべきだはない

あと1つ抜粋

同じタイミングで変更されることが多いクラスはひとつにまとめておけということだ。2つのクラスが物理的あるいは概念的に密結合していて、変更のタイニングがいつも一緒になるのであれば、それは同じコンポーネントに属するものだ。まとめておけば、ソフトウェアのリリースやデプロイの際の作業量を最小限に抑えられる。

解説

同じこと言いますが、1つのドメインモデルが変更される理由は複数あるべきではないって事ですね。
普通に このドメインモデル内に含まれるこのモジュールとこのモジュールって変更の理由全然違くない?って気づいたら、そのモジュールはもしかしたら他のドメインモデルのモジュールなのかもしれませんね。
また、本当は同じドメインモデルに入れるやつなんじゃないの?ってなったりする可能性もあるので、しっかり考察した上で最終判断をしたい所。

Aの業務に対して変更があったらAの業務を反映しているコードを修正するだけに留まらず、他のコンポーネントに属しているBを修正する必要がでた。
また、Bが表している業務に対して変更があった場合もAに影響があった場合、実はこの2つのコンポーネントって、同じコンポーネントのなんじゃない?だっていつも変更のタイミング同じだし、実は同じ業務だったけど、モデリングミスったんじゃない?って再考するタイミングだよねってこと。又は、単純にAコンポーネント内に配置していたとあるモジュールは実はBコンポーネントのものだったって事もありそうですね。

全再利用の原則 (CRP)

クリーンアーキテクチャ本13章から抜粋

コンポーネントのユーザーに対して、実際には使わないものへの依存を強要してはいけない。

もう1つ

全再利用の原則(CRP)ひとつのコンポーネントにまとめるべきクラスやモジュールを判断するための原則である。

あと1つ

わかりやすい例として、コンテナクラスとそれに対応するイテレータを考えてみよう。これらはまとめて再利用すべきものだ。お互いに密結合しているからである。したがって、これらは同じコンポーネントにまとめておくべきだ。

補足

この原則は、どのモジュール等を同じコンポーネントにまとめるかを教えてくれるが、それと同時にどのコンポーネントを同じコンポーネントにまとめてはいけないかを教えてくれてる。
まぁ、コンテナとイテレータの場合は、それらを使うAは別コンポーネントだよねとか。なぜなら、他のモジュールもコンテナとイテレータを使いたい場合、Aへの依存を強要してしまうからである。

再利用・リリース等価の原則 (REP)

クリーンアーキテクチャ本13章から一部抜粋

コンポーネントには一貫するテーマや目的があり、それを共有するモジュールを集めなければいけない。

解説

このテーマや目的がドメインモデル、業務を表現する、ですね。
その下にそのテーマや目的を共有する、リポジトリやファクトリ、エンティティ、etc...モジュール群がある感じです。
また、このコンポーネントはそれ単体でビルドが通せるかどうかも重要です。

最後に

本当はもっと沢山、色々と書きたかったんですが、それは自信もって "DDDちょっとできる" レベルになってからにしようと思います!

Whyから始めるドメイン駆動設計

この記事は、ドメイン駆動設計 Advent Calendar #1 の18日目の記事です。

ドメイン駆動設計をどうやって実現していくかについては、既にたくさんの素晴らしい記事があります。
しかし、ドメイン駆動設計をなぜやるのかについて考察した記事はそれほど多くないように思いました。
この記事では、なぜドメイン駆動設計が大事なのかについて考察することで、ドメイン駆動設計の勘所を追究していきます。
自分なりに噛み砕いたあとの文章なので、もし変な所があれば指摘頂けると大変助かります。

DDDを駆動している原則

エリック・エヴァンスのドメイン駆動設計では、DDDを駆動している原則として以下の3つが挙げられています。

  • コアドメインに集中すること
  • ドメインの実践者とソフトウェアの実践者による創造的な共同作業を通じて、モデルを探求すること
  • 明示的な境界づけられたコンテキストの内部で、ユビキタス言語を語ること

なぜこれらが大事なのでしょうか?
まず、これらの原則を全く無視した場合にどのような問題が発生するのかについて考えてみます。

ソフトウェアを開発していく中で発生する問題

的はずれなソフトウェアを作ってしまう

ソフトウェアの核心は、ドメイン(ソフトウェアの問題領域)に関係した問題を、ユーザのために解決できることです。

開発者は、複雑なドメインの問題を解決するために、ドメインに没頭しなければなりません。
しかし、大抵のソフトウェアプロジェクトでは、ドメインの問題を解決することが重要視されません。

技術指向の開発者は、自分の専門スキルを発揮できるパズルのような問題について楽しむものです。
ドメインの問題について考えるのは、自分のコンピュータ科学についての能力を向上させることと関係ないように見えてしまうのです。

この結果、ドメインに関係した問題は解決されず、的はずれなソフトウェアを作ってしまうことになる可能性があります。


エリック・エヴァンスのドメイン駆動設計では、あるコメディ番組の撮影中に発生した出来事を例にこの問題について説明しています。
要約すると以下のような内容になります。

  • あるコメディアンが、コメディ番組のシーンを繰り返し撮影し、苦労の末におもしろいものを撮ることが出来た。
  • しかし、映像編集者は全然面白くないシーンを採用した。
    • 映像編集者は、番組に関係ない人物が写り込んでいたのでこのシーンを不採用にした。
    • 映像編集者は、自分の専門的な仕事を正確に遂行することしか考えていなかった。
  • その後、コメディのなんたるかを理解する監督によって面白いシーンは復活した。

ソフトウェアが複雑になりすぎて手に負えない

多くの事柄が原因で、プロジェクトは正しい道から逸れてしまいます。
ソフトウェアが手に負えないほど複雑になると、もはや開発者はソフトウェアを十分には理解できなくなります。
何が原因でソフトウェアは複雑になっていくのでしょうか。

多くのソフトウェア(ビジネスに関係する判定・条件分岐が含まれるもの)において、最も重要な複雑さはドメインそのもの、つまりユーザの活動やビジネスです。
その複雑なドメインそのものを体系的に扱う方法が無いと、ソフトウェアはドメインの複雑さに引っ張られ全体が複雑化し、入り組んだ構造になっていきます。

ソフトウェアについての知識が失われていく

チームで開発を続けていくと、既存のロジックと矛盾したコードが生まれることがあるかもしれません。
このようなことが起こらないよう、チームメンバーと日々コミュニケーションを取り続けているはずなのになぜこのようなことが起こるのでしょうか。
このような問題が発生するとき、そのチームは知識の積み重ねに失敗しているか、知識の共有が不十分な可能性があります。
ある特定のメンバーしか把握していない知識があったり、普段メンバー同士で話している会話の内容と結びついていないような設計を行っているのかもしれません。

ソフトウェアに関する法則の中に、コンウェイの法則というものがあります。

システムを設計する組織は、その構造をそっくりまねた構造の設計を生み出してしまう

同じチームであっても、コミュニケーションが不十分なため、ドメインエキスパートと開発者の間、開発者と開発者の間に知識の差が生まれることがあります。
これが原因で、ある開発者のコードの中に重要な知識が閉じ込められ、重要な知識を共有することが考えられていないような設計が生まれてしまいます。
重要な知識は、一部のメンバーだけが理解する実装の中で死んでいくのです。

時間が限られている中で、何を優先すべきなのかが分からない

設計が大事なのは分かっています。設計に気を配らなければ、降り注ぐ要件追加の要求の中で、とても保守していけるとは思えないようなコードが増殖していくことは目に見えています。
しかし、設計にかけられる時間は限られています。
私達はどの部分の設計に時間を使うべきで、どの部分の設計をあきらめればいいのでしょうか?
取り組むべき問題の優先順位が決まっていなければ、貴重な工数をドブに捨てることになるかもしれません。

問題を解決するために役に立つDDDの原則

的はずれなソフトウェアを作ってしまう

この問題を解決するために役に立つ原則は以下のとおりです。

  • ドメインの実践者とソフトウェアの実践者による創造的な共同作業を通じて、モデルを探求すること

DDDではドメインに関係した問題をユーザのために解決するために、ドメインに関する膨大な知識を噛み砕き、ドメインについての深い洞察と、集中すべき主要な概念を反映したモデルを構築することに力を注ぎます。
この作業は、ドメインに精通したドメインエキスパートと開発者との間で、密にコミュニケーションを取りあいながら進めます。
作業を通して、チームはドメインの中心に何があるのか、何が本当に大事なのかを理解することができ、進むべき道を見失った場合でもプロジェクトを元の進路に戻すことができるようになります。

ソフトウェアについての知識が失われていく

この問題を解決するために役に立つ原則は以下のとおりです。

  • 明示的な境界づけられたコンテキストの内部で、ユビキタス言語を語ること

ソフトウェアについての知識が失われていくような状況にある場合、チーム内のコミュニケーションに問題がある可能性があります。
コード内に含まれる言葉や、書き溜めているドキュメントが実際に利用できる形になっていないため、口頭での伝承が何らかの形で途切れた瞬間、ソースコードは秘伝のタレと化していくのです。
高度に生産的なチームは、継続的に学習することで自分たちの知識をチーム内に生きた形で育て続けます。

DDDでは、知識を生きた形で育て続けるために、共有されたチームの言語であるユビキタス言語を活用します。
ユビキタスとは、いつでもどこにでも、といった意味の言葉です。
境界づけられたコンテキストとは、ある概念や言葉の集合が、まったく同じ意味のままで通じる範囲のことです。
ユビキタス言語はその名の通り、境界づけられたコンテキストの内部の、普段の会話中やソースコード、設計、ありとあらゆるコミュニケーションの中で使います。開発者、デザイナー、ドメインエキスパートなど関係なくチームの中心にある言語として育てていくものです。

曖昧さの無い知識豊富な言語を普段の会話・設計・実装のすべてで使い、チームの中心に持ってくることができれば、知識をチームの中に生かし続けることができるようになるでしょう。

情報工学は知識を扱う学問、知識を明確に表現する生きた言葉は大事です。

ソフトウェアが複雑になりすぎて手に負えない

この問題を解決するために役に立つ原則は以下のとおりです。

  • ドメインの実践者とソフトウェアの実践者による創造的な共同作業を通じて、モデルを探求すること
  • 明示的な境界づけられたコンテキストの内部で、ユビキタス言語を語ること

先の説明で、多くのソフトウェアで、最も重要な複雑さはドメインそのものと説明しました。
ドメインそのものの複雑さを解消することができれば、ソフトウェア全体の複雑さはかなり軽減することができるでしょう。
しかし、この問題をコストをかけずに解決する方法はありません。複雑さから逃げず、絶えずドメインを学習し続け、ドメインエキスパートとともにモデルを洗練させていかなければならないのです。

エリック・エヴァンスのドメイン駆動設計では、ドメインの複雑さと戦う様々なテクニックが紹介されています。
今回はこれらの中から一部を紹介します。

ドメインエキスパートと協力しモデルを探求する

ドメインそのものの複雑さに向き合わなければ、ソフトウェアはドメインの知識を表現しなくなり、一見して分かりづらい複雑な構造になります。
ドメインエキスパートと協力し、ユビキタス言語を用いてモデルを探求し続けることで、ドメインはよりシンプルに表現できるようになっていきます。
モデルが現状にそぐわないものになっていくことも多々あるはずですが、そこでモデルの探求をやめてはいけません。
複雑さに立ち向かい継続してモデルを更新し、ユビキタス言語を洗練させていきましょう。
こうすることで、ドメインの複雑さをコントロールすることができるようになっていきます。

モデルと実装を結びつける

せっかく作ったモデルでも、実際にコード内で使われないのであれば、いずれ捨て去られます。
もしモデルと実装が結びついておらず実質使われていないなら、そのモデルにほとんど価値は無く、ソフトウェアが正確であるかどうかも疑わしくなってきます。
逆にモデルと実装が結びついているなら、次のような良いことがあります。

  • モデルの探求で得た知識と、実装を通して得た知識が相互にフィードバックし合う、フィードバックループが生まれる
  • モデルの探求と実装の垣根がなくなり、普段の会話がそのまま実装に、普段の実装がそのままモデルの探求になる

モデルと実装が結びついているなら、モデルの実装の相談をしたいときにエディタを開いてコードを見せなくても、チャット上の日本語の会話だけでほとんどの相談ができるようになるはずです。

ドメインを隔離する

ドメインの知識がソースコードに散らばっている場合、ソースコードを見た時ひと目でドメインの知識の構造を理解できなくなります。
思考を最適に働かせるには、モデルを表現したオブジェクトであるドメインオブジェクトは、システムの他の要素から切り離す必要があります。
こうすることで、ドメインの概念をソフトウェアの詳細な技術にしか関係しない概念と混同したり、システム全体の中に紛れ込んでドメインを見失うことを避けられるようになります。

時間が限られている中で、何を優先すべきなのかが分からない

この問題を解決するために役に立つ原則は以下のとおりです。

  • コアドメインに集中すること

優先順位を付けて中心となる問題に集中し、些末な問題の海で溺れずにいるためにはどうしたらよいでしょう?
ドメインエキスパートと協力しモデルを探求するたび、どの知識について優先して取り組むべきかが見えてくることがあります。
まるでアルコールを蒸留するかのように、自分たちのソフトウェアを特徴付け価値のあるものにしていきたいソフトウェアの本質部分、その部分には入らないがソフトウェアを構成する上で重要な部分、が見えてくるはずです。

自分たちのソフトウェアの中で、一番の強みとなる価値ある部分、これがコアドメインです。
蒸留によりコアドメインを抽出することの利点としては、例えば以下があります。

  • モデルで最も価値がある領域が明確になり、その部分の作業に集中できるようになる
  • モデルの領域に優先順位が付き、リファクタリングの際の指針となる

現実は厳しいもので、ソフトウェアのすべての部分を等しく改良していくことは出来ません。
コアドメインを抽出することで、時間がない中でも何に集中すべきか、どこを改良するのが一番効果的かが分かるようになります。

ドメイン駆動チーム

これまでの説明を振り返ると、DDDがコミュニケーションに重きを置いているのが伝わってきます。
最後に、エリック・エヴァンスのドメイン駆動設計のまえがきに書かれている文章を紹介します。

ドメイン駆動設計を理解している個々の開発者は、価値のある設計テクニックと全体を見る視点を本書から得るだろう。しかし、最大の収穫が得られるのは、チームが一致団結してドメイン駆動設計アプローチを適用し、ドメインモデルをプロジェクトで交わされる会話の中心に持ってきたときである。
そうすることで、チームメンバは言語を共有し、コミュニケーションを豊かにして、コミュニケーションがソフトウェアとつながった状態を保てるようになる。
(中略)
ドメイン駆動設計は困難な技術的課題だが、大抵のソフトウェアプロジェクトがまさにレガシーへと風化し始めるときに、チャンスを大きく広げることができるものなのだ。

さいごに

なぜドメイン駆動設計が重要なのかについて説明してきましたが、ここまで読んだところでまだまだ分からないことがたくさんあると思います。

  • モデルはどうやって見つければいいのか
  • ユビキタス言語はどうやって作っていけばいいのか
  • モデルを実装と結びつけるとは、具体的にどんなコードになるのか
  • ドメインを隔離するとは、実際にはどうやればいいのか
  • 今あるソフトウェアが結構ひどい状態、この状態からでもDDDはできるのか
  • そもそもDDDをやるべきなのか

ドメイン駆動設計は、完璧にやろうと思うと膨大な時間がかかります。
筆者の考えとしては、最初から全部理解しようとせずに、チーム内の協力してくれそうなメンバーを誘って分かるところから色々試行錯誤してみる、というのがいいのかなと思います。
ちょこちょこ勉強を続けていきましょう。
具体的な実装のテクニックをまず知りたい場合には、以下のスライドがおすすめです。

ボトムアップドメイン駆動設計 (@nrslibさんの記事です)
https://nrslib.com/bottomup-ddd/
スライド
https://www.slideshare.net/MasanobuNaruse/bottomup-ddd-1
https://www.slideshare.net/MasanobuNaruse/bottomup-ddd-2

また、DDDのコミュニティもあるので、こちらで色々質問してみるのもいいかもしれません。

以上ですm(_ _)m

Distilling VIPER pattern

External article

DDDをチームに導入する際に考慮した4つのこと

この記事はドメイン駆動設計 #1 Advent Calendar 2018 20日目の記事です。

チーム内導入 = DDDでシステムを組み上げたよーではなくて
チームでDDDを使ってシステム開発しようという合意をスムーズにとるために考えたことを共有します

状況説明

状況がわからないと想像しづらいと思うので、まずは簡単な状況説明を...

私個人について

  • プログラマ歴5年
  • golang suki!
  • 前チームでDDDに則ったシステム開発経験済み
  • 個人的には設計周りの経験は浅い
    • チームの強い人が設計してたので、個人的には軽量DDDレベルで開発してた
    • 軽量DDDにも満たないかも...
  • 複数の副業先で前チームの知識+副業先の強い人の意見をもらいながら軽量DDDで開発を行い、ディレクトリ構成をupdateし続けていた

あらまし

  • 全社的にレガシー脱出しようという機運
  • その流れでプロダクトを5つ保守するチームが爆誕し、そのチームに引き込まれる...
  • 5つともレガシーで伸びしろ(良い言い方)がすごい
  • 5つのうち2つに関してはそのまま引き継ぐのではなくリプレイスを行うことになってた
    • 開発陣は3つのプロダクトの保守や改善(これをしないと開発速度が爆遅)を行いながら、2つのプロダクトのリプレイスを行うことに...ナニイッテイルカワカンナイ
  • 唯一救われることとしては締切がほぼほぼないこと

チームメンバー構成

スクラムで開発を回しているのでその役職と詳細を記載します

PO 1名/SM 1名 /DEV 4名 の計6名のチームです

ざっくりスキルを図で表してみました
skill.png

チームのスキルグラフから見て分かる通り、リプレイスにおいて必須スキルな設計がとてつもなく弱いチームなことがわかります...
上記状態だったので自分が経験したことのあるDDDで推し進めることにしました

4つのDDD導入へのアプローチ

1. DDDについての説明とどう関わってくるかを意識させる

そもそも自分の知識が薄かったので、わかる!ドメイン駆動設計 ~もちこちゃんの大冒険~を購入
ある日の午前中に3章まで読んで、自分の中でまとめて午後にメンバーに向けて内容の共有会+リプレイス時にこの手法を取り入れたいと思っていることの頭出しを行いました

所要時間: 2時間程度
共有した内容:

  • ドメインとは?
    • 事業の対象領域
    • 旧システムだとドメインはここにあたるよってことと、ドメインの再定義を行う必要があるよね?っていう話をした
  • ユビキタス言語とは?
    • 概念に対して同じ言葉をつかって話していこう、リプレイス時のソースコードでも変数名などでそれをそのまま使うよって話
    • まずは旧システムを保守している人をドメインエキスパートとして参加してもらいたいこと
    • その際の関係性として前みたいなDEV - 通訳 - ドメインエキスパートみたいな関係ではなくDEVとドメインエキスパートが直接やり取りできるような関係にしておきたいこと
    • 引き継ぎ時にユースケースをドメインエキスパートと出しながら、ユビキタス言語を洗い出しておきたいと思っていることを共有
  • ドメインモデルとは?
    • ドメインを構成する物・概念・振る舞い・関係性
    • 最終的にはそれらをコードでのみ表す
    • 運用に乗った場合はドメインモデルに対しての変更をイテレーティブに行っていく

伝わって欲しかったこと:

  • DDD試してみないか?
  • ドメインエキスパートの協力を得たい
  • 現状のユースケースとユビキタス言語を出そう
  • DDD難しいから学習時間抑えれるようにして皆で学習していきたい

2. 学習時間の確保

POに現状のチーム全体のスキルセットを再共有し、リプレイスする上で必要な設計の知識が今のチームにはないことを認識してもらう。また、設計に力を入れる理由を説明し学習時間の確保をお願いした

以下が学習時間確保のために話した内容

  • DDDは最初は時間がかかりそう
    • 学習と設計、進め方の整備
  • プロダクトが長く息をしそう(使われるのがわかっている)なので変更容易性が高い、かつ設計をコードで保守できるDDDがよさそう
  • チームに対してのプロダクト数が多いので保守性を高めて後々かかる人件費を抑えたほうが良い
  • 私が頑張って主導します
    • ここで テンプレートは個人的に用意してあって、それ使うから実装時間短縮できますとかもカードとして切った
    • https://github.com/mafuyuk/ddd-go-api-template 内容はちょびちょび変わるかも

これによって、DDD学習時間を工数として手に入れました
このタイミングでDEV陣にはDDD本を全員購入してもらった(会社の金で!)

3. 学習体制を整える

まず学習体制を組みにあたって考えたことは誰がどこまで、いつまでに理解できたらいいのかを考えました

以下表はその際に出したものです。スクラムのRole+ドメインエキスパート各々がDDDの把握しておくべき概念とそれによって何をできるようになって欲しいかを明示し、その内容を各Roleに共有し合意を得るようにしています

Role DDDの把握しておくべき概念 概念を把握することでどういう行動を望むのか 備考
PO ユビキタス言語、コンテキストマップ ドメインを定義できる。ドメインに対しての追加修正の可否を判断できる DDD本の1~3章
SM ユビキタス言語、コンテキストマップ、ドメイン、エンティティ、値オブジェクト ドメイン領域にどんな値オブジェクトが定義されているか?オブジェクト同士の関係はどうなっているか?を概要レベルで説明出来る DDD本の1~3、5章
DEV 全部 DDDに沿った開発、運用を行える。メンバーの教育が行える
ドメインエキスパート ユビキタス言語、ユースケース 引き継ぎ時の要素の洗い出しを行える。要件定義をDEVと同じ言語を用いて共同で進めることができる DEVが引き継ぎ時に共有する

学習スケジュールとしては4週間でエリック本を読破してその内容をチーム内共有することにしました
チーム内共有資料を用意して共有を行うのはDEV陣でPOやSM、ドメインエキスパート(希望者のみ)は上記で明記した内容が共有される会に参加(把握しておいて欲しい会への参加をDEV陣から促す形)して知識向上するという形で進めてます

共有内容はこんな感じ → DDD本3章を読んでこのように理解しました

学習体制に関しては、保守や改善、リプレイス側でも思わぬものが出てきたりでスケジュールは崩れつつあって都度見直しをかける予定です

4. ドメインエキスパートからの旧システムの引き継ぎと関わり方の相談

旧システムを保守している方、仕様に詳しい方をドメインエキスパートとして参加していただけるようにお願いしています
その際に説明したことは以下です

  • リプレイスに至った経緯
  • 現状のリプレイスの体制
    • メンバーやスケジュールなど
  • DDDについての説明
    • メンバーに頭出しするさいにつかったまとめの共有をここでも行っています
  • ドメインエキスパートというポジション
    • 上記に記載した概念を把握することでどういう行動を望むのかを共有し、お願いするようにしています。
    • どこまでやれるようになって欲しいかを明確にしていることによって負担がすくないことを事前に共有ができるため好意的に捉えてもらえています

その後、DEVメンバーと一緒に現状使われている仕様のユースケースとユビキタス言語の洗い出しを行うようにしました

まとめ

現状までの簡単なKPTをメンバーにしてみた結果を 好感触だったこと/うまくいかないこと(うまくいくようにしたいこと) とまとめてみました
うまくいかないこと(うまくいくようにしたいこと)で出たことは来年の課題ですね:muscle:

好感触だったこと

  • RoleごとにどこまでDDDについてどこまで学習すればよいか明記する
  • チーム全体でDDDについて学習をやっていこうという姿勢
  • 主導する人がいる(私)
  • ドメインエキスパート-DEV間に通訳をなくして直接ユビキタス言語を使って話すこと
  • ドメインエキスパートという概念があるので、今までみたいに要求側と開発が分断されづらい
  • 設計をコードで表すことで設計が失われないこと
  • ドメインを理解して設計することで、ソフトウェアの本質を捉えた良いものが作れそう
  • ユビキタス言語によりコミュニケーションが取りやすくなりそう

うまくいかないこと(うまくいくようにしたいこと)

  • ドメインの抽出
    • まだ引き継ぎ途中なので仕方ないのかもしれないのですが、リプレイス時にドメインの再定義をこうやるといいよ~など情報があったら共有いただきたいです:bow:
  • リソース不足の状態での工数捻出
    • 工数は捻出しているけど結局割り込みの作業などが発生していて予定通り進めれていない
  • DDDがそもそも難しい
    • 講師が欲しいなー(チラ
  • DDD以外の設計手法を選定していない
    • 手続き型は例外として他の設計手法を吟味できていない感が強いのでDDD以外にこれがいいよっていうのがあれば教えていただきたいです:bow:
  • ドメインをコードで表すことになるので、技術に弱い人がどこまでDDDの思想について来れるかが不安
    • コードがわからなくて「ここの仕様資料にまとめて欲しい」とかでてきたら困るけどそれをどう回避するのか...悩ましい
    • チームによってはここで妥協が必要になったりしそうだと思っている
  • ユビキタス言語の洗い出しが今回のプロダクトだと関連事業部が多くて意外と時間かかる(けどドメインを明確にするためにやる必要がある)
  • エリック本の前半が抽象的なので、具体例があるとわかりやすい
  • 設計を経験したことがない人にとっては、何がメリットになるのか分かりづらい
  • 初学者に対して理解しやすいドキュメント(資料、書籍など)の選定が必要

さいごに

こんな感じでうちはやっていますという共有でした
DDDを導入するのはメンバーが揃っていないチームには結構重いことだと思っているので、こういう試みをしてやっと入れれた感(導入途中だけど)がありました

現状は4. ドメインエキスパートからの旧システムの引き継ぎと関わり方の相談の途中なので、このやり方+αでうまくできたーやうまく行かなかったーがあったら加筆修正または新しく記事投稿してみようかなーと思います

まだ要件定義までいっていないので引き継いでドメインを設計する際にそもそもこのシステム必要なの?とかスコープ小さくしようなどの議論が発生すると思っていて、DDDは使わず行く可能性はまだまだありますが個人的には設計周りは結構楽しいなーと思っているのでその界隈の人たちと繋がって情報交換などしたいですねー

よかったら情報交換させてください:bow:
Twitter: #mafuyuk

DDDの構成要素とマイクロサービスの単位をどう合わせるべきか

この記事は ドメイン駆動設計 #1 Advent Calendar 2018 の 21日目 です。

前日は @mafuyuk さんの「DDDをチームに導入する際に考慮した4つのこと」でした。
明日は @dnskimo@github さんです。

この記事の内容

実務でドメイン駆動設計(以下、DDD)とマイクロサービスアーキテクチャを実践していますが、
DDDとマイクロサービスの粒度について、チームメンバーでの解釈が異なっていることもありました。

この記事では、DDDの構成要素とマイクロサービスをどう合わせるのがいいのか? を考察していきたいと思います。

いきなり結論

先に結論を言ってしまうと、「DDDのサブドメインをマイクロサービスの単位とする」 になると考えます。
境界づけられたコンテキスト : サブドメイン(のドメインモデル) : マイクロサービス が、1 : 1 : 1 になるのが理想形です。

以降、その理由を解説していきます。

まずは言葉の定義を

DDD や マイクロサービス の文脈で登場する言葉は受け手の解釈によるところが大きいので、以降の考察がブレないように記事中の言葉を定義しておきます。

DDD

マイクロサービスとの関係と言う観点では「ドメイン」「サブドメイン」「ユビキタス言語」「境界づけられたコンテキスト」あたりの概念を共通認識できていればOKです。

これらについて、 ドメイン駆動設計 #2 Advent Calendar 2018 の 18日目の記事 「DDDのドメイン・サブドメイン・ユビキタス言語・境界づけられたコンテキストを整理する」 で解説していますので、そちらを参照してください。

domain_subdomain_bc.png

マイクロサービス

書籍 「マイクロサービスアーキテクチャ(以下、Newman)」 を参考に、「マイクロサービス」 は以下の特徴を持つものとします。

  • ある関心事について、その問題を解決するための一連の機能を提供します。 そのマイクロサービスだけで問題が解決まで完結しなければなりません。
  • 1つの マイクロサービス には、そのサービスを利用するための複数のエンドポイント(RESTful API や メッセージキュー など)が含まれます。
  • 他のサービスが停止していても独立して稼働し続けることができます。 その間は他のサービスとの間に不整合が生じえますが、最終的に整合性が取れた状態になります。(結果整合性)
  • 他のサービスが稼働したまま、単独でデプロイすることができます。
  • マイクロサービスを構成するモジュールは、1つだけのこともあれば複数の場合もあります。 たとえばJVM系言語であれば1つの warモジュール だったり、APIエンドポイントごとの複数の jarモジュール だったりします。

ちなみに、同書では以下のように「マイクロサービスはDDDの境界づけられたコンテキストの単位にすべし」と書かれています。

一般に、マイクロサービスは境界づけられたコンテキストときれいに一致するようにします。
- Newman 第3章 サービスのモデル化方法 / 3.3.2 モジュールとサービス

既に述べたように、境界づけられたコンテキストに沿ってサービス境界の線を引くようにします。
それにより、チームも境界づけられたコンテキストに一致することになります。
- Newman 第10章 コンウェイの法則とシステム設計 / 10.8 境界づけられたコンテキストとチーム構造

マイクロサービスについての詳細は、以前の記事 「書籍 マイクロサービスアーキテクチャ まとめ」 も参照ください。

DDDとマイクロサービスのパターンについての考察

言葉の定義ができたところで、DDDのどの構成要素をマイクロサービスに対応づけるか?考えていきます。

マイクロサービスの単位はDDDの設計の結果、つまり「境界づけられたコンテキスト」と「ドメインモデル」がどう分割されたか?が前提となります。
例えば「ドメイン」が「サブドメイン」に分割されていないのにマイクロサービスにしようとしても、おそらくデメリットだけが強調されてメリットはないでしょう。

下表は DDDの設計の結果 と マイクロサービスの単位 をマトリクスで俯瞰したものです。
以降でそれぞれの組み合わせを見ていきます。

1.モノリシック 2.マイクロサービス
(境界づけられたコンテキスト単位)
3.マイクロサービス
(サブドメイン単位)
A.サブドメインに分割されていない ⭕️
マイクロサービス化は無理。

マイクロサービスのメリットが活かせない。

マイクロサービスのメリットが活かせない。
B.境界づけられたコンテキストに複数のサブドメインがある ⭕️
マイクロサービス化を検討すべし。

1つのマイクロサービスに、複数のサブドメインが混ざってしまう。
⭕️
ただし、成果物が混在してしまうことに注意。
C.境界づけられたコンテキストとサブドメインが1対1 ⭕️
マイクロサービス化を検討すべし。
⭕️
きれいに一致する
⭕️
きれいに一致する

A.ドメインがサブドメインに分割されていない

figures.draw-1-1-1.png

「ドメイン」が「サブドメイン」に分割されていない、つまりドメイン全体が一枚岩(泥団子とも言う)になっているなら、 そもそもマイクロサービスアーキテクチャを採用すべきではない です。
「ドメイン」が一枚岩と言ことは、それらの概念は強い整合性を保つべきで結果整合性は採用できないし、一部分だけを単独で発展させていくこともできないはずです。

そのような「ドメイン」を無理にマイクロサービスに分割してしまうと、サービス間でデータの整合性を保つのに苦労したり、複雑な基盤(分散トランザクションとか)が必要になったり、一度に多数のモジュールを同期を取ってデプロイしなければならなくなったり...とデメリットが大きすぎます。
また、ドメインの知識をプロジェクト全体で共有しなければならないので、マイクロサービスにしても組織内での調整・合意形成がボトルネックになります。

無難にモノリシックな構成にするのが良いと思います。

B.境界づけられたコンテキストに複数のサブドメインがある

figures.draw-1-1-N.png

DDDの設計の結果、「ドメイン」が「サブドメイン」に分割されていますが、 1つの「境界づけられたコンテキスト」の中に複数の「サブドメイン」が含まれている(つまり、複数の「サブドメイン」でユビキタス言語を共有している)構造です。

このケースでは、マイクロサービスアーキテクチャも採用できますが、ドメインの構造とマイクロサービスの単位がきれいに一致しない ので注意が必要です。

マイクロサービスを「境界づけられたコンテキスト」単位に作る場合、1つのマイクロサービスで複数の「サブドメイン」を扱う形になるため 「サブドメイン」を分割した意味が薄れてしまいます。

例えば、ある「境界づけられたコンテキスト」に「コアドメイン」と「支援サブドメイン」が含まれているとすると、マイクロサービスは「支援サブドメイン」のことを考慮しなければならないため、「コアドメイン」に注力できなくなります。
また、「支援サブドメイン」を変更してデプロイする際、「コアドメイン」の機能も停止しなければならなくなるなどの影響を受けてしまいます。

一方、マイクロサービスを「サブドメイン」単位に作る場合は、それぞれマイクロサービスの独立性・凝集性を確保できます。

ただし、この場合に注意が必要になるのは、成果物(ドキュメントやソフトウェア資産)は「境界づけられたコンテキスト」でまとめられるので、 1つの「境界づけられたコンテキスト」に複数のマイクロサービスの成果物が混在することになる 点です。
そのため、構成管理 や 継続的インテグレーション/継続的デリバリ で工夫が必要になるかもしれません。

C.境界づけられたコンテキストとサブドメインが1対1

figures.draw-1-N-1.png

DDDの設計の結果、「ドメイン」が「サブドメイン」に分割されており、かつ 「境界づけられたコンテキスト」と「サブドメイン」が1対1に対応している 構造です。

このケースでは、ドメインの構造とマイクロサービスの単位がきれいに一致します。

なお、1つの「境界づけられたコンテキスト」には1つの「サブドメイン」しか含まれていないので、 マイクロサービスの単位は「境界づけられたコンテキスト」でも「サブドメイン」でも同義 になります。

まとめ

以上の考察から、「DDDのサブドメインをマイクロサービスの単位とする」 のが一番しっくりきます。
これであれば、DDDの設計の結果...つまり「境界づけられたコンテキスト」と「サブドメイン」の関係が1対1なのか、1対多なのか...に関係なく、マイクロサービスの単位は変わりません。 (前出の図中の B-3C-3

このパターンは、マイクロサービスに関するパターンを紹介しているサイト microservices.io および 書籍 Microservice Patterns でも Decompose by subdomain pattern として紹介されています。

Define services corresponding to Domain-Driven Design (DDD) subdomains.

一方、「境界づけられたコンテキストをマイクロサービスの単位とする」と言った場合、DDDの設計の結果によってマイクロサービスの内容が変わってきてしまうので、解釈の仕方によっては適切なマイクロサービスの単位にならない 恐れがあると思います。(前出の図中の B-2C-2

なお、前出の 「マイクロサービスアーキテクチャ」も含めて、多くの文献で「境界づけられたコンテキストをマイクロサービスの単位とする」ことが紹介されてはいます1が、それらはおそらく 「境界づけられたコンテキスト」と「サブドメイン」が1対1になっていることを前提としている と思われます。
つまり、意図としては「サブドメインをマイクロサービスの単位とする」と同じことを意味していると思います。(前出の図中の C-2C-3

おわりに

言葉遊びのようなまとめになってしまいましたが、逆に言えばそれだけいろいろな解釈の仕方ができて、曖昧になりやすいとも言えます。
DDDとマイクロサービスアーキテクチャを採用する際は、プロジェクト・チームで認識を合わせておくほうが良いと思います。

また、マイクロサービスアーキテクチャの採用にDDDの実践は必須ではありませんが、疎結合・高凝集なマイクロサービスを導き出すための視点をDDDが与えてくれます。
DDDすべてを実践しなくても、DDDの戦略的設計(ドメインをサブドメインに分割し、ユビキタス言語を境界づけられたコンテキストで区切る...)だけでも導入する価値はあるのではないでしょうか。

参考文献


  1. 私自身も、この記事を書くまで「マイクロサービスは境界づけられたコンテキスト単位に作るべきだ」と言ってました。 

集約とトランザクション境界に関するメモ

External article

ユビキタス言語と境界付けられたコンテキストを構築する目的とは

このエントリーは、 「ドメイン駆動設計 #1 Advent Calendar 2018」の23日目の記事です。
22日目は、@dnskimo@github さんの「集約とトランザクション境界に関するメモ」でした。

はじめに

この記事は以下の書籍から数多く引用しています。この記事を読んで興味をもたれた方は併せてご覧頂ければと思います。

TL,DR

  • ドメイン駆動設計におけるドメインとは、あるシステムにおける領土・分野のこと
  • ユビキタス言語とは、ある目的に添った、ステークホルダ間での共通理解に必要な言葉の『最も小さな粒度である単語』のこと
  • 境界付けられたコンテキストとは、あるシステム境界におけるドメインにおいて『ユビキタス言語を元に構築された文脈(コンテキスト)』のこと
  • 概念モデルとは、境界付けられたコンテキスト内部でユビキタス言語によって構築される「物事」「考え」「対象」「現象」の本質を抽出して単純化した構造のこと
    • 本質的に全てのモデルは間違っているが、中には役に立つ物もある。 - George E.P. Box

『ソフトウェアの核心にある複雑さに立ち向かう』とは

 ドメイン駆動設計は、Eric Evansが発表したソフトウェア設計と開発に対するアプローチです。同書の副題は、『ソフトウェアの核心にある複雑さに立ち向かう』となっています。さて、ソフトウェアの複雑さとは何を指すのでしょうか?
 筆者にとってのソフトウェアの複雑さとは、ソフトウェア開発を取り巻く周辺環境(プロジェクト管理、設計、実装、運用など)において、
何もしなければ、エントロピー増大の法則により秩序から無秩序へと進んだ先にある、無秩序な状態』であると考えています。
無秩序なソフトウェア開発の現場では、システムを熟知しているエンジニアの退職などで全貌の把握が不可能となったり、品質担保のための管理工数の増大モチベーションの低下コストの増大納期の遅れなどが発生する可能性が高いことが否定できません。
 筆者の考えるドメイン駆動設計の『複雑さに立ち向かう』理由は、ドメイン駆動設計を利用した現場で利用されることの多いアジャイル開発(プロジェクトマネージメント手法)で定義される『予算』『時間』『品質』『スコープ』の4点のトレードオフ要素が、各々トレードオフがありながらも全ての要素が良い方向に向かわせて、結果として秩序立てた状態とすることです。
トレードオフの取捨選択(トレードオフスライダーと呼ばれます)は選択と集中のための手段ですが、プロジェクトを運営する上で『品質』も『スコープ』も『時間』も犠牲にできないという場合(『予算』は外的要因が多いため除外しています)に、可能な限り各要素を最大化を目指す為の手段であると言えます。
 ドメイン駆動設計における『複雑さに立ち向かう』ために必要な要素として、平易で簡潔な言葉で説明が可能となる『ユビキタス言語』と、ユビキタス言語により構築された機能その物である『境界付けられたコンテキスト』を関係者間で築き上げ、全関係者が納得できる形で『概念モデル』を完成することがゴールです。

『ドメイン駆動設計における分析』とは

 ドメイン駆動設計における分析とは、『ユビキタス言語』の構築『境界付けられたコンテキスト』の構築の2点が挙げられます。
 ユビキタス言語とは、ある目的に添った、ステークホルダ間での共通理解に必要な言葉の『最も小さな粒度である単語』です。
たとえば、筆者の本業であるレシピ動画メディアでは、『レシピ(料理)』『キュレーション(配信用レシピのバルクで「本日のオススメ」などの一セット)』『フライヤー(チラシ)』などがその代表例です。
これらの単語は、たまたま自然言語と一致して分かりやすい物となっていますが、組織内でしか通じない言葉となることも希ではありません。
 境界付けられたコンテキストとは、あるシステム境界におけるドメインにおいて『ユビキタス言語を元に構築された文脈(コンテキスト)』です。
境界付けられたコンテキストは、よりドメインの範囲の広いユビキタス言語であり、例えば『リテール(小売り)』や『プレミアム(特別性の高い課金)』などがあげられます。
 実際のビジネスにおけるソフトウェアの利用はビジネスの活動の全体においてその一部であり、ビジネスドメインにおける本質的な関心事はソフトウェアが生み出した成果(売上げなど)を指すことが多く、ソフトウェアそのものには関心が低い場合が多いです。
ソフトウェアエンジニアとしては、ビジネスの専門家がシステムそのものに関心が低い場合があることを理解しながらも、関係者を巻き込んで『ユビキタス言語』と『境界づけられたコンテキスト』の構築を進めることを理解しておく必要があるでしょう。

ユビキタス言語を構築する目的

 Eric Evansは、ソフトウェア開発者がドメインエキスパートやユーザーなどの関係者と会話する場合、いかなる場合においても誤解のリスクを緩和するため共通理解が可能な、なるべく平易で簡潔な言葉(単語)を用いて、システムに関わる関係者間で誤解が発生しないように努めることを推奨しました。
この言葉は共通知識や言語として組織内に浸透することから、ユビキタス[至る所にある、遍在する、至る所に姿を現わす]な言葉となり、ユビキタス言語と呼ばれることとなりました。
 ユビキタス言語は一度決まったらそのままではなく、ドメインに関連する知識が深まるに都度、徐々に変化して進化する可能性があります。もしユビキタス言語の進化が発生した場合は、該当のユビキタス言語を用いたコーディング規約やコードと同期してリファクタリング対象となり、システムへの理解がさらに深まる事につながります
 ユビキタス言語を構築する意義とは、仕様策定の会議体・設計・コードそのものなどの全ておいてに、遍在的に(ユビキタス)に現れることで、組織内の人員のシステムに対する共通理解を深めることが目的です。

境界付けられたコンテキストを構築する目的

 境界付けられたコンテキストとは、システム相互作用における境界が定義された物を指し、実際のプロジェクトでは『縄張り』のような物です。境界付けられたコンテキスト内の各コンテキストはユビキタス言語の一つとして構築されて命名されることになります。
 ビジネスドメイン(ビジネス領域)の世界では、さまざまなサブドメインに比較的簡単に分割されることがありますが、ソフトウェア開発におけるドメインは、機能や概念が重複せずに簡単にサブドメインに分割できるケースは多くありません。
各コンテキストの境界を定義して、その内部のコンテキストがどのように扱うかを、図を書く作業はコンテキストマッピングと呼ばれます。
コンテキストマッピングをすることにより、ビジネスドメインにおける責任範囲の分離に役立つことがあります。本質的にはビジネスドメインとソフトウェア開発のドメインは表裏一体であるのですが、とあるシステムで機能系とレポート系があったときに、機能系はソフトウェアドメインとビジネスドメインを比較的簡単に分離できるのに対して、レポート系はビジネスドメインで分離が難しいことが多いです。
これはレポート系が各ビジネスドメインのデータを集約して、まとめた結果をレポートや帳票として出力する必要があるためです。従ってレポート系は本当に境界づけられたコンテキストとして分離する必要があるのか、よく考慮して設計する必要があります。
 境界付けられたコンテキストにより定義された各コンテキストは、実際の企業内の物理的な組織を反映していることがよくあります。
これはコンウェイの法則と呼ばれており、コンテキスト内部の名前がユビキタス言語となり、ユビキタス言語がチーム名となることがあります。
ソフトウェアは最終的には企業内の組織構造に反映されることから、コンテキストマップに企業の組織図を反映させるべきであるという考えがあります。しかしながら筆者の経験では、コンテキストマップをそのまま企業の組織図に当てはめることはスムーズにいかないことが多く、組織構造の変更がソフトウェアの構造変更よりも苦難が大きいことが原因であると考えられます。
 近年ではコンウェイの法則を逆手に取った、逆コンウェイ戦略と呼ばれる考え方が出現しています。これは組織体にソフトウェアを当てはめるのではなく、逆に戦略的に組織構造をソフトウェアに反映させることで、ビジネスの方向性とシステムの方向性を一致させて組織内コミュニケーションのAgility(俊敏性)を高める戦略です。
従って境界付けられたコンテキストを定義する目的とは、各組織体のチーム編成を適切な設計境界により行うことで、各チームが自分に与えられたコンテキストに『縄張り意識』を持つことが、チーム内外の人員に対する責任感やチーム内の一体感を生み、人と情報の流れを円滑とすることがゴールとなります。
 近年の大規模システムを構築する際によく取られる手法であるマイクロサービスは、境界付けられたコンテキストの境界の定義を適切に進めることで、成功確率が高い大規模サービスを作る手段と言っても差し支えありません。
組織内の文化にもよりますが、一度境界づけられたコンテキストが決定されると、ソフトウェアの価値基準の戦略的な見直しが発生しなければ境界づけられたコンテキストそのものの変更が発生しないことが多いようです。

まとめ

ここまで読んでいただきありがとうございました。
ソフトウェア開発の原料は言葉である。』という名言があります。
ドメイン駆動設計の戦略的分析で重要となる要素は、『ユビキタス言語』と『境界づけられたコンテキスト』の二点の構築が必要であると述べました。これらはソフトウェア開発の現場で生物のように有機的な変化や進化をしながら構築されていく物です。このトピックを読んでくれた方が、より具体的で実践的なユビキタス言語と境界付けられたコンテキストを構築する助けになったら幸いです。
 24日目は、@j5ik2o です。お楽しみに!

補足

実際にどのように『ユビキタス言語』と『境界づけられたコンテキスト』が構築されるのか拙筆スライドも併せてご覧ください。

エンジニアのためのドメイン駆動設計実践入門 / DDD for Engineer newbie


混在したモデリングパラダイムの中で学ぶ重要なこと

このエントリーは、 「ドメイン駆動設計 #1 Advent Calendar 2018」の24日目の記事です。
23日目は、@smdmts さんの「ユビキタス言語と境界付けられたコンテキストを構築する目的とは」でした。

DDD本で触れられている、モデリングパラダイムについて考えを晒します。

モデリングパラダイム

まずはモデリングパラダイムについて考えましょう。

目的

「モデリングパラダイム」という言葉をどういう意味を持つのでしょうか。それは、以下の二つの単語で構成されます。

  • モデリング
    • 広義の意味での模型(モデル)を組み立てる事を言う。
  • パラダイム
    • ある時代のものの見方・考え方を支配する認識の枠組み。

モデルを組み立てるための認識の枠組み」と考えてよいでしょう。

DDD本の用語解説では、以下の説明があります。モデリングの「枠組み」や「スタイル」と解釈して問題なさそうです。

ドメインにおける諸概念を切り取る特定のスタイル。ツールを組み合わされて、それらの概念に類似したソフトウェアを作成する(例えば、オブジェクト指向プログラミングや論理プログラミング)

そしてこのモデリングパラダイムに基づき、切り取ったモデルをプログラミング言語を使ってソフトウェアに反映します。この反映のしやすさというのは、プログラミング言語がどのモデリングパラダイムをサポートするかで、変わってきます。
たとえば、オブジェクトパラダイムをサポートしていない、C言語でもオブジェクト指向プログラミングは可能ですが、コードの読みやすさ・書きやすさは及第点がつきそうです。1

主要なモデリングパラダイム

そのモデリングパラダイムにはどのようなものかあるでしょうか。現在、よく知られているものは以下(丸括弧は対応する言語)。

  • オブジェクトパラダイム(オブジェクト指向言語)
  • 関数パラダイム(関数型言語)
  • 論理パラダイム(論理型言語)

現在でも、主流となっているモデリングパラダイムは、言わずもがな「オブジェクトパラダイム(オブジェクト指向)」です。2

DDD本でも以下のように言及があります。3

現在主流となっているパラダイムはオブジェクト指向設計であり、今日の複雑なプロジェクトのほとんどはオブジェクトを使い始めている。

この頃は「オブジェクトパラダイムを使い始めている時期」だったが、今となってはオブジェクトパラダイムは広く普及して、さらに関数型などの他のパラダイムも使われることが多くなっていると思います。

なぜオブジェクトパラダイムが主流になったか

では、なぜオブジェクトパラダイムが主流のモデリングパラダイムになったのか。DDD本からいつか引用します。

オブジェクパラダイムが選ばれ理由は、技術的でもオブジェクトに関するものでもないらしい。シンプルで洗練されているのが売りだという話。

チームがオブジェクトパラダイムを選ぶ理由の多くは、技術的なものではないし、オブジェクトに本質的なものですらない。しかし、オブジェクトモデリングは、それが世に出た時から、シンプルでありながら洗練されているという絶妙なバランスを確かに保っている。

そのモデリングパラダイムが難解すぎて開発者も少ないのであれば普及もしくくなりますが、オブジェクトパラダイムではそういうことは問題ならなかったような下りがあります。

モデリングパラダイムがあまりにも難解な場合には、習得できる開発者の数が不足するために、間違った使い方をされてしまうだろう。チームにいる技術寄りでないメンバが、少なくともパラダイムの初歩を把握することができなければ、モデルを理解することもできず、ユビキタス言語が失われることになる。オブジェクト指向設計の基本は、たいていの人にとって自然なものに見えるようだ。開発者の中には、モデリングの機微を理解できない人もいるが、技術者でなくてもオブジェクトモデルの図を理解することはできる。

技術者でなくても理解できるというのはさておき…オブジェクトパラダイムが開発者にとって比較的扱いやすく役に立ったという観点は否定はできないでしょう。また、次のように、マーケットシェアの拡大によって好循環が作られたという経緯もあるようです。

今日、オブジェクトパラダイムには、成熟し広範囲で使われていることに起因する重要な利点もある。(中略) 新しい技術のほとんどは、人気のあるオブジェクト指向プラットフォームと統合する手段を提供している

また、「開発者コミュニティと設計文化そのものの成熟」が重要とも述べています。エコシステムとして成熟したということなんでしょう。パラダイムの善し悪しはさておき、マーケットとしても大きな力を持つので無視はしにくいと思います。

同様に重要なのは、開発者コミュニティと設計文化そのものの成熟である。斬新なパラダイムを採用するプロジェクトでは、その技術を熟知している開発者や、選択したパラダイムで効果的なモデルを作成した経験のある開発者を見つけられないかもしれない。妥当な時間内で開発者を教育することも現実的ではないだろう。そのパラダイムと技術を最大限活用するためのパターンがまだ固まっていないからだ。おそらく、その分野の開拓者を迎えれば有効だが、その洞察は、まだ手に入るかたちで公表されてはいない。

これは妥当な見方ですが、オブジェクトパラダイム自身に起因するものではないと考えています。DDD発刊から10年以上経った今、システムに求める要求や開発ツールなど大きく変わってしまったので、そろそろ他のパラダイムについてもどうなのか気になるところです。

他のモデリングパラダイムの可能性

さて、他のパラダイム、特に関数型などのモデリングパラダイムの可能性はどうだろうか。DDDとしては特に否定はされていません。

ドメインモデルはオブジェクトモデルでなくてもよい。例えば、Prologで実装され、そのモデルが論理ルールとファクトから構成されているモデル駆動設計もある。モデルパラダイムは、人がドメインについて考える際に好んで用いる、特定の方法に取り組むものだと考えられてきた。すると、そうしたドメインのモデルは、そのパラダイムによって形作られる。

場合によっては、混在したパラダイムを使うことがあります。たとえば、Javaのようなオブジェクト指向言語でも、関数モデルのために関数オブジェクトを部分的に導入することがあります。他にも、O/Rマッパーは表とオブジェクトの混在したパラダイムとして古くからよく知られています。多かれ少なかれ、昔からオブジェクトパラダイムを主軸にしつつも、対象のドメインによってはパラダイムを適切に選択してきたと言えます。

プロジェクトにおいて支配的なモデルパラダイムが何であれ、ドメインの中には、他のパラダイムならばはるかに容易に表現できる場所が必ずあるものだ。あるドメインに、ほんのわずか変則的な要素があるが、その他の点では、特定のパラダイムで問題なく機能するという場合、開発者は首尾一貫したモデルの中に若干扱いにくい対象があっても我慢することができる(中略)。しかし、ドメインの主要部分が、さまざまな異なるパラダイムに属していると思われる場合、各部分を適したパラダイムでモデル化することは、知的好奇心を刺激する。この場合には、実装をサポートするツール類を混ぜて使用することになる。相互依存関係が少なければ、他のパラダイムで作られたサブシステムはカプセル化することができる。複雑な数学的計算であっても、単にオブジェクトから呼び出せばよいというようなものだ。あるいは、さまざまな側面がより絡み合っていることもある。例えば、オブジェクトの相互作用が、何らかの数学的関係で決まる場合が挙げられる。

最近では、Scalaなどのマルチパラダイム言語も登場しています。Scalaはオブジェクト指向言語をベースに関数パラダイムを取り入れており、モデリング技法についてもオブジェクトと関数の両方が適用可能です。(これは冒頭にも述べましたが。)たとえば、関数パラダイムをサポートしていないオブジェクト指向言語では、関数型スタイルのコードは書けますが、コードの読みやすさ・書きやすさは問題があります。当然ですが、マルチパラダイムを前提した言語ではこういった問題を軽減して、より効率的なモデリングをサポートします。

原点はモデル駆動設計

こういった混在したパラダイムを選択するしないにしろ、拠り所はドメインモデルが示す戦略の部分です。DDD本では以下の警鐘を鳴らしています。詳しくは書籍をご覧ください。4

  • 実装パラダイムと対立しないこと。
  • パラダイムに合うモデルの概念を見つけること。
  • UMLにこだわらないこと。
  • 懐疑的であること。

いずれにしても、ツールやドキュメントに頼り切らず、なぜそのようなモデルなのかを語り合い、ソフトウェアと取り巻くあらゆる活動で中心的な価値観を作り上げることが肝要ではないかと思います。

オブジェクトパラダイムに貢献する関数型設計テクニック

ここからは毛色を変えて、DDD本の第10章のしなやかな設計から、オブジェクトと関数のパラダイムの交差点となりそうな設計テクニックを抜粋して紹介してみようと思います。

副作用のない関数(SIDE-EFFECT-FREE-FUNCTIONS)

副作用のあるメソッドを組み合わせてしまうと、どんな影響が発生するか予測できなくなるため、プログラムの振る舞いを理解することが極端に難しくなります(内部実装を理解しなければ扱えないというのもこれが原因の一つ)。副作用のない関数同士ならば、組み合わせて使っても何が起こるか予測できなくなることがありません。また、テストも容易になります。これが、副作用のない関数(SIDE-EFFECT-FREE-FUNCTIONS)ですこの原則は、当然ドメインロジックにも適用可能です。

次の例は、DDD本に登場する、絵の具オブジェクト(Paint)の例です(普段はScalaでコード例を書くが今日はGoで書いてみよう)。二つの絵の具を混ぜるMixInメソッドの実装をみてください。このメソッドは戻り値を返さず内部状態を更新する可変メソッドであり、絵の具オブジェクト(Paint)も同一インスタンスで複数の状態へ遷移する可変オブジェクトです。書籍では顔料部分を副作用を起こさない別の値オブジェクトをに切り出し、絵の具オブジェクト(Paint)と組み合わせる方法が提案されています。

// 絵の具を表すオブジェクト(副作用が伴う可変オブジェクト)
type Paint struct {
    volume int // 絵の具の量を表す
    red int // red, yellow, blueは絵の具の顔料を表す
    yellow int
    blue int
}

func NewPaint(volume int, red int, yellow int, blue int) *Paint {
    return &Paint{ volume, red, yellow, blue }
}

func (p *Paint) GetVolume() int { return p.volume }
func (p *Paint) GetRed() int { return p.red }
func (p *Paint) GetYellow() int { return p.yellow }
func (p *Paint) GetBlue() int { return p.blue }
func (p *Paint) String() string {
    return fmt.Sprintf("[volume = %d, red = %d, yellow = %d, blue = %d]", p.volume, p.red, p.yellow, p.blue)
}

// 絵の具を混ぜ合わせる(破壊的操作)
func (p *Paint) MixIn(other *Paint) {
    p.volume += other.volume
    ratio := float64(other.volume) / float64(p.volume)
    // 以下、顔料の更新処理
    p.red = int(float64(p.red+other.red) / ratio)
    p.yellow = int(float64(p.yellow+other.yellow) / ratio)
    p.blue = int(float64(p.blue+other.blue) / ratio)
}

func TestPaint(t *testing.T) {
    p1 := NewPaint(100, 5, 10, 15)
    p2 := NewPaint(100, 15, 15, 15)
    fmt.Printf("p1 = %v\n", p1) // p1 = [volume = 100, red = 5, yellow = 10, blue = 15]
    fmt.Printf("p2 = %v\n", p2) // p2 = [volume = 100, red = 15, yellow = 15, blue = 15]
    p1.MixIn(p2)
    fmt.Printf("p1 = %v\n", p1) // p1 = [volume = 200, red = 40, yellow = 50, blue = 60]
}

次の例は、顔料オブジェクト(PigmentColor)を導入した例です。絵の具オブジェクト(Paint)は顔料オブジェクト(PigmentColor)に顔料に関する責任を委譲しているため、先の例と違って顔料の混ぜ合わせメソッドも顔料オブジェクト(PigmentColor)に移動しています。

// 絵の具を表すオブジェクト(副作用が伴う可変オブジェクト)
type Paint struct {
    volume int
    pigmentColor *PigmentColor
}

func NewPaint(volume int, pigmentColor *PigmentColor) *Paint {
    return &Paint{ volume, pigmentColor }
}

func (p *Paint) GetVolume() int { return p.volume }
func (p *Paint) GetPigmentColor() *PigmentColor { return p.pigmentColor }
func (p *Paint) String() string {
    return fmt.Sprintf("[volumen = %d, pigmentColor = %v]", p.volume, p.pigmentColor)
}

// 絵の具を混ぜ合わせる(破壊的操作)
func (p *Paint) MixIn(other *Paint) {
    p.volume += other.volume
    ratio := float64(other.volume) / float64(p.volume)
    // 顔料の更新処理を関数化して副作用を起こさないにし、関数で得た新しいpigmentColorを入れ替える処理をmixInメソッドで行う。
    p.pigmentColor = p.pigmentColor.MixedWith(other.pigmentColor, ratio)
}

// 顔料を表すバリューオブジェクト(副作用が伴わない不変オブジェクト)
type PigmentColor struct {
    red    int
    yellow int
    blue   int
}

func NewPigmentColor(red int, yellow int, blue int) *PigmentColor {
    return &PigmentColor{red, yellow, blue}
}

func (p *PigmentColor) GetRed() int    { return p.red }
func (p *PigmentColor) GetYellow() int { return p.yellow }
func (p *PigmentColor) GetBlue() int   { return p.blue }
func (p *PigmentColor) String() string {
    return fmt.Sprintf("[red = %d, yellow = %d, blue = %d]", p.red, p.yellow, p.blue)
}

// 顔料を混ぜあわせて新たな顔料を生成して返す。副作用は起こさない関数として実装する。(非破壊的操作)
func (p *PigmentColor) MixedWith(other *PigmentColor, ratio float64) *PigmentColor {
    red := (int)(float64(p.red+other.red) / ratio)
    yellow := (int)(float64(p.yellow+other.yellow) / ratio)
    blue := (int)(float64(p.blue+other.blue) / ratio)
    return NewPigmentColor(red, yellow, blue)
}

func TestPaint(t *testing.T) {
    p1 := NewPaint(100, NewPigmentColor(5, 10, 15))
    p2 := NewPaint(100, NewPigmentColor(15, 15, 15))
    fmt.Printf("p1 = %v\n", p1) // p1 = [volume = 100, red = 5, yellow = 10, blue = 15]
    fmt.Printf("p2 = %v\n", p2) // p2 = [volume = 100, red = 15, yellow = 15, blue = 15]
    p1.MixIn(p2)
    fmt.Printf("p1 = %v\n", p1) // p1 = [volume = 200, red = 40, yellow = 50, blue = 60]
}

先の例と比較して、顔料オブジェクト(PigmentColor)の混ぜ合わせメソッドMixInはthisを破壊せずに、新たな状態を持つインスタンスを返します。また、不変性を持つ顔料オブジェクト(PigmentColor)を可変性を持つ絵の具オブジェクト(Paint)に組み合わせる際は、pigmentColorフィールドの値を新しい顔料オブジェクト(PigmentColor)で上書きすることになります。
つまり、副作用が伴うロジックを、副作用が伴わない(問合せとしての)関数と、副作用を伴う命令を分けて用いることで、副作用を安全に局所化することができるわけです。これは関数の持つ一つの特性を利用したに過ぎませんが、設計は理解しやすく、テストもしやすく、安全に扱うことにができ、他の操作と組み合わせやすくなります。こういった観点で設計をサポートすることによって、このクラスの利用者はインターフェイス仕様だけを理解すればよく、内部実装まで理解する必要はありません。

閉じた操作(CLOSURES OF OPERATIONS)

閉じた操作は、数学的性質5を利用した、モジュールの凝集度を高めるためのパターンです。
たとえば、実数の加算は実数の集合の下で閉じているという表現をします。

1 + 1 = 2

プログラム上でも以下のような操作をよく行いますが、対象となる操作(メソッド)は加算だけに限った話ではありません。

StringC = StringA + StringB
ListC = ListA + ListB
MoneyC = MoneyA + MoneyB
MoneyC = MoneyA - MoneyB

この閉じたメソッドのシグニチャには、それを所有するクラス以外に依存しない、つまり同じ型に閉じているというところに注目してください。
具体的なコード表現に落とすと以下のようになるでしょう。実装する際は、戻り値の型が引数の型と同じになるようにします。

type Money struct {
    amount int
    currency string
}

func (m *Money) GetAmount() int { return m.amount }
func (m *Money) GetCurrency() string { return m.currency }

func NewMoney(amount int, currency string) *Money {
    return &Money{ amount, currency }
}

// 閉じた操作としての実装したAddメソッド
func (m *Money) Add(other *Money) (*Money, error) {
    if m.currency != other.currency {
        return nil, fmt.Errorf("Invalid currency: %v", other)
    }
    return NewMoney(m.amount + other.amount, m.currency), nil
}

このパターンでは、余計な概念を混入させずに、モジュール内にある知識だけで対象のドメインを理解できるようになります。これが高凝集をサポートする理由です。

宣言的な設計

処理方法ではなく対象の性質などを宣言することで設計(宣言的な設計)するスタイルを、DDDでは「宣言的スタイル」と呼んでいます。そのアプローチには、以下の3つがあると紹介している。それぞれPros/Consはあるのですが、中でもDSLのアプローチに優位性を見出しているようです。詳しくは10章の宣言的な設計を読んでみてください。

  • コード生成
    • モデルの特徴を宣言することえ動作するプログラムを生成するアプローチ。
  • ルールベース
    • ルールを定義する言語を使うアプローチ
  • ドメイン特化言語(DSL=domain-specific language)
    • 特定のドメイン向けに設計されたコンピュータ言語

ドメイン特化言語

宣言的な設計がよく利用される例に、ドメイン特化言語(DSL)があります。DSLには、プログラミング言語内部で利用できる内部DSLと、プログラミング言語の外で利用する外部DSLがあります。DSLを使うと表現力が豊かでユビキタス言語との結びつきも強くできると評価している。

こういった、HowとWhatの分離は関数型のカルチャーに多く見られます。以下は、言語内DSLの一例で、Freeモナド6を使ったリポジトリの実装を示したものです(すまぬ…。ここからScalaだ…)。今回はリポジトリが対象ですが、特定の業務でもよいでしょう。

// DSLによって表現されたFreeのインスタンス
val program: Free[UserRepositoryDSL, UserAccount] = for {
  _      <- UserAccountRepository.store(userAccount)
  result <- UserAccountRepository.resolveById(userAccount.id)
} yield result
// Freeのインスタンスを評価する
val evalResult: ReaderT[Task, DBSession, UserAccount] = userAccountInterpreter.run(program)
// DBSessionを与えて実行する
val resulFuture: Future[UserAccount] = evalResult.run(AutoSession).runAsync

Freeに適用するDSLはまさに自由です。FreeのDSLを使って命令(What)を記述しただけでは何もできません。

sealed trait UserRepositoryDSL[A]

case class ResolveMulti(ids: Seq[UserAccountId])      extends UserRepositoryDSL[Seq[UserAccount]]
case class ResolveById(ids: UserAccountId)            extends UserRepositoryDSL[UserAccount]
case class Store(userAccount: UserAccount)            extends UserRepositoryDSL[Long]
case class StoreMulti(userAccounts: Seq[UserAccount]) extends UserRepositoryDSL[Long]
case class SoftDelete(id: UserAccountId)              extends UserRepositoryDSL[Long]
case class SoftDeleteMulti(ids: Seq[UserAccountId])   extends UserRepositoryDSL[Long]

// type ByFree[A] = Free[UserRepositoryDSL, A]

object UserAccountRepositoryByFree extends UserAccountRepository[ByFree] {

  override def resolveById(id: UserAccountId): ByFree[UserAccount] = liftF(ResolveById(id))
  override def resolveMulti(ids: Seq[UserAccountId]): ByFree[Seq[UserAccount]] = liftF(ResolveMulti(ids))
  override def store(aggregate: UserAccount): ByFree[Long] = liftF(Store(aggregate))
  override def storeMulti(aggregates: Seq[UserAccount]): ByFree[Long] =
    liftF(StoreMulti(aggregates))
  override def softDelete(id: UserAccountId): ByFree[Long] = liftF(SoftDelete(id))
  override def softDeleteMulti(ids: Seq[UserAccountId]): ByFree[Long] = liftF(SoftDeleteMulti(ids))

}

命令を具体的にどのように処理するか(How)は、構造的に分離されたインタプリターが担います。

class UserAccountInterpreter[M[_]](...) {
  private val dao: UserAccountDao[M] = ...

  private def interpreter: UserRepositoryDSL ~> M = new (UserRepositoryDSL 
~> M) {
    override def apply[A](fa: UserRepositoryDSL[A]): M[A] = fa match {
      case ResolveById(id) =>
        // daoを使った処理
      case ResolveMulti(ids) =>
        // daoを使った処理
      case Store(aggregate) =>
        // daoを使った処理
      case StoreMulti(aggregates) =>
        // daoを使った処理
      case SoftDelete(id) =>
        // daoを使った処理
      case SoftDeleteMulti(ids) =>
        // daoを使った処理
    }
  }

  def run[M[_]: Monad, A](program: ByFree[A]): M[A] =
    program.foldMap(interpreter)
}

それなりに高度な技術と設計概念を具現化する能力が求められるのは確かですが、DSLによって表現力が向上するのは魅力的かもしれません。

注意

本題と少し逸れますが、この例では、GroupRepositoryなどの別の型のDSLと合成する場合、どうしたらいいかは触れていません。この実装のままでは合成できません。InjectKやTagless Finalを使う方法があります。興味あれば以下のリンクを参照してみてください。

https://typelevel.org/cats/datatypes/freemonad.html#composing-free-monads-adts
https://qiita.com/yyu/items/377513f17fec536b562e

まとめ

主軸となるオブジェクトパラダイムを活用しながらも、関数型などの他のパラダイムから導入できる設計パターンが数多くあるので、参考にする価値があると思います。以上!

明日は @kimutyam さんです!


  1. 逆に、Go言語は、一般的なオブジェクト指向言語ではない(「クラス」と「継承」がないからとか、例外もないとか)とよく言われますが、「インターフェイス」と「メソッド」によって最小限のオブジェクトパラダイムをサポートするので、C言語での曲芸のようなオブジェクトコードより、見慣れたオブジェクト表現が可能です。オブジェクト指向の定義にもよりますが、必ずしも特定の言語機能さえあれば達成できるということでもないようです。 

  2. オブジェクト指向の方式(クラスベース, プロトタイプベース、Mixinなど)について個々では問いません。オブジェクト同士の相互作用として、システムの振る舞いをとらえる考え方と捉えてください。 

  3. 第二部 モデリングパラダイムからの引用 

  4. 第二部 パラダイムを混在させる際にはモデル駆動設計に忠実であること 

  5. 半群(Semigroup)の性質を利用しているのでしょう。たぶん。 

  6. Free Monad - Cats 

コンテキストマップの目的再考と運用ヒント

External article
Browsing Latest Articles All 24 Live