Skip to content

Instantly share code, notes, and snippets.

@otaon
Last active November 30, 2018 15:30
Show Gist options
  • Save otaon/2968052209d07c637531c7ed909c7605 to your computer and use it in GitHub Desktop.
Save otaon/2968052209d07c637531c7ed909c7605 to your computer and use it in GitHub Desktop.
オブジェクト指向プログラミングで守るべき原則集

オブジェクト指向原則集


参考文献


Robert C. Martin の Principles of OOD

The Open-Closed Principle (OCP) 開放/閉鎖原則

  • 拡張に対して開いているべき(open)
  • 修正に対して閉じているべき(closed)

噛み砕いて言うなら、下記の通りになる

  • コードに修正が必要になった時、不要に影響範囲が漏れ出ることを避けよ
  • 機能拡張はコード修正によってではなく、コードの追加によって実現せよ

メイヤーの開放/閉鎖原則

1988年に、バートランド・メイヤーが『オブジェクト指向ソフトウェアの構築(Object Oriented Software Construction)』の中で「開放/閉鎖原則」という語を生み出したと一般に称される。

コードが完成したら、クラスの実装はエラーの修正のためだけに変更され、新機能や機能変更には別のクラスを作る必要がある

この原則を守るコードでは、オリジナルのクラスから継承によってコードの再利用が可能となる。
派生したサブクラスは、オリジナルのクラスと同じインタフェースを持っていても持っていなくとも良い。

メイヤーの定義においては、実装の継承をしても良い。
実装は継承によって再利用可能であるが、インタフェース仕様は必ずしもそうではない。
既存の実装は修正に対して閉じており、また新しいクラスは既存のインタフェースを引き継ぐ必要はない。

ポリモーフィックな開放/閉鎖原則

1990年代、開放/閉鎖原則とは、一般的に抽象インタフェースの利用を指すように意味が変わっていった。 その定義では、実装はもはや固定ではなく、複数の実装が存在可能となり、互いにポリモーフィックに入れ替えることができる。

The Liskov Substitution Principle (LSP) リスコフの置換原則

T 型のオブジェクトxに関して真となる属性をq(x)とする。
このときST の派生型であれば、S型のオブジェクトy についてq(y)が真となる。

STの派生型であれば、プログラム内で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;
	}
}

Programming By Contract 契約プログラミング

プログラムコードの中にプログラムが満たすべき仕様についての記述を盛り込む事で設計の安全性を高める技法。
コードを呼ぶ側が事前条件と不変条件を満たす義務を負うことで、呼ばれたコードはその条件が恒真であるとの前提を利益として得る。
引き換えに、呼ばれたコードは事後条件と不変条件を義務として負い、呼ぶ側の利益としてこれを保証する。

  • 事前条件 (precondition)
    • サブルーチンの開始時に、これを呼ぶ側で保証すべき性質。例:引数チェック。
  • 事後条件 (postcondition)
    • サブルーチンが、終了時に保証すべき性質。例:戻り値チェック。
  • 不変条件 (invariant)
    • クラスなどのオブジェクトがその外部に公開しているすべての操作の開始時と終了時に保証されるべき、オブジェクト毎に共通した性質。

The Dependency Inversion Principle (DIP) 依存性逆転の原理

この原則は、下記のことを表す。

  • 上位レベルのモジュールは、下位レベルのモジュールに依存すべきではない
  • 上位レベルのモジュール、下位レベルのモジュール、ともに抽象(abstractions)に依存すべき
  • 抽象は、詳細に依存してはならない
  • 詳細は、抽象に依存すべき

例えば、下記のような依存関係があるとする。

NOT-DI1

下位レベルのモジュールの再利用性を高めるためには、上記を、下記のような依存関係に逆転させたい。 ロジック層とデータアクセス層との間だけにこの原則を適用したコード例を下記に示す。

NOT-DI2

サンプルプログラム

  • ユーザの誕生日から現在の年齢を計算する
  • ユーザの情報は永続化して保存可能
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);
	}
}

イメージ

DI1

依存性の注入

依存関係を、クラスやソースコードの外側から注入することで、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);
	}
}

イメージ

DI2

下位モジュールから上位モジュールに依存する

パッケージの持ち方を変える事により、「下位モジュールから上位モジュールに依存する」が実現できる。

イメージ

DI3

The Interface Segregation Principle (ISP) インターフェース分離原則

  • クライアントは自分が使うインタフェースだけを依存すべきである
  • 単一のインタフェースより複数のインタフェースを使うべきである
  • インタフェースをクライアントごとに分離すべきである

メインルーチンから依存性を注入する

Userクラスから実際にUser Repositoryクラスを使うにはどうしたら良いか。
最も簡単な方法は、これらのクラスの更に外側にあるMainクラスからオブジェクトを注入することである。
Mainクラスがアプリケーション全体を統括する責務を持つならば、LogicDLLパッケージおよびDataAccessDLLパッケージに依存していても問題ない。

イメージ

DI4

The Reuse/Release Equivalence Principle (REP)

The Common Reuse Principle (CRP)

The Common Closure Principle (CCP)

The Acyclic Dependencies Principle (ADP)

The Stable Dependencies Principle (SDP)

The Stable Abstractions Principle (SAP)

プログラミングスタイル

The Law of Demeter(デメテルの法則)

digraph G {
// Setting
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
bgcolor=white,
fontcolor = Black,
fontsize = 14,
style = "filled",
rankdir = TB,
splines = polyline,
ranksep = 1.0,
nodesep = 0.9
];
node [
colorscheme = "rdylbu9"
style = "solid,filled",
fontsize = 14,
fontname = "Migu 1M",
color = 9,
fillcolor = 6,
];
edge [
style = solid,
fontsize = 14,
fontcolor = white,
fontname = "Migu 1M",
color=gray,
labelfloat = true,
labeldistance = 2.5,
labelangle = 70
];
// Elements
subgraph cluster0 {
label="LogicDLL";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
U [
shape=box,
label="User"
];
}
subgraph cluster1 {
label="DataAccessDLL";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
ID [
shape=box,
style=filled,
label="UserRepositoryInterface"
];
D [
shape=box,
style=filled,
label="UserRepository"
];
D -> ID [
tailport=n,
headport=s
];
{ rank=min; ID; }
}
subgraph cluster2 {
label="UserPackage";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 12,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
UL [
shape=box,
label="Main"
];
}
// Relationships
U -> ID [
tailport=s,
headport=n
];
U -> D [
tailport=s,
headport=n,
weight=0
];
UL -> U [
];
}
digraph G {
// Setting
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
bgcolor=white,
fontcolor = Black,
fontsize = 14,
style = "filled",
rankdir = TB,
splines = polyline,
ranksep = 1.0,
nodesep = 0.9
];
node [
colorscheme = "rdylbu9"
style = "solid,filled",
fontsize = 14,
fontname = "Migu 1M",
color = 9,
fillcolor = 6,
];
edge [
style = solid,
fontsize = 14,
fontcolor = white,
fontname = "Migu 1M",
color=gray,
labelfloat = true,
labeldistance = 2.5,
labelangle = 70
];
// Elements
subgraph cluster0 {
label="UserPackage";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 12,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
UL [
shape=box,
label="Main"
];
}
subgraph cluster1 {
label="LogicDLL";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
U [
shape=box,
label="User"
];
}
subgraph cluster2 {
label="DataAccessDLL";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
ID [
shape=box,
style=filled,
label="UserRepositoryInterface"
];
D [
shape=box,
style=filled,
label="UserRepository"
];
D -> ID [
tailport=n,
headport=s
];
{ rank=min; ID; }
}
// Relationships
U -> ID [
tailport=s,
headport=n
];
UL -> U [
];
}
digraph G {
// Setting
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
bgcolor = white,
fontcolor = Black,
fontsize = 14,
style = "filled",
rankdir = TB,
splines = polyline,
ranksep = 1.0,
nodesep = 0.25,
];
node [
colorscheme = "rdylbu9"
style = "solid,filled",
fontsize = 14,
fontname = "Migu 1M",
color = 9,
fillcolor = 6,
];
edge [
style = solid,
fontsize = 14,
fontcolor = white,
fontname = "Migu 1M",
color = gray,
labelfloat = true,
labeldistance = 2.5,
labelangle = 70,
];
// Elements
subgraph cluster0 {
label = "UserPackage";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color = "#AFAF4F",
fillcolor = "#FDFDD7",
fontcolor = Black,
fontsize = 12,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
];
UL [
shape = box,
label = "Main",
];
}
subgraph cluster1 {
label="LogicDLL";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color = "#AFAF4F",
fillcolor = "#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.05,
];
U [
shape = box,
label = "User",
];
ID [
shape = box,
style = filled,
label = "UserRepositoryInterface",
];
U -> ID [
tailport = s,
headport = n,
];
}
subgraph cluster2 {
label = "DataAccessDLL";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color = "#AFAF4F",
fillcolor = "#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.05,
];
D [
shape = box,
style = filled,
label = "UserRepository",
];
}
// Relationships
UL -> U [
weight = 1000,
];
ID -> D [
dir=back,
weight = 1000,
];
}
digraph G {
// Setting
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
bgcolor = white,
fontcolor = Black,
fontsize = 14,
style = "filled",
rankdir = TB,
splines = polyline,
ranksep = 1.0,
nodesep = 0.5,
compound = true,
newrank = true,
];
node [
colorscheme = "rdylbu9"
style = "solid,filled",
fontsize = 14,
fontname = "Migu 1M",
color = 9,
fillcolor = 6,
];
edge [
style = solid,
fontsize = 14,
fontcolor = white,
fontname = "Migu 1M",
color = gray,
labelfloat = true,
labeldistance = 2.5,
labelangle = 70,
];
// Elements
subgraph cluster0 {
label = "UserPackage";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color = "#AFAF4F",
fillcolor = "#FDFDD7",
fontcolor = Black,
fontsize = 12,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
];
UL [
shape = box,
label = "Main",
];
}
subgraph cluster1 {
label="LogicDLL";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color = "#AFAF4F",
fillcolor = "#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.01,
];
U [
shape = box,
label = "User",
];
URI [
shape = box,
style = filled,
label = "UserRepositoryInterface",
];
U -> URI [
headport = n,
tailport = s,
weight = 1000,
];
}
subgraph cluster2 {
label = "DataAccessDLL";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color = "#AFAF4F",
fillcolor = "#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.05,
];
UR [
shape = box,
style = filled,
label = "UserRepository",
];
}
{ rank = same; UL; U; }
// Relationships
URI -> UR [
dir=back,
weight = 1000,
];
UL -> U [
headport = w,
tailport = e,
//lhead = "cluster1",
//ltail = "cluster0",
weight = 0,
];
UL -> UR [
headport = w,
tailport = e,
//lhead = "cluster2",
//ltail = "cluster0",
weight = 0,
];
}
#!/bin/zsh
notnums=2
nums=4
for num in {1..${notnums}}
do
dot -T png NOT-DI${num}.dot -o NOT-DI${num}.png
done
for num in {1..${nums}}
do
dot -T png DI${num}.dot -o DI${num}.png
done
sleep 1
open NOT-DI{1..${notnums}}.png DI{1..${nums}}.png
digraph G {
// Setting
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
bgcolor=white,
fontcolor = Black,
fontsize = 14,
style = "filled",
rankdir = TB,
splines = polyline,
ranksep = 1.0,
nodesep = 0.9
];
node [
colorscheme = "rdylbu9"
style = "solid,filled",
fontsize = 14,
fontname = "Migu 1M",
color = 9,
fillcolor = 6,
];
edge [
style = solid,
fontsize = 14,
fontcolor = white,
fontname = "Migu 1M",
color=gray,
labelfloat = true,
labeldistance = 2.5,
labelangle = 70
];
// Elements
subgraph cluster0 {
label="UserPackage";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 12,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
UL [
shape=box,
label="Main"
];
}
subgraph cluster1 {
label="LogicDLL";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
U [
shape=box,
label="User"
];
}
subgraph cluster2 {
label="DataAccessDLL";
graph [
fontname = "Migu 1M",
labelloc = "t",
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
D [
shape=box,
style=filled,
label="UserRepository"
];
}
// Relationships
UL -> U [
];
U -> D [
];
}
digraph G {
// Setting
graph [
fontname = "Migu 1M",
labelloc = t,
labeljust = "c",
bgcolor=white,
fontcolor = Black,
fontsize = 14,
style = "filled",
rankdir = TB,
splines = polyline,
ranksep = 1.0,
nodesep = 0.9,
newrank = true,
];
node [
colorscheme = "rdylbu9"
style = "solid,filled",
fontsize = 14,
fontname = "Migu 1M",
color = 9,
fillcolor = 6,
];
edge [
style = solid,
fontsize = 14,
fontcolor = white,
fontname = "Migu 1M",
color=gray,
labelfloat = true,
labeldistance = 2.5,
labelangle = 70
];
// Elements
subgraph cluster0 {
label="UserPackage";
graph [
fontname = "Migu 1M",
labelloc = t,
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 12,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
UL [
shape=box,
label="Main"
];
}
subgraph cluster1 {
label="LogicDLL";
graph [
fontname = "Migu 1M",
labelloc = t,
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
U [
shape=box,
label="User"
];
}
subgraph cluster2 {
label="DataAccessDLL";
graph [
fontname = "Migu 1M",
labelloc = t,
labeljust = "c",
color="#AFAF4F",
fillcolor="#FDFDD7",
fontcolor = Black,
fontsize = 14,
style = "filled,rounded",
rankdir = TB,
splines = spline,
ranksep = 1.0,
nodesep = 0.9
];
D [
shape=box,
style=filled,
label="UserRepository"
];
}
{ rank = source; }
// Relationships
UL-> U [
];
U -> D [
dir=back,
];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment