オブジェクト指向設計の5つの原則「SOLID」を解説各原則が推奨する個々の設計プラクティスについて学ぶ

TechTargetは、「オブジェクト指向設計の原則『SOLID』」に関する記事を公開した。本稿ではSOLIDの入門編として、この開発体系を具現化する5つの原則、各原則が推奨するプラクティス、そしてこの考え方が重要である理由について説明する。

» 2024年01月30日 08時00分 公開
[Fred ChurchvilleTechTarget]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

 TechTargetは2023年12月19日(米国時間)、「オブジェクト指向設計の原則『SOLID』」に関する記事を公開した。

画像 オブジェクト指向設計の5つの原則「SOLID」について(提供:TechTarget)

 オブジェクト指向プログラミングにある程度精通していれば、この開発スタイルが、特定の言語やフレームワークの選択よりも、基礎となる設計手法に深く関わっていることを知っているだろう。オブジェクト指向の適切な設計については数多くの主張や見解があるが、「SOLID原則」は、オブジェクト指向設計に携わる全ての開発者が従うべきルールとして、その権威を確立している。

 SOLIDの原則を真に理解するには、この原則が推奨する個々の設計プラクティスについて学び、「各原則を並べて議論する必要性」を理解しなければならない。そこで本稿では、SOLIDが表すオブジェクト指向設計の5つの原則をそれぞれ確認する。「各原則がどう違うか」ではなく「各原則を相互に結び付ける根本的な概念とは何か」について説明する。

オブジェクト指向設計のSOLID原則とは

 オブジェクト指向プログラミングには特有の5つの原則がある。この5つの原則はその頭文字をとって“SOLID”と呼ばれる。この略語があるため、以下に示すオブジェクト指向プログラミングの原則は比較的簡単に覚えられる。

  • 単一責任の原則(S:Single Responsibility)
  • オープン/クローズドの原則(O:Open、Closed)
  • リスコフ置換の原則(L:Liskov Substitution)
  • インタフェース分離の原則(I:Interface Segregation)
  • 依存関係逆転の原則(D:Dependency Inversion)

 これらの原則はいずれも排他的なものではない。それどころか、相互に包含関係があるともいえる。1つの原則を目標として追求していくと、他の幾つかの原則にも従うことになる場合がある。あるいは、ある原則を追求した結果として他の原則も副産物として生じることもある。つまり、上記のSOLID原則のプラクティスを適切に順守すると、自然と別の原則のプラクティスにも準拠している可能性があるということだ。

 例えば、インタフェース分離の原則は、多くの点で単一責任の原則を反映している。つまり、単一責任の原則を正しく実装すれば、通常はインタフェース分離の原則にも従うことになる。同様に、オープン/クローズドの原則とリスコフ置換の原則の両方に厳密に従えば、依存関係逆転の原則には簡単に従うことができる。

 ここからは、オブジェクト指向設計の5つのSOLID原則が実際に何を意味するかをしっかりと理解できるよう、それぞれを詳しく見ていく。なお、各原則冒頭の要約文は、オブジェクト指向プログラミングの分野にSOLID原則を最初に適用したとされているロバート・C・マーチン氏のコメントを引用したものだ。

S:単一責任の原則

