のんびり精進

調べた情報などをおすそ分けできれば。

Dart/Flutterでドメイン駆動設計(DDD)してみた - 導入編

f:id:kabochapo:20191108180129g:plain

カテゴリ別にメモを管理できるアプリの開発を DDD(Domain-driven design)でやってみたものです。

github.com

二つの記事から成り、この記事はその一つ目です。

  • 導入編(本記事)
    解決しようとした問題点や、DDD と関連用語の意味の他、モデリング・レイヤ分け・ディレクトリ構成の検討において考えたことなどをまとめています。
  • 実装編
    Dart/Flutter での実装を中心としますが、一つ目で触れていない点(集約など)の説明も含みます。

やってみようと思った経緯

何かを作るとき、設計がメチャクチャであっても運良くそれっぽく出来上がることがあります。 小さなものなら直しやすかったり、あるいは問題があまり顕在化しなかったりするかもしれません。

しかし、大きなものでは次第に破綻してしまうことが容易に想像できます。

Flutter でも、小さなアプリを作って学ぶ間は「なんて簡単にできるんだ!」と感動したものですが、その段階を過ぎて大きめのアプリを作り始めたとき、急に悩むようになりました。

出てきた問題点

  • ビジネスロジックのクラスが肥大化した
  • API・DBの操作処理などのファイルを分けたら依存関係が混沌とした
  • どこに何の処理が書かれているか見えにくく、似た処理が複数箇所に散らばって無駄が生じた
  • そうなったことに気づいていたが、改善が難しかった
  • 何日間か中断してから再開すると構成を忘れていて読み解かないといけなくなった
  • ちょっとした変更の影響が大きくなって手間取った

ビジネスロジックを UI から分離するのは既にやっていてもそうなってしまい、何か別の解決策があるはずだと考えて模索した結果「これだ!」と思えたのが DDD です。

主流のアーキテクチャパターンではダメ?

ネイティブアプリ開発では MVP、MVVM といったものが主流のようです。 アプリ側の経験は少ないですが、MVVM は BLoC パターンと共通しているものがあると思えて調べてみました。

flutter - MVVM vs Bloc patterns - Stack Overflow

これによると、両者はほぼ同様の考え方だと捉えて良いようです。

その二つが似ているのであれば、BLoC パターンで解決できない問題は MVVM でも同じなのでは…。 それよりも DDD を取り入れるほうが確実に解決できるはずだと考えました。

何が変わるか

ChangeNotifierProvider を用いていて、これ自体は変わったことではありません。 状態管理やリビルドの制御の方法として一般的になってきているものです。 それを使ったよくあるサンプルとの違いは、Model をより細かく設計(DDD)しているという点です。

また、「メモのタイトル専用の型(使うときに文字数のバリデーションも同時に行う)」「タイトルの変更(タイトルを作り直してエンティティが持つ値を差し替える)」といった機能を細かく作って組み合わせ、アプリケーションサービスで取りまとめておくことで、Flutter の UI 側からそれらの機能を使うときに「あれ?もうほとんどできてる!」という驚きを感じるくらい自然に出来上がっていきました。

主に参考にしたもの

現場で役立つシステム設計の原則

DDDって何?と思っていたときに最初に読み、取り入れていこうと思うきっかけになった本です。 増田さんによる本ですので、以後は短く「増田本」と呼ばせていただきます。

ボトムアップデザイン駆動設計
ボトムアップデザイン駆動設計 後編

増田本を読んで良さがわかったものの具体的な実装がわからないというモヤモヤがこの記事で解消されました。 スライドもわかりやすいので、そちらをさっと見てから記事を読むと良いかもしれません。 コードは C# ですが、Dart な人にも理解は難しくないと思います。

little hands' lab

調べていると辿り着くことが多く、疑問が解けるような明解な情報が多いです。 また、今回のコードをほぼ書いてから、答え合わせのように 2019-08-31 の記事 を読みました。
同じ方(松岡さん)が Twitter に時々書かれている質問箱の回答もとてもわかりやすいです。

WEB+DB PRESS Vol.113

「体験 ドメイン駆動設計」という貴重な特集があり、作った後に読んで復習になりました。

読み終えていないもの

購入して少しだけ見たところから進んでいません。 評判を聞く限り、どちらも大変為になる良書ですが、いきなり読むと難しいかもしれません。

実践ドメイン駆動設計
「エリック・エヴァンスのドメイン駆動設計」よりもこちらから入ったほうがわかりやすいそうです。

Clean Architecture 達人に学ぶソフトウェアの構造と設計

はじめにご理解ください

  • DDD について学びながら作ったサンプルです。奥が深い DDD を部分的に理解した段階で作ったものですので、間違った捉え方をしている部分があるかもしれません。
  • 上記の「ボトムアップデザイン駆動設計」をベースにしましたので、DDD の本質的なところはそちらをお読みください。
  • その記事では「ユーザ」と「サークル」が出てきますが、私のサンプルでは「メモ」と「カテゴリ」にしています。
  • Flutter に依存するのは UI(プレゼンテーション)層とインフラストラクチャ層のみで、この記事の中心部分は Dart と DDD の話になります。

用語(ドメインやモデルとは)

DDD で使われる用語には DDD 以外でも聞いたことがあるものがあります。 MVC の M もモデルであり、データベースに関連することを行う部分というイメージです(この捉え方自体が間違っているようですが)。

そのような既知の言葉のイメージが理解の邪魔になりました。 同じ呼び名であっても DDD の文脈では別の意味だったりします(「エンティティ」など)。 もともと知っている言葉に引きずられないようご注意ください。

ドメイン(Domain)

松岡さんの ドメイン駆動設計は何を解決しようとしているのか より:

業務アプリケーションに限って言うなれば、「アプリケーションの対象となる業務領域」とでも言うとわかりやすいでしょうか。

ドメインモデル(Domain model)

WEB+DB PRESS の特集記事より:

DDD ではドメインの概念のうち重要な知識を抽出した結果できあがる概念をドメインモデルと言います。

業務範囲の様々なことを抽象的に表す模型のようなものが「モデル」、その模型を抽出する作業が「モデリング」だと私は捉えました。

ドメインオブジェクト(Domain object)

ドメインモデル」の他に「ドメインオブジェクト」という言葉もよく目にします。 同じ意味で使われることも多いようですが、おそらく本来は、前者が先述のとおりの模型のようなもの、後者がその模型を実際のオブジェクト(クラス)として実装したものだろうと思います。

ドメインモデル貧血症(Anemic domain model)

増田本を読んでいると、「判断/加工/計算」といった処理をドメインオブジェクトに持たせることが何度も説かれていて、DDD の中でとても大事な考え方の部分なのだと伝わってきます。

ある部品が行うべき処理をその部品側でなくそれを使う側に書くと、同じ目的のコードが複数の箇所に重複して散在しやすくなります。 データを格納するだけのデータクラスを作るのではなく、そこにロジックも一緒に持たせることでそれを防ぐことができ、変更が容易なシステムになります。

ここで WEB+DB PRESS Vol.113 の特集記事の第5章にある一文をご紹介します。

すべてのコードが「あるべきところにある」というだけで、どれほどの開発者が幸福でいられるでしょうか。

何だかとても印象深い言葉で、まるで DDD のキャッチコピーのような名言です。

これに反するのが、ドメインオブジェクトにドメイン知識が欠乏している「ドメインモデル貧血症」と呼ばれる状態です。 名前が付くほどのアンチパターンですので、そうならないよう肝に銘じておきましょう。

利口な UI(Smart UI)

ユーザインタフェースビジネスロジックを持ちすぎてしまっているアンチパターンであり、ドメインモデル貧血症の原因になるものです。 UI とロジックの分離ができていないという DDD 以前の状態ですね。

Flutter でもそういう書き方ができてしまいます。 後で分けようと思って一時的にでも書いてしまうと悪化していくので、最初から注意したいところです。

ドメイン駆動設計(Domain-driven design)

「なるほど」と思える説明がまたまた WEB+DB PRESS Vol.113 の特集記事にありましたので引用しておきます。 その特集には他にも多数の重要な情報が詰まっていますが、ここに全て抜粋するわけにはいきません。 購入して特集全体を読まれることをお勧めします。

