- Qiita - オブジェクト指向の法則集
- wikipedia - 開放/閉鎖原則
- wikipedia - リスコフの置換原則
- wikipedia - 依存性逆転の原則
- SlideShare - 20160526 依存関係逆転の原則
- The Interface Segregation Principle (ISP) インターフェース分離原則
- いろは二〇八 - Graphvizの基本 - 02
- Qiita - Graphvizとdot言語でグラフを描く方法のまとめ
- Graphviz - Node, Edge and Graph Attributes
- 拡張に対して開いているべき(open)
- 修正に対して閉じているべき(closed)
噛み砕いて言うなら、下記の通りになる
- コードに修正が必要になった時、不要に影響範囲が漏れ出ることを避けよ
- 機能拡張はコード修正によってではなく、コードの追加によって実現せよ
1988年に、バートランド・メイヤーが『オブジェクト指向ソフトウェアの構築(Object Oriented Software Construction)』の中で「開放/閉鎖原則」という語を生み出したと一般に称される。
コードが完成したら、クラスの実装はエラーの修正のためだけに変更され、新機能や機能変更には別のクラスを作る必要がある
この原則を守るコードでは、オリジナルのクラスから継承によってコードの再利用が可能となる。
派生したサブクラスは、オリジナルのクラスと同じインタフェースを持っていても持っていなくとも良い。
メイヤーの定義においては、実装の継承をしても良い。
実装は継承によって再利用可能であるが、インタフェース仕様は必ずしもそうではない。
既存の実装は修正に対して閉じており、また新しいクラスは既存のインタフェースを引き継ぐ必要はない。
1990年代、開放/閉鎖原則とは、一般的に抽象インタフェースの利用を指すように意味が変わっていった。 その定義では、実装はもはや固定ではなく、複数の実装が存在可能となり、互いにポリモーフィックに入れ替えることができる。
T
型のオブジェクトx
に関して真となる属性をq(x)
とする。
このときS
がT
の派生型であれば、S
型のオブジェクトy
についてq(y)
が真となる。
S
がT
の派生型であれば、プログラム内でT
型のオブジェクトが使われている箇所は全てS
型のオブジェクトで置換可能であるべき。
リスコフの置換原則を契約プログラミングに適用すると、契約と継承の相互作用に次のような制約をもたらす
- 派生型は、事前条件を強めることはできない。つまり、上位の型よりも強い事前条件を持つ派生型を作ることはできない。
- サブクラスは、スーパークラスが動く状況なら必ず動かなければならない。
- 派生型は、事後条件を弱めることはできない。つまり、上位の型よりも弱い事後条件を持つ派生型を作ることはできない。
- サブクラスは、スーパークラスが達成することは必ず達成しなければならない。
車クラス と F1クラス を用意する。
これらのクラスを利用するドライバクラスを用意する。
class ドライバクラス
{
var car = new Car();
var f1 = new F1();
var crt = CreateCurrentLocation();
var dst = CreateRandomDestination();
// 事前条件チェック
if (dst == null) dst = DefaultDestination;
if (crt == null) crt = Home;
// サブルーチン呼び出し
var message = car.SetDestination(crt, dst);
// 事前条件を満たしているのでメッセージの妥当性が保証されている
car.Read(message);
var dst = CreateRandomDestination();
// 事前条件チェック
if (dst == null) dst = DefaultDestination;
// サブルーチン呼び出し
var message = f1.SetDestination(crt, dst);
// 事前条件を満たしているのでメッセージの妥当性が保証されている
f1.Read(message);
}
class Car
{
public Message SetDestination(string currentPlace, string destination)
{
// 事前条件
if (currentPlace == null) throw new NullCurrentPlaceException();
if (destination == null) throw new NullDestinationException();
// 不変条件
InvariantTest();
this.Destination = destination;
ChangeDriveMode(Mode.Ready);
var message = new Message(this.Mode.Message);
// 事後条件
if (!this.Destination.IsValid) throw new InvalidDestinationException();
if (!this.Mode.IsReady) throw new NotPreparedException();
if (message == null) throw new NullMessageException();
// 不変条件
InvariantTest();
return message;
}
// クラス不変条件の判定
private void InvariantTest()
{
if (!this.CurrentPlace.IsValidLocation) throw new InvalidLocationException();
if (!this.Destination.IsValidLocation) throw new InvalidLocationException();
if (this.CurrentPlace == this.Destination) throw new InvalidLocationException();
}
}
class F1 : Car
{
public Message SetDestination(string currentPlace, string destination)
{
// 事前条件 派生型では事前条件を弱めることのみできる
if (destination == null) throw new NullDestinationException();
// 不変条件
InvariantTest();
this.Destination = destination;
ChangeDriveMode(Mode.Ready);
var message = new Message(this.Mode.Message);
// 事後条件 派生型では事後条件を強化のみできる
if (!this.Destination.IsValid) throw new InvalidDestinationException();
if (!this.Mode.IsReady) throw new NotPreparedException();
if (message == null) throw new NullMessageException();
if (!this.IsIgnited) throw new NotIgnitedException();
// 不変条件
InvariantTest();
return message;
}
}
プログラムコードの中にプログラムが満たすべき仕様についての記述を盛り込む事で設計の安全性を高める技法。
コードを呼ぶ側が事前条件と不変条件を満たす義務を負うことで、呼ばれたコードはその条件が恒真であるとの前提を利益として得る。
引き換えに、呼ばれたコードは事後条件と不変条件を義務として負い、呼ぶ側の利益としてこれを保証する。
- 事前条件 (precondition)
- サブルーチンの開始時に、これを呼ぶ側で保証すべき性質。例:引数チェック。
- 事後条件 (postcondition)
- サブルーチンが、終了時に保証すべき性質。例:戻り値チェック。
- 不変条件 (invariant)
- クラスなどのオブジェクトがその外部に公開しているすべての操作の開始時と終了時に保証されるべき、オブジェクト毎に共通した性質。
この原則は、下記のことを表す。
- 上位レベルのモジュールは、下位レベルのモジュールに依存すべきではない
- 上位レベルのモジュール、下位レベルのモジュール、ともに抽象(abstractions)に依存すべき
- 抽象は、詳細に依存してはならない
- 詳細は、抽象に依存すべき
例えば、下記のような依存関係があるとする。
下位レベルのモジュールの再利用性を高めるためには、上記を、下記のような依存関係に逆転させたい。 ロジック層とデータアクセス層との間だけにこの原則を適用したコード例を下記に示す。
- ユーザの誕生日から現在の年齢を計算する
- ユーザの情報は永続化して保存可能
public void Main()
{
var user = new User (Date Time.Now);
Console.WriteLine("You are " + user.Age.ToString() + "years old.");
user.Save(user);
}
public class User
{
public DateTime Birthday { set; }
public int Age { get => GetAge(); }
private int GetAge()
{
return 17;
}
public void Save()
{
var repository = new UserRepository();
repository.Save(this);
}
}
public class UserRepository
{
public void Save(User user)
{
// Insert Database
}
}
上位モジュール(Logic DLL)と下位モジュール(Data Access DLL)を抽象に依存させる。
しかし、単純に抽象に依存させると、User
クラスがUserRepository
インターフェースに依存してしまっている。
public interface IUserRepository
{
void Save(User user);
}
public class User
{
private DateTime _birthday;
// コンストラクタ
public User(DateTime birthday)
{
_birthday = birthday;
}
public int Age => GetAge();
private int GetAge()
{
return 17;
}
public Save()
{
// まだ、UserRepositoryの実体に依存してしまっている
IUserRepository repository = new UserRepository();
repository.Save(this);
}
}
依存関係を、クラスやソースコードの外側から注入することで、User
クラスをUserRepository
インターフェースに依存させなくできる。
public class User
{
// フィールドで依存先のインターフェースを持つ
private IUserRepository _repository;
private DateTime _birthday;
// コンストラクタ
// コンストラクタでIUserRepositoryの実装を受け取る
public User(DateTime birthday, IUserRepository repository)
{
_birthday = birthday;
_repository = repository;
}
public int Age => GetAge();
private int GetAge()
{
return 17;
}
public Save()
{
// コンストラクタで受け取ったインスタンスを使う
// IUserRepository型である限り、実体が何なのかは無視できる
_repository.Save(this);
}
}
パッケージの持ち方を変える事により、「下位モジュールから上位モジュールに依存する」が実現できる。
- クライアントは自分が使うインタフェースだけを依存すべきである
- 単一のインタフェースより複数のインタフェースを使うべきである
- インタフェースをクライアントごとに分離すべきである
User
クラスから実際にUser Repository
クラスを使うにはどうしたら良いか。
最も簡単な方法は、これらのクラスの更に外側にあるMain
クラスからオブジェクトを注入することである。
Main
クラスがアプリケーション全体を統括する責務を持つならば、LogicDLL
パッケージおよびDataAccessDLL
パッケージに依存していても問題ない。