はじめに
本投稿は、Object Design Style Guideの要約記事です。著者に承諾を得た上で投稿しております。
という方にオススメの書籍です。
追記:ご本人からツイート頂きました。
https://twitter.com/matthiasnoback/status/1482987571944865795
著者
Matthias Noback。
2011年以降、matthiasnoback.nlにて、プログラミングに関するあらゆるトピックについてブログを書いている。Matthiasの他の著書に、A Year with Symfony (Leanpub, 2013)、Microservices for Everyone (Leanpub, 2017)、Principles of Package Design (Apress, 2018)がある。
序文(by Ross Tuck)の概要
『The AP Stylebook』や『The Chicago Manual of Style』など、ジャーナリストがよく使うスタイルガイドをご存知の方も多い。この本は、それらの本と同様に、より大きなチームの中で明確で一貫したトーンを達成するためのガイドラインと手引きを提供している。
Matthiasが行ったのは、ベストプラクティスを集めて、まとまりのある、認識しやすいスタイルにすることだった。
その意図は読者を束縛することではなく、参照点を提供することにあると考えている。イノベーションは制約の中で起こる、と言われている。このスタイルを反復し、改良し、ここから始めるのがよいだろう。ただし、個々の状況はユニークであるため、あなた自身のものにしなければならない。
対象読者
本書は、少なくともオブジェクト指向プログラミング言語の基本的な知識を持つプログラマーを対象としている。クラス設計に関する言語の可能性を理解している必要がある。
構成・対象範囲
第1章
第2章
第3章
第4章
第5章
第6章
第7章
第8章
第9章
第10章
第11章
全体像
こちらは全体像の把握がしやすいよう、私で作成したマインドマップです。このままでは見づらいかと思いますので、学習に使用する際はこちらをコピーし、ノードを開閉して見るとよいかと思います。
各章の要約
第1章
オブジェクトは、与えられたクラスに基づいてインスタンス化されます。
クラスは、プロパティ、定数、およびメソッドを定義します。
プライベートなプロパティやメソッドは、同じクラスのインスタンスからしかアクセスできません。パブリックなプロパティとメソッドは、オブジェクトのクライアントなら誰でもアクセスできます。
オブジェクトは、そのすべてのプロパティが変更できず、それらのプロパティに含まれるすべてのオブジェクトがそれ自体不変である場合、不変です。
依存関係は、その場で作成することも、既知の場所から取得することも、コンストラクタの引数として注入することもできます(これを依存関係の注入と呼びます)。
継承を使用すると、親クラスの特定のメソッドの実装をオーバーライドすることができます。インターフェイスでは、メソッドを宣言しても、その実装をインターフェイスを実装するクラスに完全に任せることができます。
ポリモーフィズムとは、オブジェクトの型(通常はインターフェース)によって定義されたメソッドをコードで使用できるが、実行時の動作はクライアントから提供された特定のインスタンスによって異なることを意味する。
オブジェクトがそのプロパティに他のオブジェクトを割り当てることをコンポジションと呼びます。
ユニットテストでは、オブジェクトの動作を指定して検証します。
テストを行う際には、オブジェクトの実際の依存関係をテストダブレットと呼ばれるスタンドイン(スタブやモックなど)で置き換えることができます。
動的配列は、キーや値の型を指定せずにリストやマップを定義するために使用されます。
第2章
サービスは、すべての依存関係と設定値をコンストラクタの引数として与え、一度に作成する必要があります。すべてのサービスの依存関係は明示的であり、オブジェクトとして注入されるべきです。すべての設定値は検証されるべきです。コンストラクタが何らかの形で無効な引数を受け取った場合は、例外を投げる必要があります。
コンストラクション後、サービスは不変でなければなりません。サービスのメソッドを呼び出しても、その動作は変更できません。
アプリケーションのすべてのサービスを統合すると、大きな不変のオブジェクトグラフが形成され、多くの場合、サービスコンテナによって管理されます。コントローラは、このグラフのエントリーポイントです。サービスは一度インスタンス化すると何度も再利用できます。
第3章
サービスオブジェクトではないオブジェクトは、依存関係ではなく、値または値のオブジェクトを受け取ります。オブジェクトの構築時には、一貫した動作を行うために必要最小限のデータを提供する必要があります。提供されたコンストラクタの引数が何らかの形で無効である場合、コンストラクタはそれに関する例外を投げるべきです。
プリミティブ型の引数を(値)オブジェクトで囲むと便利です。これにより、これらの値に対する検証ルールの再利用が容易になります。また、値の型(クラス)にドメイン固有の名前を指定することで、コードに意味を持たせることができます。
サービスではないオブジェクトの場合、コンストラクタは静的メソッドにすべきで、名前付きコンストラクタとしても知られています。
コンストラクタには、ユニットテストで指定されたとおりにオブジェクトを動作させるために必要な以上のデータを与えてはいけません。
これらのルールが適用されないオブジェクトのタイプとして、データ転送オブジェクトがあります。DTOは、外部から提供されたデータを運ぶために使用され、その内部をすべて公開します。
第4章
作成後に変更できない不変的なオブジェクトを常に好む。オブジェクトに何か変更を加えたい場合は、まずコピーを作成してから変更を加えます。これを行うメソッドには宣言的な名前を付け、単にプロパティを新しい値に変更できるようにするのではなく、便利な動作を実装する機会を得ましょう。モディファイアのメソッドが呼び出された後、オブジェクトが有効な状態であることを確認してください。そのためには、正しいデータだけを受け取り、オブジェクトが不正な状態遷移をしないようにしてください。
エンティティのような変更可能なオブジェクトでは、モディファイア・メソッドの戻り値の型はvoidにすべきです。このようなオブジェクトで起こる変化は、内部で記録されたイベントを分析することで明らかになります。不変オブジェクトとは対照的に、ミュータブルオブジェクトは流暢なインターフェイスを持つべきではありません。
第5章
メソッドを実装するためのテンプレートは、作業を始める前にテーブルをクリアにすることを目的としています。まず、提供された引数を分析し、間違っていると思われるものは例外をスローすることで拒否します。その後、実際の作業を行い、失敗した場合に対処します。最後に、クライアントに値を返すことができるように、まとめを行います。
InvalidArgumentExceptionsは、クライアントが提供した引数に問題があることを知らせるために使用する必要があります。RuntimeExceptionsは、論理的なミスではない問題が発生したことをクライアントに知らせるために使用します。
カスタム例外クラスと名前付きコンストラクタを定義することで、例外メッセージの品質を向上させ、例外の作成とスローを容易にします。
第6章
クエリメソッドは、情報を取得するために使用できるメソッドです。クエリメソッドは、単一の戻り値の型を持つ必要があります。NULL を返すこともできますが、NULL オブジェクトや空のリストなどの代替手段を探すようにしてください。代わりに例外を投げることもできます。クエリメソッドは、オブジェクトの内部をできるだけ見せないようにしましょう。
質問したいことや得たい答えに応じて、特定のメソッドや戻り値を定義してください。質問に対する答えがシステムの境界を越えなければ得られない場合は、これらのメソッドの抽象化(実装の詳細を含まないインターフェース)を定義します。
情報を取得するためにクエリを使用するサービスをテストする際には、自分で書いた偽物やスタブで置き換えて、実際の呼び出しをテストしないようにしてください。
第7章
タスクを実行するためには、コマンド・メソッドを使用する必要があります。これらのコマンド・メソッドは、命令的な名前(「Do this」、「Do that」)を持ち、範囲を限定する必要があります。メインのジョブと、そのジョブの影響を区別する。他のサービスに追加のタスクを実行させるためにイベントをディスパッチします。コマンドメソッドは、タスクの実行中に、クエリメソッドを呼び出して必要な情報を収集することもできます。
サービスは、内部だけでなく外部からも不変でなければなりません。データを取得するサービスと同様に、タスクを実行するサービスは何度でも再利用できるべきです。タスクの実行中に何か問題が発生した場合は、(それがわかった時点で)例外を投げること。
システムの境界を越えるコマンド(リモートのサービスやデータベースなどにアクセスするコマンド)の抽象化を定義します。コマンド・メソッドを呼び出すコマンド・メソッドをテストする場合は、モックやスパイを使ってこれらのメソッドの呼び出しをテストすることができます。これには、モックツールを使用するか、独自のスパイを作成することができます。
第8章
ドメイン・オブジェクトでは、書き込みモデルと読み取りモデルを分けましょう。エンティティからのデータを必要としているだけのクライアントは、状態を変更するためのメソッドを公開しているエンティティではなく、専用のオブジェクトを使用する必要があります。
読み取りモデルは、書き込みモデルから直接作成することもできますが、より効率的な方法は、書き込みモデルが使用するデータソースから作成することです。それが不可能な場合や、効率的な方法で読み取りモデルを作成できない場合は、ドメインイベントを使用して、時間をかけて読み取りモデルを構築することを検討する。
第9章
サービスの動作を変更する必要がある場合は、コンストラクタの引数で動作を設定できるようにする方法を探します。大規模なロジックを置き換えたいためにこの方法が使えない場合は、コンストラクタの引数として渡される依存関係を交換する方法を探します。
変更したい動作がまだ依存関係で表現されていない場合は、抽象化(上位概念やインターフェース)を導入することで依存関係を抽出します。そうすれば、変更するのではなく、置き換えることができる部分が出てきます。抽象化することで、動作を合成したり装飾したりすることができるので、最初のサービスに知られずに(あるいはそれに合わせて変更されずに)、より複雑な動作をすることができます。
継承を利用して、サービスのメソッドをオーバーライドして動作を変更するのはやめましょう。常にオブジェクトコンポジションを使ったソリューションを探しましょう。クラスのパブリックインターフェースに含まれていない限り、すべてのプロパティとメソッドをプライベートにしてください。
第10章
アプリケーションのフロントコントローラは、受信したリクエストをコントローラのいずれかに転送します。これらのコントローラはアプリケーションのインフラストラクチャ層の一部であり、入力されたデータをアプリケーション層の一部であるアプリケーションサービスやリードモデルのリポジトリへの呼び出しに変換する方法を知っています。
アプリケーションサービスは、配信メカニズムに依存せず、ウェブアプリケーションやコンソールアプリケーションでも同じように使用できます。アプリケーションサービスは、アプリケーションのユースケースの1つと考えられる、1つのタスクを実行します。その過程で、書き込みモデルのリポジトリからエンティティを取得し、メソッドを呼び出し、その変更された状態を保存することがあります。エンティティ自体は、その値オブジェクトも含めて、ドメイン層の一部です。
読み取りモデルリポジトリは、情報を取得するために使用することができるサービスです。リポジトリは、ユースケースに固有で、必要な情報をすべて提供し、それ以上の情報を提供しない読み取りモデルを返します。
本章で説明したオブジェクトの種類は、当然ながらレイヤーに属します。コードが下位レイヤーのコードにのみ依存するレイヤリングシステムは、ドメインやアプリケーションのコードをアプリケーションのインフラ面から切り離す方法を提供します。
第11章
おわりに
本記事は要約のみの紹介ですが、書籍では具体的なコードを交えた解説が多数あります。
ベストプラクティスの"なぜ"が詰まった良書であると思いますので、興味を持たれた方は是非原書をご一読ください。