SOLIDの原則について
SOLIDの原則とは
SOLIDの原則とは
SOLID原則は、オブジェクト指向プログラミングにおいて、柔軟性や再利用性を高めるための5つの原則のことです。
この原則は、ソフトウェア構築時に参考となるガイドラインであり、ソフトウェアの拡張性や保守性を高める方法論となります。
名前の由来
SOLIDは、通称「アンクルボブ」こと、ロバート・マーチン氏(Robert C. Martin)が提唱した原則の内、5つの原則の頭文字をとって命名されています。
この名称自体は、マイケル・フェザーズ氏 (Michael Feathers)が2004年頃、いくつかの原則から5つを選び出し、SOLIDという語呂合わせの頭字語にして普及したと言われています。
なお、原則の考案者はそれぞれ別人であり、SRP、ISP、DIPはRobert C. Martin氏、OCPはBertrand Meyer氏、LSPはBarbara Liskov氏によって考案されています。
単一責任の原則(SRP:Single Responsibility Principle)
単一責任の原則とは
単一責任の原則とは、「1つのクラスは1つの責任だけを持つべきである」という原則です。
つまり、クラスに持たせる役割は一つだけにするべきであり、複数の役割が存在する場合にはクラスを分割するという考え方です。
これは、クラスに多くの機能を持たせると、1つの機能変更が他の機能に影響していまい、不具合が生まれることを示唆しています。
“a module should be responsible to one and only one actor.”
(モジュールは単一の機能についてのみ責任を持つべきである)
変更による影響を受けるクラスは1つだけでなければならない
単一責任の原則では、ソフトウェア仕様を一部変更した際には「その影響を受ける仕様は当該クラスだけでなければならない」という考えもあります。
例えば、ユースケースが変更されてクラスを修正する場合に、そのクラスを変更する理由は1つ以上存在してはならないとしています。
もしも、クラスを変更する理由が2つ以上あるならば、そのクラスは2つ以上の責任を持ってしまっていることを意味します。
“There should never be more than one reason for a class to change.”
(変更するための理由が、一つのクラスに対して一つ以上あってはならない)
重複責任と単一責任について
例として、設定ファイルを読み込む処理クラスを考えてみます。
下図左のFileReaderクラスは重複責任であり、今後別の設定ファイルを読み込むように機能拡張すると、既存処理に大きな影響が発生します。
これを下図右のように、具体的な責任に分割したクラスを作成すると、変更や拡張による影響は修正対象クラスのみに限定できます。
開放閉鎖の原則(OCP:Open-Closed Principle)
開放閉鎖の原則とは
開放閉鎖の原則とは、ソフトウェアは「拡張に対して開いている」、「修正に対して閉じている」という2つの条件を満たすように設計すべきであるという原則です。
これは、「モジュールの機能には、追加や変更が可能であり、その影響が他のモジュールに及ばないようにする」という考え方です。
“software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
(ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。)
拡張に対して開いている
「拡張に対して開いている」とは、機能追加を行う場合に、既存のコードには修正を入れずオブジェクトの追加によって実現できることを意味します。
これは、既存のソースコードを変更するのではなく、新規ソースコードを追加することによってシステムの振る舞いを変更できるように設計すべきであるということです。
修正に対して閉じている
「修正に対して閉じている」とは、不具合修正を行う場合に、バグの存在するオブジェクトのみを修正すれば対応が完了することを意味します。
つまり、不具合のあるクラスに対する修正が、他クラスの動作を変更したり、デグレードを発生させないように設計することです。
インターフェースを活用する
開放閉鎖の原則は、単一責任の原則に則り具体的な責任に分割したクラスとインターフェースを用意することで実現できます。
例えば下記のように、新規にDB設定ファイルやカスタム機能設定ファイルというクラスを追加した場合、既存処理に影響が及ぼさずに機能追加を実現できます。
また、これらのクラスは独立しているので、修正は当該クラスで完結し、その修正は他クラスへ影響しません。
リスコフの置換原則(LSP:Liskov Substitution Principle)
リスコフの置換原則とは
リスコフの置換原則とは、派生クラス(サブクラス)はその基底クラス(スーパークラス)と置換可能でなければならないという原則です。
これは、個々のパーツが交換可能となり、継承元と継承先は完全に置き換え可能なものとして設計するという考え方です。
つまり、リスコフの置換原則は、継承を正しく使用するためのガイドラインであり、条件を満たさないならば継承を利用してはならないという原則です。
“Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.”
(ある基底クラスへのポインタないし参照を扱っている関数群は、その派生クラスのオブジェクトの詳細を知らなくても扱えるようにしなければならない)
(LSP違反の例)正方形・長方形問題
正方形・長方形問題とは、「正方形クラスは長方形クラスを継承するべきか?」というオブジェクト指向における命題です。
一般的に、正方形が長方形であること(つまり、正方形 is a 長方形)は正であり、正方形クラスが長方形クラスを継承することは「正しい設計」です。
しかし実際には、継承を行った場合、クラスの振る舞いが変化してしまい、正しい継承であると言えなくなってしまいます。
例えば、長方形クラス(Rectangle)に縦横の長さを指定して面積を取得する処理は、以下のように実行できます。
Rectangle rectangle = new Rectangle();
rectangle.setWidth(5);
rectangle.setHeight(2);
Assert(rectangle.Area() == 10);
一方、長方形を継承した正方形クラス(Square)は、縦と横の辺の長さが等しい長方形であることから、縦横の長さを指定によってその挙動が変わってしまいます。
Rectangle rectangle = new Square();
rectangle.setWidth(5); // 内部的にはwidthとheightがそれぞれ5になる。
rectangle.setHeight(2); // 内部的にはwidthとheightがそれぞれ2になる。
Debug.Assert(rectangle.Area() == 10); // 面積は4となるためエラーとなる。
つまり、長方形クラスがRectangleなのかSquareなのかで振る舞いが異なるため、リスコフの置換原則に違反しているということになります。
正方形・長方形問題の解消方法
リスコフの置換原則に則るためには、共通の基底となるインターフェイスまたは抽象クラスを作成します。
つまり、SquareクラスはRectangleクラスを継承せず、それぞれのクラスがIShapeインターフェイスを継承するように変更します。
つまり、正方形クラスは長方形クラスを継承すべきではなく、それぞれが上位クラスを継承する形にします。
「正方形 is a 長方形」という関係性は現実の世界では正しいけど、「ソフトウェアの世界」という立場では正しくない関係であると割り切るのです。
このように、クラス関係を見直したり、変数を不変(Immutable)にするなど改善を加えることで、より良い設計を実現していきます。
インターフェース分離の原則(ISP:Interface Segregation Principle)
インターフェース分離の原則とは
インターフェース分離の原則とは、インターフェースの利用者に不要なメソッドの実装を強制してはいけないという原則です。
“Clients should not be forced to depend upon interfaces that they do not use.”
(クライアントは、使用しないインターフェイスの実装を強制されてはならない)
インターフェースの分離とは
インターフェースの分離とは、インターフェースは利用者の立場から最小限の規則にすべきである、という考え方です。
巨大なインターフェースを用意するのではなく、それぞれの目的に特化した小さなインターフェースを複数用意します。
これにより、サブクラスで不必要で無駄なコードを実装しないようにでき、コードの拡張性・可読性・保守性が向上します。
“Many client-specific interfaces are better than one general-purpose interface.”
(汎用なインターフェースが一つあるよりも、各クライアントに特化したインターフェースがたくさんあった方がよい)
インターフェースを適切に分離する
例えば、下図左のようにCRUD操作用Operationインターフェースを各ユーザが実装すると、各ユーザで不要なメソッドを禁止処理する必要があります。
インターフェース分離の原則では、下図右のように各クライアントに特化したインターフェースを複数用意して、必要なものだけ実装するように設計します。
これにより、開放閉鎖の原則と同様の効果を実現することができます。
依存性逆転の原則(DIP:Dependency Inversion Principle)
依存性逆転の原則とは
依存性逆転の原則とは、具象に依存するのではなく抽象に依存するべきであるという原則です。
言い換えると、上位モジュール(使う側)は下位モジュール(使われる側)に依存してはならないという考え方です。
そして、この原則に従って実装でなく抽象に依存させるように変更すると「依存関係が逆転する」という現象が発生します。
“Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.”
(抽象は詳細に依存してはならない。詳細(具象的な実装内容)が抽象に依存するべきである)
“High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).”
(上位モジュールはいかなるものも下位モジュールから持ち込んではならない。双方とも抽象(例としてインターフェース)に依存するべきである)
「具象に依存するのではなく抽象に依存する」とは?
まずは、依存性逆転の原則に従わずに、MVVMモデルにおいて、DBへデータを書き込む処理を考えてみます。
ここでは、ModelクラスがDatabaseWriterクラスを呼び出して、DBへのデータ書き込み処理を実行します。
つまり、Modelクラスが具象であるDatabaseWriterクラスを利用(依存)しているということです。
ソースコードで示すと以下のようになります。
Modelクラスの中でDatabaseWriterクラスをnewして、データ書き込み処理を実現しています。
class ViewModel
{
var model = new Model();
void WriteDataToDB(string data)
{
model.write(data);
}
}
class Model
{
bool write(string data)
{
var dbWriter = new DatabaseWriter();
dbWriter.write(data);
}
}
依存性逆転の原則を実現するには、他クラスを直接コールするのではなく、インターフェースや抽象クラスなどを介してコールします。
これにより、上位モジュールと下位モジュールが「抽象に依存している」という状態となります。
ここでは、IFileWriterインターフェースを追加して、DatabaseWriterはIFileWriterを実装し、ModelはIFileWriterを利用するように変更します。
さらに、インターフェースIFileWriterをMVVM側のパッケージに含めるように配置します。
すると、DatabaseWriterがMVVMパッケージに対して依存する形になり、依存の方向が逆転します。
これをソースコードで示すと以下のようになります。
依存性逆転の結果、ViewModelでDatabaseWriterクラスをnewして、ModelクラスにIFileWriterを渡すように変更しています。
なお、このように、依存するものを外部から受け渡してもらうことを依存性の注入(DI:Dependency Injection)といいます。
class ViewModel
{
var model = new Model();
void WriteDataToDB()
{
model.data = data;
IFileWriter dbWriter = new DatabaseWriter();
model.write(dbWriter);
}
}
class Model
{
string data;
bool write(IFileWriter writer)
{
writer.write(data);
}
}
以上が依存性逆転の原則の実例です。
依存性逆転の原則のメリット
依存性逆転の原則のメリットは、クラスやモジュールを疎結合にできることです。
これにより、拡張性の向上や再利用性の向上、変更容易性の向上などが得られます。
例えば、上記クラス図に対して、CSVファイルへ書き込む処理やテキストファイルへ書き込む処理を追加する場合、Modelに対して一切の変更を行わずに機能追加を実現できます。
新しい下位モジュールを追加する場合には、IFileWriterという抽象を満たすように実装するだけ完了し、上位モジュールへの影響を最小限に抑えられます。
そして、下位モジュールは抽象を介しているため実装の詳細が隠蔽されており、変更に伴う修正範囲が限定され、バグの発生リスクが低減します。
デザインパターンにおける、ストラテジーパターン、オブザーバーパターン、ビルダーパターンなどはDIP原則に従った方法といえます。
class ViewModel
{
var model = new Model();
void WriteData()
{
model.data = data;
IFileWriter dbWriter = new DatabaseWriter();
model.write(dbWriter);
IFileWriter csvWriter = new CsvWriter();
model.write(csvWriter);
}
}
class Model
{
string data;
bool write(IFileWriter writer)
{
writer.write(data);
}
}