little hands' lab

ドメイン駆動設計を布教したい

ドメイン駆動設計を導入するために転職して最初の3ヶ月でやったこと[DDD]

この記事は ドメイン駆動設計 Advent Calendarの記事です。

今年の9月にログラスというスタートアップに転職しました。

ログラスは元々DDDについて講師として勉強会をさせてもらっていた会社であり、DDD自体は社として取り組んでおりある程度進んでいました。ですが、講師ではなく中の人になったからこそできる色々な取り組みがあり、3ヶ月である程度形になりました。

本記事では、DDDを広めるための取り組みについて、極力再現性がある形を意識しつつ、ご紹介したいと思います。

入社時の状況

まず、取り組みを始める前の入社時の状況について説明します。

実装について

DDDには取り組んでいて、ドメイン層のオブジェクトである程度知識を表現できていました。 しかし、複雑な処理はユースケース層(アプリケーション層)のServiceクラスが担っており、Serviceクラスの肥大化や、Serviceクラス同士の参照で呼び出しが深くなっているなどの改善余地がありました。

ドメインオブジェクトにはテストがありましたが、Serviceクラスはテストを書くのが大変でテストがあまり書かれていませんでした。

モデリングについて

概念モデルを作成してそれを元に実装していましたが、コードからは少し乖離しており初見でのキャッチアップにはハードルがありました。

なにをしたか

まず、やったことを列挙します。

  • TDD Boot Campの@t_wadaさんの基調講演観賞会を行った
  • Serviceクラスを1パブリックメソッドのみにし、リネームした
  • レイヤーごとのオブジェクトの依存関係を整理した
  • 参照実装を作成した
  • 「責務」と「テスト」の重要性を説き続けた
  • ドメインモデル図を作成してそれを元にした開発を行った

上にあるものから比較的簡単に実行でき、効果の即効性があるものです。下に行くほど、遅効性ですがDDDと言う観点では本質的な内容になります。 大体この順序で進めました。

テストの話が多い理由

「DDDの話なのにテストの話が半分をしめてる?」と思ったかもしれません。はい、意図的なものです。
それくらいテストは大事なもので、テストとDDDは切っても切り離せません。

なぜか?

ドメイン駆動設計では、モデリングをし、コードに落としながら新しい発見があれば随時モデルをアップデートし、コードにも反映していきます。このサイクルは小さく、頻繁なほど良いです。
そのため、頻繁にドメイン層のコードを修正したいですが、その際に自動テストで守られなければバグを埋め込んでしまうのが怖くて修正できなくなります。つまり、 テストがなければモデルとコードを改善していくことができない のです。

そのため、ドメイン駆動設計を広める上でテストを広めることは必ずセットになります。


実施内容詳細

以下、実施内容を詳細に説明していきます。

TDD Boot Campの@t_wadaさんの基調講演観賞会を行った

テスト自体に関しての認識、スキルは、一つのチームでもかなりバラバラなものです。 そのため、「TDDとは、良いテストとは」といったことについて圧倒的にまとまっている以下の基調講演の観賞会を行いました。

TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング - YouTube

これまで在籍したチームで「テストコードをみんなで書き始めよう」としたことは何度かありましたが、"テストにきちんとテスト観点を書く、テスト容易性と重要性を意識する"などの共通言語ができたため、初速が圧倒的に速かったです。テストを書いていこう!としているチームには是非お勧めします。

Serviceクラスを1パブリックメソッドにした

次に、入社直後ですから、変に意識が高い高尚なことをいうより、明確に困っているところに、分かりやすく改善効果を出すことを目指します。そして、最初に目をつけたのがServiceクラスでした。

冒頭に書いた通り、ユースケース層(アプリケーション層)のServiceクラスが肥大化し、それ故にテストを書くのが難しくなり、結果としてテストが書かれないという結果になっていました。
そのため、Serviceクラスを1パブリックメソッドにすることにより、テスト容易性を高めました。

1Serviceに複数のパブリックメソッドがあると、Serviceクラスが依存するクラスが増えます。各パブリックメソッドの最大公約数的に増えていくからです。

f:id:little_hands:20201218055306p:plain

しかし、それぞれのパブリックメソッドを見ると、実は一部しか使用していないことがほとんどです。

f:id:little_hands:20201218060656p:plain

ここで、1メソッドの単体テストを書くとき、「どのクラスをモックしたらいいのかが簡単に判別できない」という問題が発生します。

f:id:little_hands:20201218060707p:plain

この関係が、「1クラス1パブリックメソッド」になるとどうなるでしょうか?

f:id:little_hands:20201218055548p:plain

なんと、圧倒的にシンプルに!!
これにより、テストがしやすくなりますし、クラス自体の依存クラスから処理の概要を推測しやすくなります。

