契約による設計
呼び出し側の責任の事前条件、呼び出され側の責任の事後条件、オブジェクトの一生を通じて守られるクラス不変表明の3つからなるテクニック。
必ずしもコードで表現されるわけでなく、コメントなどによって指示されるケースもある。
事前条件は、メソッドを呼び出す時に呼び出し側が守らなければならないルールで、違反した場合にはどのような結果になるか保証されないことを意味する。
# 事前条件: pathは/files/からの相対パスで指定され、存在していること
def import_file(path)
# ...
end
通常は以下のように例外を発生させることになる。
# 事前条件: pathは/files/からの相対パスで指定され、存在していること
def import_file(path)
path = Pathname.new('/files/') / "./#{path}"
unless path.exist?
raise "指定されたファイルが/files/に存在しない: path => #{path}"
end
# ...
end
事後条件は、事前条件が守られてメソッドが呼び出された時に保証される結果である。
# 事前条件: pathは/files/からの相対パスで指定され、存在していること
# 事後条件: 処理されたファイルは削除されていること
def import_file(path)
path = Pathname.new('/files/') / "./#{path}"
unless path.exist?
raise "指定されたファイルが/files/に存在しない: path => #{path}"
end
# ...
end
後述するが、事後条件はテストコードでチェックすることができるので通常はプロダクトコードにはチェック処理を組み込まない。
クラス不変表明は、オブジェクトが生成されてから消滅するまで維持されていなければならない性質である。
# クラス不変表明: 0 <= @collection.length <= 10
# 10個の要素を持てるコレクション
class Collection
def initialize
@collection = []
end
# 事後条件: 追加できた場合は@collectionの最後にitemが追加される
# 追加できた場合はtrueを、追加できなかった場合はfalseを返す
def try_add(item)
return false if 10 < @collection.length
@collection << item
true
end
end
クラス不変表明もテストコードで工夫すればチェックすることができるので通常はプロダクトコードにはチェック処理を組み込まない。
ではどのように、事後条件とクラス不変表明をテストコードで表現するかだが、以下のようにすれば良い。
RSpec.describe Collection, type: :model do
let(:target) { described_class.new }
# インスタンス変数をメタプログラミングを使って取得している
let(:collection) { target.instance_variale_get(:@collection) }
# クラス不変表明テスト
def class_invariant_spec
expect(collection.length).to be >= 0
expect(collection.length).to be <= 10
end
describe '#try_add' do
# 事後条件テスト
def postcondition_spec(item)
expect(last_added_item).to eq item
end
let(:last_added_item) { collection.last }
it '10回目の実行までtrueを返す' do
10.times do |item|
expect(target.try_add(item)).to eq true
postcondition_spec(item)
class_invariant_spec
end
end
it '11回目以降の実行ではfalseを返す' do
10.times { |item| target.try_add(item) }
last_item = last_added_item
10.times do |item|
expect(target.try_add(item)).to eq false
postcondition_spec(last_item)
class_invariant_spec
end
end
end
end
事後条件はメソッドに紐づくものなので常にそのメソッドのテストにおいて記述するべきだが、クラス不変表明はテスト対象のメソッドに関連するものに限るかすべてのクラス不変表明をチェックするかは言語とプロジェクトの性質によって判断されるところだろう。
上記のようにRSpecの場合、メソッドとしてくくり出すことができるので、class_invariant_spec
やpostcondition_spec
のようにクラス不変表明と事後条件をメソッドにして呼び出すことでテスト記述をシンプルにできる。
事前条件については、呼び出し時にチェックしなければならないため、テストコードでチェックすることは不可能であり、チェックするのであればプロダクトコードに記述しなければならない。手放しでチェックロジックを組み込めば、本番環境においても実行されてしまいパフォーマンスに悪影響がでる可能性がある。あるいは、記録だけ残して本番環境ではそのまま動いて欲しいケースもある。
そのため、事前条件のチェックを組み込むのであれば、C言語のプリプロセッサのような仕組みを導入して環境ごとに実行するチェックを指定できるようにした方が良い。
Rails環境なら以下のようなコードを用意すればよいだろう。
# 各メソッド名は本番環境でどのように振る舞うかをイメージしている
class Precondition
class << self
# 本番環境でチェックする
def check
yield
self
end
# 本番環境でチェックし問題があればログに記録する
def logging
yield
self
rescue StandardError => e
Rails.logger.fatal(e.message)
raise unless Rails.env.production?
self
end
# 本番環境では忘れる=実行しない
def forget
return self if Rails.env.production?
yield
self
end
end
end
def import_file(path)
path = Pathname.new('/files/') / "./#{path}"
# 利用例。本番環境以外では例外が呼び出し元に投げられるが、本番環境ではログに記録されるだけ
Precondition.logging do
unless path.exist?
raise "指定されたファイルが/files/に存在しない: path => #{path}"
end
end
# ...
end
継承における契約による設計の取り扱いはリスコフの置換原則を参照。
参考文献:オブジェクト指向入門(Bertrand Meyer著 1990年出版) 7章 ソフトウェア構築への体系的アプローチ、Code Complete 第2版 上 完全なプログラミングを目指して 第8章 防御的プログラミング