🙆

依存関係逆転の原則とストラテジーパターンとステートパターン

2024/04/29に公開

依存関係逆転の原則とストラテジーパターンとステートパターン

依存関係逆転の原則と GoF のデザインパターンであるストラテジーパターンとステートパターンはクラス図にしているととても似ています。

依存関係逆転の原則のクラス図
依存関係逆転の原則

ストラテジーパターンのクラス図
ストラテジーパターン

ステートパターンのクラス図
ステートパターン

じゃあ同じかというとそういうわけでもありません。自分なりの見解をつらつら書きました。

ステートパターン

ステートパターンは区分に対してふるまいを持たせたものです。

次のコードはレンタルビデオのレンタル料金を求めるものですが、区分があって、区分ごとに計算方法が異なります。新作は高くても借りてくれるので高くなるでしょう。子供向けはちょっと割り引くと、親が自分用だけでなく子供向けも一緒に借りてくれるかもしれません。

decimal レンタル料金(レンタルビデオ区分 区分, int 泊数)
{
    switch(区分)
    {
        case レンタルビデオ区分.新作: return ...;
        case レンタルビデオ区分.子供向け: return ...;
        case レンタルビデオ区分.通常: return ...;
    }
}

enum 型がプログラム言語に用意されていればそれを使いますが、そうでない場合は数値定数や文字列定数を利用することが多いです。

ステートパターンは switch 文を利用せずに、区分それぞれに計算方法を持たせます。

ステートパターンを利用するメリット・デメリットはこのドキュメントの範疇外なので述べませんが、ステートパターンはストラテジーパターンとよく似ていても、あくまで区分それぞれにふるまいを記述する方法となります。

余談ですが、ステートは日本語では状態と呼ばれることがありますが、私はこういった区分も状態の一種とみなしています。

ストラテジーパターン

ストラテジーパターンは名前の通り、開発者が戦略を自由に交換できるようにするためのものです。

典型的なのがソートだと思います。

次の例は JavaScript で数値型の配列をソートするコードです。

const array = [3, 43, 21, 4, 659];
array.sort((x, y) => y - x);

sort() 関数はソートアルゴリズムが実装されていますが、要素の大小を比較する関数を引数でもらっています。この要素の大小を比較する関数は、sort() 関数の利用者が用意するのが一般的です。このおかげで、文字列などの他の型のソートや、オブジェクトのプロパティやもしくは計算で求めた値でのソートなどもできるようになります。

極端に表現すると次のようになります。

ストラテジーパターンを採用すると、フレームワークに対して利用者が追加のコードを書くことで、無限の拡張性を得ることができます。

ステートパターンとの違い

ステートパターンは区分をオブジェクトで置き換えます。区分とそのふるまいは同じ開発者が書くことが多いでしょう。その後の修正でも、製品の開発チームが区分クラスを増やしたり修正したりします。インターフェイスとその実装クラスの関係がとても近いと言えるでしょう。

ストラテジーパターンでは、インターフェイスの実装は、全く別の組織の開発者が書くことを意図しています。(もちろん、インターフェイスの定義をした開発者自身が実装するケースもあります) インターフェイスの定義をした開発者が全く知らない実装が無数にあることになります。

ステートパターンは switch 文に置き換えることができますが、ストラテジーパターンを switch 文で置き換えることは不可能です。

  • ステートパターン - 区分ごとにふるまいをかえたいなあ
  • ストラテジーパターン - フレームワークを利用する開発者に戦略を自由に書いてほしいなあ

依存関係逆転の原則

アプリケーションを開発するときによく使われる三層アーキテクチャを例にします。

名前 説明
プレゼンテーション層 画面を担当します。利用者の操作をアプリケーション層に伝え、結果を画面に反映させます。
アプリケーション層 アプリケーションの機能を担当します。
インフラストラクチャ層 データの永続化や外部 API の呼び出しなどを担当します。

プレゼンテーション層はアプリケーション層を呼び出しますし、アプリケーション層はインフラストラクチャ層を呼び出します。

これはこれで間違っているわけではないのですが、アプリケーション層はプレゼンテーション層が欲しい機能を提供する必要がありますが、プレゼンテーション層がアプリケーション層に依存しているのがいやらしいのです。

プレゼンテーション層にはビジネスロジックもデータベースも関係ありません。しかし、画面ラフを見ながらどういうデータ構造にするかとかどういうロジックにしようかと考えたことはありませんか? 結果、データベース定義を先にしてしまい、それにプレゼンテーション層が引きずられてしまいます。

  • プロジェクトの初期の十分な知識がないころりにデータベース定義を作成するので、データベース定義の品質が悪くなりやすい
  • データベース定義は修正しずらいので、品質の悪いデータベース定義に、本来関係のないアプリケーション層やプレゼンテーション層が引きずられる
  • 三層いっぺんに考えているので、ビジネスロジックがプレゼンテーション層やインフラストラクチャ層に書かれやすい

最後の問題はかなり厄介で、せっかく役割ごとに三層に分けたのに、コードが分散するので読みづらく修正しやすくなります。べた書きのコピペコードの方がはるかにましです。

依存関係逆転の原則では、依存関係を呼び出し方向ではなく、設計の依存関係に合わせ逆転させます。

アプリケーション層のインターフェイスがプレゼンテーション層と同じパッケージにあるのがみそです。こうすることで、プレゼンテーション層が本当に欲しい、データベース定義に引きずられていないアプリケーション層のインターフェイスが得られます。

もちろん、依存関係逆転の原則を持ち出したのにかかわらず、データベース定義も一緒くたに考え続けているなら、真に依存関係を逆転させたとは言えません。

ストラテジーパターンとの違い

依存関係逆転の原則を導入するメリットの一つに、単体テスト時のモックへの差し替えがやりやすくなるというものがあります。これをもってしてストラテジーパターンだと言い張ることもできると思います。しかし、ストラテジーパターンのように、まったく別の組織の開発者に実装を書く自由を与えているわけではないため、ストラテジーパターンと呼ぶのはちょっと微妙だと思います。

また、このモックへの差し替えのやりやすさは依存関係逆転の原則の本質ではないと考えます。

最後に

依存関係逆転の原則やデザインパターンのストラテジーパターン・ステートパターンは、技術的には動的束縛による実装の切り替えを利用しています。(極端な話ですが、GoF のデザインパターンの多くはこの動的束縛を利用していて、それらはすべて似ています) 形に違いがあるはずだとか、同じ形だから同じパターンなのではないかと混乱するように思います。

それをどう呼ぶかで混乱するようなら気にしないのも一つの手だと思います。例で挙げたレンタルビデオ区分のコードをストラテジーパターンだと思って書いたからと言って、ステートパターンで書いた時とコードの違いは出ないでしょう。

形とか分類のこだわるよりも、メリットが得られているかどうかの方が大事です。

Discussion