開放閉鎖原則(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_algorithmenumeratorメソッドを実装しているクラスのインスタンスであることを要求している。

デザインパターンのクラス図で、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インターフェースを実装したクラス、つまり、ツリーの要素を列挙する振る舞いを持つクラスならどれでもよく、実体のクラスがなんであるかは気にせずに済む。

まとめ

  • 開放閉鎖原則とは簡単にいえば、バリエーションの追加は最小の修正のみで容易にできなければならないということ

開放閉鎖原則が重要になるのはバリエーションがある場合である。他のクラスに委譲するからといって即座にインターフェースを定義するようなことはしてはならない。拡張される委譲なのかそうでないのかを区別しなければならない。