「同じ理由で変更するものはまとめ、異なる理由で変更するものは分離する」(Gather together those things that change for the same reason, and separate those things that change for different reasons)

 単一責任の原則は、オブジェクト指向設計の基本原則の一つを具現化するもので、コードベースに追加する各オブジェクトクラス(オブジェクト内で定義される特定のメソッド、変数、パラメーター)は、1つの明確なジョブまたは関数のみに責任を持つ必要があることを表す。つまり、クラスを変更する理由は「1つの個別プロセスを再構成する」という1つの目的に限定する必要がある。

 残念だが、この考え方は“行き過ぎ”になる可能性がある。人によっては「他のメソッドと異なる操作をする全てのメソッドに独自のクラスが必要だ」と解釈するかもしれない。だが、その解釈は間違いだ。そういった解釈のままだと、わずかに異なる方法で同じ目的を達成するだけの冗長なクラスが生成される。その結果、少なくともコードは不必要に複雑になる。最悪の場合は、緊密に結び付いた複数のクラスが網目のように複雑に絡み合う状態になり、1つのクラスを更新すると次々と更新が必要になる可能性がある。

 このような解釈ではなく、クラスを作成する開発者は、メソッドの動作のわずかな違いではなく、メソッドの全体的な出力に目を向ける必要がある。2つのユニークなメソッドが大まかに同じプロセスをサポートしているのであれば、それぞれの動作をもたらす細部とは関係なく、同じクラスに含めるようにするとよいだろう。

O:オープン/クローズドの原則

「ソフトウェアのエンティティ(クラス、モジュール、関数など)は、拡張に対してはオープンであり、変更に対してはクローズでなければならない」(Software entities <classes, modules, functions, etc.> should be open for extension, but closed for modification)

 オープン/クローズドの原則が設計に及ぼす影響は比較的分かりやすい。この原則では、既存のコードを変えずに、開発者がコードベースに新たな機能を追加できる必要がある。もしかしたら「システムの基盤となる既存のコードを全く更新することなく複雑な機能を追加することなど現実的ではない」と思えるかもしれない。だが、プラグインアーキテクチャモデルやコンテナベースのデプロイメントなど、この原則を実現する一般的なアプローチはたくさんある。

 理論上、開発者は単一責任の原則を気にせずに、オープン/クローズドの原則を実装できる。ただし、その場合、“クラスとそのコードセグメントを可能な限り効率的かつ論理的に分離する”というSOLIDの全体的な意図に反することになる。クラスに複数の責任を持たせると、新たな機能が追加されるたびにコードベースの複数部分を更新しなければならなくなる。言い換えれば、本質的にはある問題(不変性)を別の問題(結合)に置き換えているにすぎない。

L:リスコフ置換の原則

「スーパークラスのオブジェクトは、アプリケーションの機能を停止することなく、そのサブクラスのオブジェクトと置き換えられれなければならない」(Objects of a superclass shall be replaceable with objects of its subclasses without breaking the application)

 リスコフ置換の原則は複雑に思えても心配しなくていい。これは多くの開発者が最初に苦労する原則だからだ。

 経験豊富な開発者なら知っていることだが、クラスには他のクラスが含まれることがある。この原則を説明するために、他のクラスを含むクラスを「スーパークラス」、そのスーパークラスに含まれるクラスを「サブクラス」と定義する。ただし、これらの用語は2つ以上のクラスの関係を表しているだけだ。例えば、スーパークラスは他のスーパークラスのサブクラスになるかもしれない。つまり、関係を表すために表現を変えているにすぎない。

 この原則を説明するためのスーパークラスとサブクラスの表現を使うと、リスコフ置換の原則は「サブクラスに含まれるオブジェクトはスーパークラスのオブジェクトと同じ動作を示さなければならない」と要約できる。具体的には、サブクラスのオブジェクトに関連付けられるメソッドは、スーパークラスのメソッドでは対応しない、変数の呼び出しやパラメーターの要求、データ型を返すなどができない。あるいは、サブクラスのオブジェクトが処理のために何かを受け取る場合、スーパークラスは同じ入力を受け取り、同じ方法で処理できなければならない。具体的なケースがどうあれ、動作は同じでなければならない。

 リスコフ置換の原則が達成すべき目的の一つは、クラッシュの原因となるランタイムエラーの量と頻度を減らすことにある。

 要求側のクライアントがサブクラスを呼び出す場合、どこかの時点でそのスーパークラスも呼び出すだろう。また、障害発生時に、クライアントがスーパークラスに要求を中継する可能性がある。その場合、要求側のクライアントは「サブクラスと同じ入力をスーパークラスにも渡さなければ」とスーパークラスに入力を渡すだろう。もしもサブクラスが、スーパークラスが要求していないパラメーターや変数などを要求していた場合、その要求は例外を返すことになる。

I:インタフェース分離の原則

「クライアントは、使用していないインタフェースに依存することを強制されるべきではない」(Clients should not be forced to rely on interfaces they don't use)

 この原則を説明するために、インタフェースの定義を「2つ以上のソフトウェアコンポーネントが、必要な指示を相互に受け渡すステートメント、関数、コードベースのデータの集合体」とする。多くのメソッドでは、クライアントが特定の入力を提供する必要があり、個々のメソッドでは、何をすべきかについての指示が必要になる。それらのやりとりを仲介するのがインタフェースだ。

 1つのインタフェースを介して動作する「Class A」というクラスがあるとする。ある開発者が単一責任の原則に従い、Class Aの機能を拡張する新しいクラス「Class B」を作成することにした。この2つのクラスはわずかに異なるメソッドを使って同様の出力を生成するため、開発者はこの2つのクラスを1つのインタフェースの下に配置しても問題ないと考えるかもしれない。

 だが、それは間違いだ。2つのクラスのどちらか1つだけにアクセスしたいクライアントは、メソッドが必要としないパラメーターを渡さなければならなくなる(つまり不必要なデータ交換が生じる)。さらに、Class Bを更新するためにインタフェースの変更が必要になった際にClass Aも変更しなければならない可能性がある。これはオープン/クローズドの原則に反することになる。

 この例の場合は、クラスごとにインタフェースを作成するとよいだろう。両クラスのインタフェースをカプセル化する大きなインタフェースを用意すれば、クライアントは2つのメソッドにアクセスできるようになる。

D:依存関係逆転の原則

 依存関係逆転の原則は、技術的には2つの原則に分かれている。

「上位レベルのモジュールは下位レベルのモジュールに依存してはならない。どちらも抽象化に依存すべき」(High-level modules should not depend on low-level modules. Both should depend on abstractions)

「抽象は詳細に依存してはならない。詳細は抽象に依存すべき」(Abstractions should not depend on details. Details should depend on abstractions)

 本質的に、ソフトウェア設計のモジュールが上位レベルのコンポーネントに依存している場合、その上位レベルコンポーネントは、依存する下位モジュールからの影響を受けるべきではない。

 なお、ソフトウェア開発において「モジュール」「コンポーネント」という用語はさまざまな意味を持つため、本稿では、クラスの依存関係の観点でこの原則を説明する。

 一見、この原則は、対象となる結合を逆転させ、依存関係を下位クラスから上位クラスにのみ向かわせるだけの問題のように思えるかもしれない。ただし、依存関係の反転の目的は、依存関係を完全に切り離す別のインタフェースを追加することだ。この追加のインタフェースは抽象化と呼ばれる。

 この新しい抽象化は、下位レベルと上位レベルのクラスをつなぐ“接着剤”の役割を果たす。それと同時に、それぞれが他方に影響を与えることなく変更できる柔軟性も提供する。抽象インタフェースを使用すれば、上位クラスは下位クラスに必要な全てのジョブを処理できる。抽象インタフェースを使用しても処理できないような複雑なジョブであれば、上位クラスが独自のインタフェースを使えばいい。抽象化が保たれている限り、各クラスは他のクラスの運営に干渉せずに、好きなように変更できる。

 ソフトウェアアーキテクチャレベルでは、こうした抽象インタフェースの例は多数存在する。最近の最も顕著な例の一つが、マイクロサービスアーキテクチャ内のコンポーネントを分離するAPIゲートウェイだ。とはいえ、こうした抽象インタフェースにはさまざまな形式があり、今後さらに増えていくだろう。

Copyright © ITmedia, Inc. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。