実を言うとDDDが求めることはとても単純です。ドメインに焦点を当て、対象を正しく理解し、表現すること。
(中略)
言ってしまえば当たり前のことを当たり前にやるためのプラクティスがDDDです。

「当たり前のことを当たり前にやる」、これも名言ですね。刺さりました。 本来は当たり前のことができていれば悩む必要がないのでしょう。 まず何が当たり前なのかわかっていなかったのかもしれません。

DDDはドメインと向き合い、分析から設計、そして開発が相互に影響し合うよう努力を重ねることでイテレーティブな成長を促す手法です。

単にコードの見通しを良くするような目的で取り入れるものではなく、プロダクトやビジネス自体にとって成長に繋がる手法だと捉えるのがいいですね。

DDDの目標はプロダクトの関係者が一丸となり、共通の認識を持って知識を深め、プロダクトを改良し続けることです。

ビジネスサイド・開発サイドと二分した考えでいると実現しにくいことだろうと思います。 「ビジネスには興味がない、技術が第一だ」と考えていると成し得ないわけです(耳が痛い…)。 開発者にとっては意識の転換が必要になりますね。

これらの引用の他、もう少し後に載せている松岡さんの「モデリングから利益を得るためのアプローチ」の図や引用元の記事もわかりやすいです。

コードの面では、オブジェクト指向の役割が大きいなと感じます。 そこの理解が浅いとご自分で思われる方は、まずそこを強化されると良いかと思います。

戦略的/戦術的設計、軽量 DDD

ドメイン駆動設計のメリットと始め方 ~ 1章「DDDへの誘い」 (1/3):CodeZine(コードジン)

こちらに書かれているのがわかりやすかったです。

戦略的設計(Strategic design patterns)

「チームで使うパターン」のことです。ビジネスにおける言語に価値を置き、業務に関わる人の考え方をドメインモデルとして表現します。

戦術的設計(Tactical design patterns)

「テクニカルなパターン」のことです。アーキテクチャ、DDD固有のパターンといった技術的な内容が含まれます。

軽量 DDD(DDD-Lite)

なお、「戦略的設計」を実施せず、エンジニアが取り組みやすい「戦術的設計」にだけ注力すると、「軽量DDD」と呼ばれる事業価値を発揮できない貧弱なDDDになってしまうため注意が必要です。

DDD を形だけ取り入れるのは不十分なんですね。 軽量 DDD から入ることを勧めている人が時々いますが、上記を理解すると危険だなと思います(が、取り組みやすくてある程度の改善も見込めるので絶対悪ではないのかもしれません…)。

ドメインモデリング

履歴書をモデリングするというちょっとした例が特集記事に書かれていてわかりやすかったのですが、「名前」「経歴」等のあらゆるものをモデルとして抽象化するのは無理があるし、「資格」の情報が抜けていれば採用の判断に不十分なものになってしまうということで、必要十分なものを取捨選択するのがモデリングでは大事なんだなと思いました。

他に、松岡さんの記事(ドメイン駆動設計は何を解決しようとしているのか)の「日報のモデリング」の例もわかりやすいです。

モデリングの意味

モデリングや DDD そのものの意味も 同じ記事 で解説されていて、そこに書かれているように「意図を組んだ上で取り組むことができるようになる」と思います。ぜひお読みください。

ここにはその記事からお借りした図を貼っておきます。

f:id:kabochapo:20191105230058p:plain
モデリングから利益を得るためのアプローチ(松岡さんの記事より)

ユースケース

ドメインモデリングのためには、まず「ユースケース図」を描いてモデリングの範囲を絞るのが良いようです。

f:id:kabochapo:20191105231354j:plain
ユーザ関連処理のユースケース図(ボトムアップドメイン駆動設計より)

ボトムアップドメイン駆動設計 のこの図とほぼ同じになるので脳内でしか描きませんでしたが、後で真似て描いてみました(PlantUML を使いました)。

f:id:kabochapo:20191108185254p:plain

こんな図で良いのでしょうか…。

そもそも「ユースケース」とは何なのか悩んだのですが、WEB+DB PRESS の特集記事によれば「ユーザーとアプリケーションが行える相互作用の種類」とのことでした。 上の図ではユーザが行うことばかりになっていますが、「相互」なのでユーザに向かう矢印もあり得るのかもしれません。

話を戻すと、このようにユーザとアプリの間で行われることを図にして、重要な部分を特にモデリングするように範囲を絞るために見るのがユースケース図だと思われます。

メモアプリのサンプルでは特に対象外にする部分はなさそうですが、もし「メモをバックアップする」という機能があれば、最初から含めるのはやめて一旦モデリングの対象外にするかもしれません。

ドメインモデル図

ドメインモデル図を描くことがモデリングの本格的な過程の部分だと思います。

ですが、ボトムアップドメイン駆動設計にはドメインモデル図は出てきませんでしたので、サンプル作成時にはスキップしました。 次の図は後で描いてみたものです。

f:id:kabochapo:20191105233649p:plain

松岡さん流の簡易的なクラス図のようなダイアグラムです。 「現場でDDD!のハンズオン、持ち帰ってやってみた」で次のように説明されています。

  • クラス図の簡易版のようなものを作成する
  • 属性は代表的なものだけで良い、メソッドはなくて良い
  • ドメイン知識(ルール、制約)をドメインモデルに吹き出しの形で表現する
  • 集約の境界を決定し、オブジェクトを囲む
    • 集約またぎの場合は、オブジェクト同士のhas-a関係ではなく、必ずIDの参照の形にする

これに倣ってルールを吹き出しに書いていますが、メモのほうの「存在しないカテゴリIDを指定できない」は実装していません。 図を後で描いたときに抜けていることに気づいたものです。 やはり先にこのような図にしたほうがいいなと実感できました。

さて、上の図では集約が二つあり、その中にオブジェクトが一つずつあります。 各集約にエンティティが一つと値オブジャクトがいくつかあるだけ(一つずつのエンティティがそのまま集約ルート)なので、シンプルすぎる図になりました(集約等の詳細は後述します)。

もっと複雑なアプリであれば、概念や用語が適切でなかったり含め忘れているオブジェクトやルールがあったりする可能性があり、このモデリングの過程で気づいて改善していくことが重要になるはずです。 また、集約の境界もそこで決めることになります。

ボトムアップドメイン駆動設計ではサークルのエンティティにユーザ(のID)のリストを持たせていますが、メモアプリではメモにカテゴリIDを持たせました。 リストを持たせるならドメインモデル図は変わってきます。そういった検討もこの段階でできると思います。

ディレクトリ構成

構成を考える上でアーキテクチャが関係してくるので、まずそれを見ていきます。

増田本では、業務アプリケーションは 三層アーキテクチャ が一般的だとしています。 また、三層+ドメインモデル にして、ドメインモデルに集めたロジックを三層が利用することが勧められています。

しかし、アーキテクチャには様々なものがあります。

複数のアーキテクチャ

ドメイン駆動設計で実装を始めるのに一番とっつきやすいアーキテクチャは何か[DDD]

またまた松岡さんの記事です。情報が豊富でありがたいですね。

アーキテクチャの図をどう見ればいいのか最初はわからなかったのですが、どのアーキテクチャにも層があり、層が依存する方向が図で示されているのだと理解しました。

上記四つのうちでは オニオンアーキテクチャ が松岡さんオススメのものです。 上下に重ねた図になっていて、丸い玉ねぎ状よりわかりやすいです。

f:id:kabochapo:20191108175556p:plain
オニオンアーキテクチャ(松岡さんの記事より)

書かれているとおり「上の層から下の層」という依存方向なので、アプリケーション層に書かれたものをドメインモデルが使うのはダメだけれど逆は OK ということがわかります。

依存関係逆転の原則(DIP

SOLID 原則の一つで、これがとても大事だそうです。

先ほどの図でもこの原則が踏まえられているようですが、より明確になるよう改良された図が松岡さんの別の記事(新卒にも伝わるドメイン駆動設計のアーキテクチャ説明[DDD])にありました。下図が転載したものです。

f:id:kabochapo:20191108175657p:plain
オニオンアーキテクチャ(松岡さんの記事より)

インフラストラクチャ層はデータの永続化などを行う層で、レイヤードアーキテクチャでは最下層に来るものです。 その位置を逆転して上に持っていった図になります。

永続化にはデータベースなどの外部のツールが必要になりますが、その処理を担当するインフラストラクチャ層が下にあればドメイン層がそこに依存してしまうことになります。 そうすると、プラットフォームへの依存も起こってきます。

DDD ではドメインを他に依存させない考えがあるのでドメインが最下層になることが重要であり、そのために永続化処理のインタフェースだけを下層(ドメイン層)、実装を上層(インフラストラクチャ層)に置くことになります。 実装は差し替えることができるので、テストにおいても大きな意味を成します。

これはオブジェクト指向だからこそ実現できるものですね。 理解したときは少し感動しました。

アーキテクチャについてもう少しだけ…

先ほどの図で変更された点について次のように書かれています。

  • ApplicationServiceという呼称をUseCaseに変更した
  • Infra層からDomain層への依存を実装の矢印で表現した
  • Domain層をModelとServiceに分けるのはやめて一つにした
  • 登場要素をマッピングした

アプリケーション層ユースケース層 という呼称に変わっています。 「ユースケース」と言えば「ユースケース図」を先ほど描きましたね。 あのユースケース図に載せた処理を扱う層だと言えそうです。

また、「ModelとServiceに分けるのはやめて一つに」ということで、結局「三層+ドメインモデル」に近い四つの層になっています。 増田本ではインフラストラクチャ層ではなくデータソース層となっていますが、これもほぼ同じものだと思います。

ようやくディレクトリ構成

というわけで、今見た層をディレクトリ分けに使えば良さそうです。

その前に、ボトムアップドメイン駆動設計のスライド にある構成例を参考に見てみましょう。

f:id:kabochapo:20191108180644p:plain
ディレクトリ構成例(ボトムアップドメイン駆動設計より)

この中で次の二点が少し気になりました。

  • Application が Domain の下にある
  • Domain の下に Model があるが、GitHub の実際のコード を見ると Model ではなく Domain であり、Domain の下にまた Domain という構成になっている

この二点について自分でアレンジし、presentationapplicationdomaininfrastructure の四つが lib の中で並列になるようにしました。

f:id:kabochapo:20200316170848p:plain

他にも考慮した点がありますので書いておきます。

  • application
    ボトムアップドメイン駆動設計」の記事をベースにしたので「application」にしました。 先ほどの図のように「usecase」という名前にするのも良いと思います。 「service」はドメインサービスと紛らわしいので避けました(ドメインサービスについては 実装編 を参照ください)。
  • application/dto
    ボトムアップドメイン駆動設計のコードには Application の下に Models があって DTO のファイルが入っています。 それを model と呼ぶのはどうかと思い、そのまま「dto」としました。
  • presentation/notifier
    MVVM に照らすと ViewModel に該当するので、それを短く表して「model」としました(プレゼンテーション層なのでドメインモデルと混同することはないでしょう)。
    わかりやすく「notifier」に変更しました。 ディレクトリ名もクラス名も変えています。
  • test
    ボトムアップドメイン駆動設計の構成例には永続化処理のテストのための InMemory がありますが、Flutter にはテスト用ディレクトリが用意されているのでそれを使います。
  • common
    依存を無視して共通処理のディレクトリを作っていいのか迷いましたが、Exception のクラスはアプリケーション層からもドメイン層からも使うので作りました。
  • lib/src
    Dart には lib の下に src というディレクトリを作る(他のライブラリから読み込まない private なコードをそこに置く)という慣習がありますが、パッケージではなく一つのアプリを作るときには必要性を感じないので lib 直下に配置しました。 なお、Flutter チームのサンプルアプリでも必ずしも src があるわけではありません。
  • 単数形
    dtopage などはその中に複数の DTO や複数ページ用のファイルがあるので複数形にしたかったのですが、dtos という複数形は読みにくいのでやめました。

パッケージ?

先ほどの「ボトムアップドメイン駆動設計」のディレクトリ構成例ではパッケージやフォルダが色分けされています。 その記事のコードは C# なのでパッケージ(namespace)として分割されています。

これを Dart/Flutter ではどう扱えば良いでしょうか…。

「パッケージ」は Dart にもありますが、C# の namespace とは異なるものです。

他に「ライブラリ」という概念もあります。 基本的には一つのファイルが一つのライブラリですが、librarypartpart of というキーワードを使って複数のファイルに分けることもできるので、これを namespace のような括りに使えそうです。

ただ、問題が二点あると思いました。

  • 分割したファイル分の part の記述が必要
    ライブラリの中にファイルが増えるたびに追記していかないといけなくて手間です。
  • キーワードの可視範囲が広くなりすぎる
    Dart ではメソッド等の名前をアンダースコアで始めると不可視になりますが、これは library private(クラスの外から見えないのではなく他のライブラリから見えない)です。 一つの層を一つのライブラリにしてしまうと、アンダースコアを付けたものが同じ層のどこからも見えてしまうことになります。

そこで、単にディレクトリとして分けるだけにしました。

疑問、懸念

  • 層なのに並列にディレクトリを分けただけでいいの?
  • リポジトリのインタフェースをドメイン層、実装をインフラ層に置くというのは、どこに置くかというルール決めに過ぎないのでは?

何がどこにあるという取り決めをしておくことでわかりやすくなり、それでメリットがあるのだから OK と受け取っておけば良いのでしょうか…。 実装をドメイン層に置いたり上の層に依存しようとしたりするとエラーになる、といった言語や IDE による制約や支援がないと、開発チーム内で共通認識しておくルールでしかなくなってしまい、人為的に誤った記述をしてしまうのを防げないように思えます。

パッケージに分けて、どのパッケージからは見えるという指定まで可能な言語だとやりやすそうです(Scala のアクセス限定子は可能?)。 Dart の言語自体には残念ながらそういう機能はありませんが、package:meta が提供するアノテーションの中に役立つものがあるかもしれません。

Flutter + DDD で何が変わるのか

ChangeNotifier を使うパターンの場合、よくある形をシンプルに描くと次のような図になると思います。 BLoC パターンでも ChangeNotifier の部分が BLoC に置き換わる感じです。

f:id:kabochapo:20191118235128p:plain

ChangeNotifier や BLoC の部分にビジネスロジックが来ますが、そこの設計は自由で、あまり考えずに書くと少数の巨大なクラスにしてしまうこともあり得ます。

一方 DDD &オニオンアーキテクチャを採用すると、次のように細かく設計することになります(矢印は依存の方向)。 この場合、ChangeNotifier や BLoC は、UI から Application 以下の層への入り口のようなものになると考えています。

f:id:kabochapo:20191118235156p:plain

クラスの数が多くなって(一見)複雑になったり、設計にコストがかかったりするので、アプリの規模、開発メンバー数/スキル、予算等によって検討したほうがいいですね。 後で改修が難しくなって余計にコストがかかることや、不具合の修正が困難になって困ることを考えると、そうなりにくい DDD のほうがメリットが大きいように個人的には思います。

注意

ボトムアップドメイン駆動設計 に次のように書かれています。

ボトムアップドメイン駆動設計で実践してきたドメイン駆動設計は形を踏襲しただけの紛い物です。
原典では個々のパターンの繋がりが想像しづらいので、それらの繋がりがわかるように動くものを用意したに過ぎません。

ドメイン駆動設計のマインドは原典を読んで感じ取ってください。

後編の最後に書かれている部分です。 ここまでしっかりと読まないまま参考にしていて、後で読んで「え、そうなの…?」と思いましたが、DDD の意味などの理解が深まってくると確かにそうだなと感じます。

上の図で表しているのも形だけであり、その形だけに捕らわれると「軽量 DDD」になってしまいますね。 単に「この形に似せて作るものなのだ」と受け取るのではなく、ドメインをよく理解して何のためにどうしたいのかを考えて設計しましょう。

実装編に続きます…

一つの記事にしようと思いましたが、合わせると一冊の薄い本になりそうな分量になってしまいました。 ここで一度区切りをつけ、続きは別の記事にしようと思います。

kabochapo.hateblo.jp