また、パブリックメソッドが複数あるデメリットとして、プライベートメソッドとの対応が見づらくなります。これも、1パブリックメソッドに限定することにより、圧倒的にシンプルになります。

f:id:little_hands:20201218061246p:plain

以上により、複雑でテストを書くのを断念していたクラスに、単体テストを書くようになりました。 *1

ここで 「テストがあると安心!」という感覚をチームに浸透させていくことが非常に大切 です。 「テストを書かないと怒られる」ではなく「テスト書きたい!書かせて!」となれば自然にテストが増えていきます。

客観的な指標として、ユースケース層のテストケースが3ヶ月で爆増しました。

f:id:little_hands:20201218184136p:plain

また、文化的な洗脳・・・布教にも成功しました。笑

f:id:little_hands:20201218061029p:plain

レイヤーごとのオブジェクトの依存関係を整理

Serviceクラス同士が呼び合ってしまうと、複雑な処理では1つのServiceから複数のServiceを呼び出していく階層が深まり、コードを読む際に全体像を追うのが難しくなります。

f:id:little_hands:20201218063746p:plain

そのため、上記の赤色のようなServiceクラス同士の呼び合いを禁止し、以下のような構成として整理しました。

f:id:little_hands:20201218063834p:plain

まず、controllerから呼ばれるのはUseCaseという名前のクラスで統一します。UseCaseクラスは、ドメイン層で公開されているメソッドを組み合わせて ユースケースを組み立てるクラス です。UseCaseクラスからは、他のUseCaseクラスを参照することは許可しません。

前述の「1クラス1パブリックメソッド」としたServiceクラスの多くはこのクラスにリネームされます。一方、複数のユースケースから呼ばれる処理や、処理として独立していて単体でテストを書きたいような処理は、その処理を行うクラスとして独立させます。そして、Serviceという抽象的な名前ではなく、責務にあった名前をつけます。

何らかのデータを取得する処理を持つクラスであればXxxFetcher、変換する処理を持つクラスであればXxxConverterなどです。

レイヤーごとのテスト方針

各レイヤーの依存関係を整理したところで、各レイヤーのテスト方針を整理しました。

ドメイン層のエンティティ、値オブジェクトは、基本的に単体テストを行います。DIなどは行わないため、素直に単体テストを書くことができます。*2

ユースケース層のUseCaseクラス、用途ごとの独立クラス(Fetcherなど)は「まずは単体テストを検討し、必要であれば結合テスト」という方針にしました。 前述の通り、UseCaseクラスなどは1パブリックメソッドになっており、テストが書きやすくなっているはずです。依存クラスをうまく整理できていれば、それらをモックして「ユースケースを組み立てられていること」だけアサーションすれば十分なことが多いです。更新系でリポジトリでエンティティがsaveされるようなクラスは、モックライブラリでsaveメソッドに渡される値をキャプチャしてアサーションします。

結合テストが必要になるのは、複雑な処理でモックするクラスが多くなってくる場合などです。モッキングのコードが増えるとそれ自体でバグが生まれる可能性が高まるため、実クラスをDIさせ、DBなどまで含めて結合テストします。

インフラ層では、リポジトリの実装クラスに対するテストを書きます*3 。ここではDBまで含めた結合テストを行います。リポジトリでDBとの入出力をしっかりテストして置けると、UseCaseクラスからは非常に安心して呼び出しできるからです。

クラス名の重要性

ここで、1パブリックメソッドになったServiceクラスを、UseCaseクラスとリネームしたのには理由があります。
それは、クラスの責務を明確に意識できるようにするため です。

責務とは、「そのクラスが何を表すか/何を行うか」です。

UseCaseクラスは、「ドメイン層で公開されているメソッドを組み合わせてユースケースを組み立てるクラス」です。このことが、名前から推測しやすいのがポイントです。

一方、「Service」というクラス名からは、「なんらかの処理をするんだろうな」ということしか推測できません。結果、責務の認識がぶれて、ついごちゃごちゃしたクラスになってしまいます。

責務、責務と折に触れて唱え続けることが重要です。(これについては後半でも言及します。)

参照実装を作成した

ここまではボトムアップの改善でしたが、並行して「DDDの実装はこんな感じ」という参照実装を作成します。ようやくDDDっぽい話になってきましたね?

「エンティティとは、リポジトリとは、集約とは・・・」と定義の話だけを延々とするより、コードを見るのが一番早いです。 抽象的な原理を解釈して具体化するより、具体的な事例をもとに横展開する方が難易度が低いのです。

そのため、まずはコントローラーからユースケース、エンティティ、リポジトリまでと、対応するテストを一通り書きます。テストまで書くのもポイントです。テスト自体もお手本があると横展開してもらいやすいからです。

既存のコードにDDDの実装パターンに基づいたリファクタを加える際に、都度このコードを参照しながら説明することで、理解を助けます。

DDDの実装パターンとはどういうものか、ということを書き出すとそれだけで数記事できてしまうので、本記事ではここまでの説明とします。(ようやくDDDの話っぽくなってきたのに!笑)

「責務」と「テスト」の重要性を説き続けた

これまでの説明でも繰り返し出てきましたが、「責務!」「テスト!」とことあるごとに言い続けます。

原則を伝えることは非常に重要です。 各ケースでここのバラバラなプラクティスを伝えるのだと、ちょっと違うケースに当たったら応用が効かなくなってしまいます。そのため、「個別の事象におけるプラクティス + 背景にある原則」という形で伝えることが重要です。

そして、重要性を解くポイントは 「最初に重要な方針として説明する」ではなく「言い続ける」 ことです。

責務とテストの重要性については、言われて一発で理解するものではありません。頭でわかって、体験して初めて実感するものなのです。

そのため、必要性を実感しやすいようなタイミングで、ことあるごとに強調します。

  • 責務の重要性
    • ペアプロ/モブプロをしながら、レビューのコメントの根拠として
    • リファクタリングの指針として
    • 責務過剰でfatなクラスを読み、可読性を下げている原因として
  • テストの重要性
    • 機能拡張時にテストがあって助けられた時
    • api経由だと動作確認が大変な時
    • 複数データパターンのテストを書きながら(ParameterizeTestを書くと圧倒的に楽だぞ!と)

鬱陶しいぐらい言います。slackのemojiも作ります。

f:id:little_hands:20201218190641p:plain

f:id:little_hands:20201218190621p:plain

何個かは僕が登録しましたが、「テスト書かせて!」「テスト欲しい!」は僕じゃないですよ、本当です。笑

slackのemojiはふざけているようで、リアクションで繰り返し使われるので文化の浸透には結構効果があると思います。

このようないろんなプロセスを経て、チームの設計議論に責務の話が自然に出てきて、全員がちゃんとテスト書かないと気持ち悪く感じるようになったら成功です。笑

ドメインモデル図を作成してそれを元にした開発を行った

最後にしてようやくドメイン駆動っぽくなってきました。

実物なので文字が読める解像度では載せられませんが、このようなものです。

f:id:little_hands:20201218074659p:plain

このモデル図は、以下のような用途で利用します

  • 曖昧なルール/制約/仕様について議論して、発見したことを記載する
  • よりよいモデルの単位や名前について議論して、更新する
  • ここに書かれている知識をもとに、実装に落とす

モデル図は初期の設計フェーズで作って終わりではなく、随時チームの理解を深めながら更新していきます。発見した内容はwikiなどではなく、ここに集約します。

この モデル図の角丸四角と、クラスの単位は完全に一致しています。  そのため、実装している上で「これはどこに書けばいいんだ?」と迷った時に地図のような役割を果たします。

重要なのは「モデル図があると便利じゃん!」と思ってもらうことです。
この図を見ると、結構複雑なモデルになっていることが伝わると思いますが、この地図がなければ何度も迷子になっていたと思います。

テストと同じで、「モデリングしないと怒られる」とかではなく、「モデリングがあると仕様整理や実装楽じゃん!」と言う体感を持ってもらうのが重要です。

まとめ

以上、転職してから3ヶ月の間にやったことをまとめました。 テストコードの量などからも、一定の成果が上がってきたと言えそうです。

大切にしていることは「重要な原則を何度も繰り返すこと」「義務感ではなく、必要性を感じて取り組んでもらうこと」です。
本記事が何かの参考になれば幸いです。

本記事を読んでいただき、何か聞いてみたいことがあった場合、「質問箱」というサービスを通じて質問を受け付けていますので、よろしければご利用ください。

松岡@ログラス/DDD,アジャイル (@little_hand_s) | Twitter
(プロフィールをご参照ください) 

また、ログラス社はこのような開発環境で一緒に開発したい!というエンジニアを募集中です。よろしければご応募ください。

求人詳細はこちら

実装パターンなどについての解説

本記事で飛ばしてしまった実装パターンなどについては、こちらを読んでいただけるとわかりやすいと思います。

little-hands.booth.pm

初めてDDDを学ぶ方、もしくは実際に着手して難しさにぶつかっている方向けの書籍です。 迷子になりがちな「DDDの目的」や「モデル」の解説からはじめ、具体的なモデリングを行い実装まで落とす事例を元に、DDDの魅力や効果を体感することを目指します。もしよろしければお求めください。

*1:なお、「1クラス1パブリックメソッド」は、ユースケース層のようなDIで複数クラスに依存するクラスの方針であり、エンティティや値オブジェクトは対象外です。

*2:ファクトリー、ドメインサービスといったクラスはDIを行いますが、本記事では言及しません

*3:リポジトリはインターフェイスがドメイン層、実装クラスがインフラ層です