はじめに
最近、「オブジェクト設計スタイルガイド」を読んでいるので、本書の中で自分が疑問に感じたポイントをQ&A形式で振り返ってみたいと思います。
この記事では、各セクションの簡単な説明の後に、[Q]で疑問に感じたこと、[A]で調べてわかったこと、そして[NOTE]で覚えておきたいことをまとめています。
なお、サンプルコードにPHPを使用していますが、言語を問わず広く有効な設計手法なので、他言語でも応用できる内容になっています。
それでは、1〜3章を振り返りながら、一緒に「オブジェクト設計」について学んでいきましょう!
すべての依存関係を明示する
通常サービスは、仕事をするためにほかのサービスを必要とします。
これらのサービスは依存関係であり、コンストラクタ引数として注入されるべきです。
スタティックな依存関係をオブジェクトの依存関係に変換する
アプリケーションによっては、スタティックメソッドを使うことで、依存関係をグローバルに取得できます。
サービスがこのように依存関係を取得するすべての箇所を、コンストラクタ引数として依存関係を受け取るようにサービスを書き直しましょう。
final class DashboardController
{
+ private Cache $cache;
+
+ public function __construct(Cache $cache)
+ {
+ $this->cache = $cache;
+ }
+
public function execute(): Response
{
$recentPosts = [];
- if (Cache::has('recent_posts')) {
- $recentPosts = Cache::get('recent_posts');
+ if ($this->cache->has('recent_posts')) {
+ $recentPosts = $this->cache->get('recent_posts');
}
// ...
}
}
複雑な関数をオブジェクトの依存関係にする
時には依存関係がオブジェクトではなく関数であるために隠れていることがあります。
そういった関数は、json_encode()
や simplexml_load_file()
のように、その言語の標準ライブラリの一部であることが多いです。
関数呼び出しをラップするカスタムクラスを導入することで、通常そうであるような隠れた依存関係ではなく、サービスの真のオブジェクトの依存関係にできます。
ラッパクラスは、標準ライブラリ関数にカスタムロジックを追加するための格好の場所です。
たとえば、デフォルト引数を提供したり、エラー処理を改善したりできます。
+ final class JsonEncoder {
+ /**
+ * @throws RuntimeException
+ */
+ public function encode(array $data): string
+ {
+ try {
+ return json_encode(
+ $data,
+ JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT
+ );
+ } catch (JsonException $previous) {
+ throw new RuntimeException(
+ 'Failed to encode data: ' . var_export($data, true),
+ 0,
+ $previous
+ );
+ }
+ }
+ }
+
final class ResponseFactory
{
+ private JsonEncoder $jsonEncoder;
+
+ public function __construct(JsonEncoder $jsonEncoder)
+ {
+ $this->jsonEncoder = $jsonEncoder;
+ }
+
public function createApiResponse($data): Response
{
return new Response(
- json_encode($data, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT),
+ $this->jsonEncoder->encode($data),
[
'Content-Type' => 'application/json'
]
);
}
}
システムコールを明示する
言語が提供する関数やクラスの一部も、暗黙的な依存関係とみなすことができます。
それは外の世界にアクセスする関数です。
その例として DateTime
クラスや time()
、file_get_contents()
などの関数が挙げられます。
メソッド引数として現在時刻を渡すことで、依存関係を明示化しましょう。
final class MeetupRepository
{
public function __construct(/* ... */)
{
// ...
}
public function findUpcomingMeetups(
string $area,
+ DateTime $now
): array {
- $now = new DateTime();
// ...
}
}
[Q]
$this->cache->has('recent_posts')
のように、スタティックメソッドをインスタンス経由で呼んでも、PHPStanに怒られないの?
[A]
警告が出る可能性は高いです。
ただし、「依存関係の明示化」や「テスト容易性」のメリットが大きいため、/** @phpstan-ignore-next-line */
を利用して警告を抑制するのも一つの手です。
サービスをインスタンス化した後にそのサービスの振る舞いを変更しない
final class Importer
{
private bool $ignoreErrors = true;
public function ignoreErrors(bool $ignoreErrors): void
{
$this->ignoreErrors = $ignoreErrors;
}
// ...
}
$importer = new Importer();
// ここでImporterを使うとエラーは無視される。
// ...
$importer->ignoreErrors(false);
// ここで使うとエラーは無視されない。
// ...
このようなことが起きないようにしましょう。
すべての依存関係や設定値は最初から持っているべきで、サービスをインスタンス化した後に再設定できてはいけません。
サービスをインスタンス化した後で変更できないようにし、省略可能な依存関係を許さなければ、できあがったサービスは時間が経過しても予測可能な振る舞いをし、メソッドを実行した人に応じて突然別の実行パスをたどるということも起きません。
[NOTE]
サービスクラスをイミュータブルにすると、状態の変更がないため、複数のスレッドから同時にアクセスされても予期せぬ副作用が起こりません。
いわゆるスレッドセーフな状態を実現できます。
ドメイン不変条件
ドメイン不変条件とは、オブジェクトが表す概念に関するドメイン知識に基づいて、与えられたオブジェクトに対して常に真であるものを指します。
例として PriceRange
は、ある品物に対して入札者が支払うであろう最低価格と最高価格をセント単位で表します。
コンストラクタで Assert
を使い以下の不変条件を検証しているため、常に意味のある値を保持することが保証されます。
- 最低価格 >= 0
- 最高価格 >= 0
- 最高価格 > 最低価格
final class PriceRange
{
private int $minimumPrice;
private int $maximumPrice;
public function __construct(int $minimumPrice, int $maximumPrice)
{
Assert::greaterThanOrEqual($minimumPrice, 0);
Assert::greaterThanOrEqual($maximumPrice, 0);
Assert::greaterThan($maximumPrice, $minimumPrice);
$this->minimumPrice = $minimumPrice;
$this->maximumPrice = $maximumPrice;
}
}
すべてのオブジェクトが作成時に必要最低限のデータを受け取り、そのデータが正しく意味をなすものであることを確認すれば、アプリケーションでは完全で有効なオブジェクトしか目にしないようになります。
すべてのオブジェクトは、意図したとおりに使用できると考えて問題ないはずです。
[NOTE]
サービスは依存関係を持つことができ、それらはコンストラクタ引数として注入される必要があります。
しかし、それ以外のオブジェクトでは依存関係を受け取るべきではなく、値、バリューオブジェクト、またはそれらのリストのみを受け取るようにしましょう。
バリューオブジェクトが何らかのタスクを実行するためにサービスを必要とする場合は、必要に応じてメソッド引数として注入しましょう。
コンストラクタインジェクションは、「ずっと必要な依存を明示的に渡す」という意味を持つので、ドメインの振る舞いを実装したり、複数エンティティをまたぐ処理を扱うサービスクラスの責務をサービス以外のクラスにも負わせてしまうことになります。
場合によっては、メソッド引数としてサービスを渡す必要があるということは、その振る舞いをサービスとして実装すべきだと示唆しているかもしれません。
おわりに
今回は、「オブジェクト設計スタイルガイド」の1~3章を題材に、自分が疑問に感じたポイントを紹介しました。
依存関係やオブジェクトの状態を明確に保つことで、安全で予測可能な設計を目指しましょう!
なお、続きとなる1~3章の記事も公開しているので、ぜひあわせてご覧ください!