制約フレームワークを書かないと連載させてもらえない Web 雑誌があるようなので、昨日こんなの書いてた。
静的解析もできないし、ドキュメンテーションにも活きないので各方面の識者から怒られそうだが {}
で書けるという見た目の点で許してほしい。
あとはつっこまれるとしたら、継承したときに事前条件、事後条件が強化される/緩和されるのあたり。全部やると大変なんすよ。invariant もないですね (method_added でイベントフックしてすべてのメソッドコールへ prepend かぶせるとかすればできるかな)。
class BeerLover
extend Contracts
attr_accessor :age
def initialize(age)
self.age = age
end
assert_pre :drink, -> { age >= 20 } # can call instance methods
def drink
'beer'
end
end
class Calcurator
extend Contracts
assert_pre :tasu, ->(a, b) { a > 0 and b > 0 }
def tasu(a, b)
a + b
end
assert_post :hiku, ->(result) { result >= 0 }
def hiku(a, b)
a - b
end
end
実装はこんなかんじ。
module Contracts
class AssertionError < RuntimeError; end
def self.extended(obj)
obj.include AssertionHelper
obj.const_set('PreAssertions', Module.new)
obj.const_set('PostAssertions', Module.new)
obj.prepend obj.const_get('PreAssertions'), obj.const_get('PostAssertions')
end
def assert_precondition(method_name, assertion)
self::PreAssertions.instance_eval do
define_method(method_name) do |*args|
raise AssertionError.new(method_name) unless self.instance_exec(*args, &assertion)
super *args
end
end
end
alias_method :assert_pre, :assert_precondition
def assert_postcondition(method_name, assertion)
self::PostAssertions.instance_eval do
define_method(method_name) do |*args|
result = super *args
raise AssertionError.new(method_name) unless self.instance_exec(result, &assertion)
end
end
end
alias_method :assert_post, :assert_postcondition
module AssertionHelper
def assert condition
unless condition
raise Contracts::AssertionError
end
end
end
end