Created
September 6, 2009 20:12
-
-
Save thinkerbot/181961 to your computer and use it in GitHub Desktop.
A DSL pattern supporting inheritance
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'benchmark' | |
# A basic version of a dsl that registers key-value pairs onto a Class. When | |
# looking up a key-value pair, Classes that use the dsl will check the | |
# registry of each ancestor in order, as if looking up a method. | |
# | |
# Call cache_ancestors! to cache the ancestry and speedup lookup. | |
module Dsl | |
def self.extended(base) | |
base.registry ||= {} | |
base.cache = nil | |
end | |
def inherited(base) | |
base.registry ||= {} | |
base.cache = nil | |
end | |
attr_accessor :registry | |
attr_accessor :cache | |
def set(key, value) | |
registry[key] = value | |
end | |
def value(key) | |
each_ancestor do |ancestor| | |
if ancestor.registry.has_key?(key) | |
return ancestor.registry[key] | |
end | |
end | |
end | |
def cache_ancestors! | |
@cache = ancestors | |
end | |
def each_ancestor | |
(cache || ancestors).each {|ancestor| yield(ancestor)} | |
end | |
end | |
# Now the benchmarks, first without caching (ie regenerate ancestors each | |
# time), then with caching to model performance with an each_ancestor method. | |
puts "Benchmark without cache" | |
class A | |
extend Dsl | |
set(:one, 1) | |
end | |
class B < A | |
set(:two, 2) | |
end | |
class C < B | |
set(:three, 3) | |
end | |
Benchmark.bm(20) do |x| | |
n = 100000 | |
x.report "A.value(:one)" do | |
n.times { A.value(:one) } | |
end | |
x.report "B.value(:two)" do | |
n.times { B.value(:two) } | |
end | |
x.report "C.value(:three)" do | |
n.times { C.value(:three) } | |
end | |
end | |
puts | |
puts "Benchmark with cache" | |
A.cache_ancestors! | |
B.cache_ancestors! | |
C.cache_ancestors! | |
Benchmark.bm(20) do |x| | |
n = 100000 | |
x.report "A.value(:one)" do | |
n.times { A.value(:one) } | |
end | |
x.report "B.value(:two)" do | |
n.times { B.value(:two) } | |
end | |
x.report "C.value(:three)" do | |
n.times { C.value(:three) } | |
end | |
x.report "Array.new" do | |
n.times { Array.new } | |
end | |
end | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Models a DSL that can be included into a class or module while | |
# supporting inheritance where parents can dynamically modify | |
# children (much as when you add a method to a parent class). | |
module Dsl | |
module ClassMethods | |
UNDEFINED = Object.new | |
def self.initialize(base) | |
base.registry ||= {} | |
end | |
attr_accessor :registry | |
def set(key, value) | |
registry[key] = value | |
end | |
def value(key) | |
each_ancestor do |ancestor| | |
if ancestor.registry.has_key?(key) | |
value = ancestor.registry[key] | |
return value == UNDEFINED ? nil : value | |
end | |
end | |
end | |
def values | |
values = {} | |
ancestors.reverse.each do |ancestor| | |
next unless ancestor.kind_of?(ClassMethods) | |
ancestor.registry.each_pair do |key, value| | |
values[key] = value unless value == UNDEFINED | |
end | |
end | |
values | |
end | |
def remove(key) | |
unless registry.has_key?(key) | |
raise NameError.new("#{key.inspect} not set on #{self}") | |
end | |
registry.delete(key) | |
end | |
def undefine(key) | |
unless values.has_key?(key) | |
raise NameError.new("#{key.inspect} not defined in #{self}") | |
end | |
registry[key] = UNDEFINED | |
end | |
private | |
def inherited(base) | |
ClassMethods.initialize(base) | |
super | |
end | |
def each_ancestor | |
yield(self) | |
blank, *ancestors = self.ancestors | |
ancestors.each do |ancestor| | |
yield(ancestor) if ancestor.kind_of?(ClassMethods) | |
end | |
nil | |
end | |
end | |
module ModuleMethods | |
module_function | |
def included(base) | |
base.extend ClassMethods | |
base.extend ModuleMethods unless base.kind_of?(Class) | |
# initialize any class variables | |
ClassMethods.initialize(base) | |
end | |
end | |
extend ModuleMethods | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'benchmark' | |
require 'dsl' | |
class A | |
include Dsl | |
set :def, :a | |
end | |
class B < A | |
set :def, :b | |
end | |
class X | |
include Dsl | |
set :def, :x | |
end | |
class Y < X | |
set :def, :y | |
end | |
Benchmark.bm(20) do |x| | |
n = 1000000 | |
x.report "A.value(:undef)" do | |
n.times { A.value(:undef) } | |
end | |
x.report "B.value(:undef)" do | |
n.times { B.value(:undef) } | |
end | |
x.report "A.value(:def)" do | |
n.times { A.value(:def) } | |
end | |
x.report "B.value(:def)" do | |
n.times { B.value(:def) } | |
end | |
x.report "X.value(:undef)" do | |
n.times { X.value(:undef) } | |
end | |
x.report "Y.value(:undef)" do | |
n.times { Y.value(:undef) } | |
end | |
x.report "X.value(:def)" do | |
n.times { X.value(:def) } | |
end | |
x.report "Y.value(:def)" do | |
n.times { Y.value(:def) } | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'test/unit' | |
require 'dsl' | |
# run assumption tests assuring this model is correct | |
require 'module_test' | |
require 'misc_test' | |
class DslModuleTest < Test::Unit::TestCase | |
module X | |
include Dsl | |
set :key_x, :x | |
end | |
module Y | |
include X | |
set :key_y, :y | |
end | |
class A | |
include Y | |
set :key_a, :a | |
end | |
class B < A | |
set :key_b, :b | |
end | |
def test_methods_from_included_module_are_available_in_module | |
assert_equal :x, A.value(:key_x) | |
assert_equal :y, A.value(:key_y) | |
assert_equal :a, A.value(:key_a) | |
assert_equal :x, B.value(:key_x) | |
assert_equal :y, B.value(:key_y) | |
assert_equal :a, B.value(:key_a) | |
assert_equal :b, B.value(:key_b) | |
end | |
end | |
class DslModifiedModuleTest < Test::Unit::TestCase | |
module X | |
include Dsl | |
set :key_x, :x | |
end | |
module Y | |
include X | |
set :key_y, :y | |
end | |
class A | |
include Y | |
set :key_a, :a | |
end | |
class B < A | |
set :key_b, :b | |
end | |
###################################################### | |
# late include into module X, and define a new method | |
module LateInModule | |
include Dsl | |
set :key_late_in_module, :late_in_module | |
end | |
module X | |
include LateInModule | |
set :key_late_x, :late_x | |
end | |
###################################################### | |
# late include into class A, and define a new method | |
module LateInClass | |
include Dsl | |
set :key_late_in_class, :late_in_class | |
end | |
class A | |
include LateInClass | |
set :key_late_a, :late_a | |
end | |
###################################################### | |
# define a class after late include | |
class DefinedAfterLateInclude | |
include X | |
end | |
###################################################### | |
# inherit a class after late include | |
class InheritAfterLateInclude < A | |
end | |
def test_late_inclusion_works_for_classes_but_not_modules | |
assert_equal :x, A.value(:key_x) | |
assert_equal :y, A.value(:key_y) | |
assert_equal :a, A.value(:key_a) | |
assert_equal :late_x, A.value(:key_late_x) | |
assert_equal :late_a, A.value(:key_late_a) | |
assert_equal nil, A.value(:key_method_late_in_module) | |
assert_equal :late_in_class, A.value(:key_late_in_class) | |
assert_equal :x, B.value(:key_x) | |
assert_equal :y, B.value(:key_y) | |
assert_equal :a, B.value(:key_a) | |
assert_equal :b, B.value(:key_b) | |
assert_equal :late_x, B.value(:key_late_x) | |
assert_equal :late_a, B.value(:key_late_a) | |
assert_equal nil, B.value(:key_method_late_in_module) | |
assert_equal :late_in_class, B.value(:key_late_in_class) | |
assert_equal :x, DefinedAfterLateInclude.value(:key_x) | |
assert_equal :late_x, DefinedAfterLateInclude.value(:key_late_x) | |
assert_equal :late_in_module, DefinedAfterLateInclude.value(:key_late_in_module) | |
assert_equal :x, InheritAfterLateInclude.value(:key_x) | |
assert_equal :y, InheritAfterLateInclude.value(:key_y) | |
assert_equal :a, InheritAfterLateInclude.value(:key_a) | |
assert_equal :late_x, InheritAfterLateInclude.value(:key_late_x) | |
assert_equal :late_a, InheritAfterLateInclude.value(:key_late_a) | |
assert_equal nil, InheritAfterLateInclude.value(:key_method_late_in_module) | |
assert_equal :late_in_class, InheritAfterLateInclude.value(:key_late_in_class) | |
end | |
end | |
class DslRemovalTest < Test::Unit::TestCase | |
module X | |
include Dsl | |
set :key_x, :x | |
set :key_y, :y | |
set :key_z, :z | |
remove :key_x | |
end | |
module Y | |
include X | |
end | |
class Z | |
include Y | |
end | |
class A | |
include Dsl | |
set :key_a, :a | |
set :key_b, :b | |
remove :key_a | |
end | |
class B < A | |
end | |
class C < B | |
set :key_a, :A | |
set :key_b, :B | |
end | |
def test_remove_removes_a_defined_key_in_self_and_subclasses | |
assert_equal nil, Z.value(:key_x) | |
assert_equal :y, Z.value(:key_y) | |
assert_equal :z, Z.value(:key_z) | |
assert_equal nil, A.value(:key_a) | |
assert_equal :b, A.value(:key_b) | |
assert_equal nil, B.value(:key_a) | |
assert_equal :b, B.value(:key_b) | |
end | |
def test_removed_keys_can_be_redefined | |
assert_equal :A, C.value(:key_a) | |
assert_equal :B, C.value(:key_b) | |
end | |
def test_remove_raises_error_for_key_not_defined_in_self | |
err = assert_raises(NameError) { X.send(:remove, :key_x) } | |
assert_equal ":key_x not set on DslRemovalTest::X", err.message | |
err = assert_raises(NameError) { Y.send(:remove, :key_x) } | |
assert_equal ":key_x not set on DslRemovalTest::Y", err.message | |
err = assert_raises(NameError) { Z.send(:remove, :key_z) } | |
assert_equal ":key_z not set on DslRemovalTest::Z", err.message | |
err = assert_raises(NameError) { B.send(:remove, :key_b) } | |
assert_equal ":key_b not set on DslRemovalTest::B", err.message | |
end | |
end | |
class DslUndefTest < Test::Unit::TestCase | |
module X | |
include Dsl | |
set :key_x, :x | |
set :key_y, :y | |
set :key_z, :z | |
undefine :key_x | |
end | |
module Y | |
include X | |
undefine :key_y | |
end | |
class Z | |
include Y | |
undefine :key_z | |
end | |
class A | |
include Dsl | |
set :key_a, :a | |
set :key_b, :b | |
undefine :key_a | |
end | |
class B < A | |
undefine :key_b | |
end | |
class C < B | |
set :key_a, :A | |
set :key_b, :B | |
end | |
def test_undef_key_removes_a_defined_key_in_self_and_subclasses | |
assert_equal nil, Z.value(:key_x) | |
assert_equal nil, Z.value(:key_y) | |
assert_equal nil, Z.value(:key_z) | |
assert_equal nil, A.value(:key_a) | |
assert_equal :b, A.value(:key_b) | |
assert_equal nil, B.value(:key_a) | |
assert_equal nil, B.value(:key_b) | |
end | |
def test_undef_keys_can_be_redefined | |
assert_equal :A, C.value(:key_a) | |
assert_equal :B, C.value(:key_b) | |
end | |
def test_undef_key_raises_error_for_key_not_defined_anywhere_in_ancestry | |
err = assert_raises(NameError) { X.send(:undefine, :key_x) } | |
assert_equal ":key_x not defined in DslUndefTest::X", err.message | |
err = assert_raises(NameError) { Y.send(:undefine, :key_unknown) } | |
assert_equal ":key_unknown not defined in DslUndefTest::Y", err.message | |
err = assert_raises(NameError) { Z.send(:undefine, :key_unknown) } | |
assert_equal ":key_unknown not defined in DslUndefTest::Z", err.message | |
err = assert_raises(NameError) { B.send(:undefine, :key_unknown) } | |
assert_equal ":key_unknown not defined in DslUndefTest::B", err.message | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'test/unit' | |
# Tests that constants from included modules do not cause internal conflicts | |
class ConstantResolutionTest < Test::Unit::TestCase | |
module A | |
module ClassMethods | |
def m | |
ClassMethods | |
end | |
end | |
def self.included(base) | |
class << base; include ClassMethods; end | |
end | |
end | |
module B | |
module ClassMethods | |
def n | |
ClassMethods | |
end | |
end | |
def self.included(base) | |
class << base; include ClassMethods; end | |
end | |
end | |
class C | |
include A | |
include B | |
end | |
def test_class_methods_dont_conflict | |
assert_equal A::ClassMethods, C.m | |
assert_equal B::ClassMethods, C.n | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'test/unit' | |
OBJECT_ANCESTORS = if RUBY_VERSION < "1.9" | |
[Object, Kernel] | |
else | |
[Object, PP::ObjectMixin, Kernel, BasicObject] | |
end | |
class ModuleTest < Test::Unit::TestCase | |
module X | |
def method_x; :x; end | |
end | |
module Y | |
include X | |
def method_y; :y; end | |
end | |
class A | |
include Y | |
def method_a; :a; end | |
end | |
class B < A | |
def method_b; :b; end | |
end | |
def test_inclusion_adds_module_to_ancestors | |
assert_equal [X], X.ancestors | |
assert_equal [Y, X], Y.ancestors | |
assert_equal [A, Y, X] + OBJECT_ANCESTORS, A.ancestors | |
assert_equal [B,A, Y, X] + OBJECT_ANCESTORS, B.ancestors | |
end | |
def test_methods_from_included_module_are_available_in_module | |
assert_equal :x, A.new.method_x | |
assert_equal :y, A.new.method_y | |
assert_equal :a, A.new.method_a | |
assert_equal :x, B.new.method_x | |
assert_equal :y, B.new.method_y | |
assert_equal :a, B.new.method_a | |
assert_equal :b, B.new.method_b | |
end | |
end | |
# Demonstrates you CAN include a module into a class after it has been | |
# defined; the methods propagate down the inheritance chain. You | |
# CANNOT include a module in a previously included module and expect | |
# the same. | |
class ModifiedModuleTest < Test::Unit::TestCase | |
module X | |
def method_x; :x; end | |
end | |
module Y | |
include X | |
def method_y; :y; end | |
end | |
class A | |
include Y | |
def method_a; :a; end | |
end | |
class B < A | |
def method_b; :b; end | |
end | |
###################################################### | |
# late include into module X, and define a new method | |
module LateInModule | |
def method_late_in_module; :late_in_module; end | |
end | |
module X | |
include LateInModule | |
def method_late_x; :late_x; end | |
end | |
###################################################### | |
# late include into class A, and define a new method | |
module LateInClass | |
def method_late_in_class; :late_in_class; end | |
end | |
class A | |
include LateInClass | |
def method_late_a; :late_a; end | |
end | |
###################################################### | |
# define a class after late include | |
class DefinedAfterLateInclude | |
include X | |
end | |
###################################################### | |
# inherit a class after late include | |
class InheritAfterLateInclude < A | |
end | |
def test_ancestors_are_assessed_on_each_call | |
assert A.ancestors.object_id != A.ancestors.object_id | |
end | |
def test_late_inclusion_works_for_classes_but_not_modules | |
# ok LateInModule | |
assert_equal [X, LateInModule], X.ancestors | |
# no LateInModule !! | |
assert_equal [Y, X], Y.ancestors | |
# no LateInModule !! | |
assert_equal [A, LateInClass, Y, X] + OBJECT_ANCESTORS, A.ancestors | |
# no LateInModule !! | |
assert_equal [B, A, LateInClass, Y, X] + OBJECT_ANCESTORS, B.ancestors | |
# ok LateInModule | |
assert_equal [DefinedAfterLateInclude, X, LateInModule] + OBJECT_ANCESTORS, DefinedAfterLateInclude.ancestors | |
# still no LateInModule !! | |
assert_equal [InheritAfterLateInclude, A, LateInClass, Y, X] + OBJECT_ANCESTORS, InheritAfterLateInclude.ancestors | |
assert_equal :x, A.new.method_x | |
assert_equal :y, A.new.method_y | |
assert_equal :a, A.new.method_a | |
assert_equal :late_x, A.new.method_late_x | |
assert_equal :late_a, A.new.method_late_a | |
assert_equal false, A.new.respond_to?(:method_late_in_module) | |
assert_equal :late_in_class, A.new.method_late_in_class | |
assert_equal :x, B.new.method_x | |
assert_equal :y, B.new.method_y | |
assert_equal :a, B.new.method_a | |
assert_equal :b, B.new.method_b | |
assert_equal :late_x, B.new.method_late_x | |
assert_equal :late_a, B.new.method_late_a | |
assert_equal false, B.new.respond_to?(:method_late_in_module) | |
assert_equal :late_in_class, B.new.method_late_in_class | |
assert_equal :x, DefinedAfterLateInclude.new.method_x | |
assert_equal :late_x, DefinedAfterLateInclude.new.method_late_x | |
assert_equal :late_in_module, DefinedAfterLateInclude.new.method_late_in_module | |
assert_equal :x, InheritAfterLateInclude.new.method_x | |
assert_equal :y, InheritAfterLateInclude.new.method_y | |
assert_equal :a, InheritAfterLateInclude.new.method_a | |
assert_equal :late_x, InheritAfterLateInclude.new.method_late_x | |
assert_equal :late_a, InheritAfterLateInclude.new.method_late_a | |
assert_equal false, InheritAfterLateInclude.new.respond_to?(:method_late_in_module) | |
assert_equal :late_in_class, InheritAfterLateInclude.new.method_late_in_class | |
end | |
end | |
class MethodRemovalTest < Test::Unit::TestCase | |
module X | |
def method_x; :x; end | |
def method_y; :y; end | |
def method_z; :z; end | |
remove_method :method_x | |
end | |
module Y | |
include X | |
end | |
class Z | |
include Y | |
end | |
class A | |
def method_a; :a; end | |
def method_b; :b; end | |
remove_method :method_a | |
end | |
class B < A | |
end | |
class C < B | |
def method_a; :A; end | |
def method_b; :B; end | |
end | |
def test_remove_method_removes_a_defined_method_in_self_and_subclasses | |
assert_equal false, Z.new.respond_to?(:method_x) | |
assert_equal true, Z.new.respond_to?(:method_y) | |
assert_equal true, Z.new.respond_to?(:method_z) | |
assert_equal false, A.new.respond_to?(:method_a) | |
assert_equal true, A.new.respond_to?(:method_b) | |
assert_equal false, B.new.respond_to?(:method_a) | |
assert_equal true, B.new.respond_to?(:method_b) | |
end | |
def test_removed_methods_can_be_redefined | |
assert_equal :A, C.new.method_a | |
assert_equal :B, C.new.method_b | |
end | |
def test_remove_method_raises_error_for_method_not_defined_in_self | |
err = assert_raises(NameError) { X.send(:remove_method, :method_x) } | |
assert_equal "method `method_x' not defined in MethodRemovalTest::X", err.message | |
err = assert_raises(NameError) { Y.send(:remove_method, :method_x) } | |
assert_equal "method `method_x' not defined in MethodRemovalTest::Y", err.message | |
err = assert_raises(NameError) { Z.send(:remove_method, :method_z) } | |
assert_equal "method `method_z' not defined in MethodRemovalTest::Z", err.message | |
err = assert_raises(NameError) { B.send(:remove_method, :method_b) } | |
assert_equal "method `method_b' not defined in MethodRemovalTest::B", err.message | |
end | |
end | |
class UndefMethodTest < Test::Unit::TestCase | |
module X | |
def method_x; :x; end | |
def method_y; :y; end | |
def method_z; :z; end | |
undef_method :method_x | |
end | |
module Y | |
include X | |
undef_method :method_y | |
end | |
class Z | |
include Y | |
undef_method :method_z | |
end | |
class A | |
def method_a; :a; end | |
def method_b; :b; end | |
undef_method :method_a | |
end | |
class B < A | |
undef_method :method_b | |
end | |
class C < B | |
def method_a; :A; end | |
def method_b; :B; end | |
end | |
def test_undef_method_removes_a_defined_method_in_self_and_subclasses | |
assert_equal false, Z.new.respond_to?(:method_x) | |
assert_equal false, Z.new.respond_to?(:method_y) | |
assert_equal false, Z.new.respond_to?(:method_z) | |
assert_equal false, A.new.respond_to?(:method_a) | |
assert_equal true, A.new.respond_to?(:method_b) | |
assert_equal false, B.new.respond_to?(:method_a) | |
assert_equal false, B.new.respond_to?(:method_b) | |
end | |
def test_undefined_methods_can_be_redefined | |
assert_equal :A, C.new.method_a | |
assert_equal :B, C.new.method_b | |
end | |
def test_undef_method_raises_error_for_method_not_defined_anywhere_in_ancestry | |
err = assert_raises(NameError) { X.send(:undef_method, :method_x) } | |
assert_equal "undefined method `method_x' for module `UndefMethodTest::X'", err.message | |
err = assert_raises(NameError) { Y.send(:undef_method, :method_unknown) } | |
assert_equal "undefined method `method_unknown' for module `UndefMethodTest::Y'", err.message | |
err = assert_raises(NameError) { Z.send(:undef_method, :method_unknown) } | |
assert_equal "undefined method `method_unknown' for class `UndefMethodTest::Z'", err.message | |
err = assert_raises(NameError) { B.send(:undef_method, :method_unknown) } | |
assert_equal "undefined method `method_unknown' for class `UndefMethodTest::B'", err.message | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment