開放閉鎖原則(Open-Closed Principle)
ソフトウェアの構成要素(クラス、モジュール、関数など)は拡張に対して開いて(オープン:Open)いて、修正に対して閉じて(クローズド:Closed)いなければならない。
アジャイルソフトウェア開発の奥義 第2版 p.128
簡単にいえば、バリエーションの追加は最小の修正のみで容易にできなければならないということである。
開放閉鎖原則はオブジェクト指向の原則ではあるが、まず、メソッドレベルの話をする。単純な例として、コード値を日本語に対応付けてViewに出力したいケースを考える。
<% case code %>
<% when 1 %>
正常
<% when 2 %>
警告あり
<% when 3 %>
停止
<% end %>
当然、codeの種類が増えればこのViewは変更しなければならないし、複数箇所に存在するならすべての箇所を修正しなければならない。通常、ヘルパーメソッドを作ってこの問題に対処する。
def code_to_s(code)
case code
when 1
'正常'
when 2
'警告あり'
when 3
'停止'
end
end
<%= code_to_s(code) %>
これによって、Viewを修正することなく(修正に対して閉じていて)codeの種類を増やせるようになった(拡張に対して開いている)。
次にクラスレベルの話をする。ツリー構造のデータセットから値を検索するメソッドを考える。
class Tree
attr_accessor :children, :value
def initialize(value)
@children = []
@value = value
end
def find(value)
return self if @value == value
@children.each do |child|
result = child.find(value)
return result unless result.nil?
end
nil
end
end
# サンプルコード
root = Tree.new(0)
3.times { |i| root.children << Tree.new(i) }
3.times { |i| root.children.last.children << Tree.new(10 + i) }
3.times { |i| root.children.last.children.last.children << Tree.new(100 + i) }
root.find(100)
このTree#find
メソッドは深さ優先探索で実装されている。また、循環参照を考慮されていない実装になっている。
深さ優先探索と幅優先探索を使い分けたくなった場合、find
メソッドに幅優先探索のコードを追加した上で引数に切り替えフラグを追加しなければならなくなる。引数を追加するため、呼び出し元にも修正が波及してしまう。
class Tree
def find(value, type)
case type
when :depth_first
# 深さ優先探索のコード
when :breadth_first
# 幅優先探索のコード
end
end
end
そこで、探索アルゴリズムをクラスにして、インスタンスを渡すように変更する。
class Tree
attr_accessor :children, :value, :search_algorithm
def initialize(value, search_algorithm = nil)
@children = []
@value = value
@search_algorithm = search_algorithm
end
def find(value)
@search_algorithm.enumerator(self).each do |child|
return child if child.value == value
end
nil
end
end
# 深さ優先探索クラス
class DepthFirstSearch
# 逐次読み込みできるようにすべきだが本題ではないので配列化している
def enumerator(tree)
list = []
list << tree
tree.children.each do |child|
list += self.enumerator(child)
end
list
end
end
root = Tree.new(0, DepthFirstSearch.new)
3.times { |i| root.children << Tree.new(i) }
これによって、Tree
クラスもfind
メソッドも変更することなく(修正に対して閉じていて)探索アルゴリズムを増やせるようになった(拡張に対して開いている)。
もし、探索件数に上限を持たせた探索を追加したくなったとしてもenumerator
メソッドを定義したクラスを用意すれば、DepthFirstSearch
クラスと同じように適用できる。
ここで「enumerator
メソッドを定義したクラスを用意する」というのがポイントになる。Rubyはダックタイピングな言語なので意識的にコードで表現することはないが、Tree
クラスはsearch_algorithm
がenumerator
メソッドを実装しているクラスのインスタンスであることを要求している。
デザインパターンのクラス図で、interfaceが出てくるのはこの要求を表現するためである(例:Strategyパターン)。
公称型の言語であるJavaで表現すると以下のようになる。
interface EnumerableBySearchAlgorithm {
public <E> List<E> enumerator(Tree<E> tree);
}
class DepthFirstSearch implements EnumerableBySearchAlgorithm {
public <E> List<E> enumerator(Tree<E> tree) {
// ...
}
}
class Tree<E> {
public Tree(E value, EnumerableBySearchAlgorithm searchAlgorithm) {
// ...
}
}
EnumerableBySearchAlgorithm
インターフェースを実装したクラス、つまり、ツリーの要素を列挙する振る舞いを持つクラスならどれでもよく、実体のクラスがなんであるかは気にせずに済む。
まとめ
- 開放閉鎖原則とは簡単にいえば、バリエーションの追加は最小の修正のみで容易にできなければならないということ
開放閉鎖原則が重要になるのはバリエーションがある場合である。他のクラスに委譲するからといって即座にインターフェースを定義するようなことはしてはならない。拡張される委譲なのかそうでないのかを区別しなければならない。