From: Jim Weirich Date: 2006-03-15T00:07:04+09:00 Subject: Re: abstract method in Ruby I've been following the discussion on abstract method declaration with some interest. It seems to me that the ideal implementation should not only throw an exception on unimplemented methods, but also must pass Matz's example (implementation provided by base classes) and the method_missing example. With that in mind, I started to develope an implementation of abstract_method in a test first way, where you write a test case and then only implement enough code to make the test pass. Then you write another test case and only add enough code to make that test pass. Repeat as needed. With that in mind, I came up with 5 test cases: -- BEGIN UNIT TESTS ------------------------------------------------ require 'test/unit' require 'abstract_method' class TestAbstractMethod < Test::Unit::TestCase # This is the basic use case for abstract methods where the abstract # method is declared in the parent class and redefined in the child # class. def test_can_override_abstract_method parent = Class.new { abstract_method :foo } child = Class.new(parent) { def foo :foo_result end } assert_equal :foo_result, child.new.foo end # Here we make sure that not implementing the abstract method in the # child class will cause an exception when the method is invoked on # the child object. We don't particularly care what exception is # thrown, but the exception message must mention the missing method # by name. def test_unimplemented_abstract_method_throws_exception parent = Class.new { abstract_method :foo } child = Class.new(parent) { } begin child.new.foo fail "Oops" rescue Exception => ex assert_match /\bfoo\b/, ex.message end end # Now we make sure that our implementation passes Matz's example # where an abstract method in a mixin is actually implemented in the # base class. We need to make sure that the mixin doesn't hide the # implemented behavior. def test_abstract_method_in_mixin_may_be_implemented_in_base_class abstract_mixin = Module.new { abstract_method :foo } parent = Class.new { def foo :foo_result end } child = Class.new(parent) { include abstract_mixin } assert_equal :foo_result, child.new.foo end # This is a similar scenario to the previous test case where we make # sure the abstract declaration doesn't interfer with implemented # behavior. This time the implemented behavior is provided by the # method missing technique. def test_abstract_method_may_be_implemented_by_method_missing parent = Class.new { abstract_method :foo } child = Class.new(parent) { def method_missing(sym, *args, &block) :foo_result end } assert_equal :foo_result, child.new.foo end # Finally we want to ensure that +abstract_method+ can take multiple # method names, and that the method names may be either strings or # symbols. def test_abstract_method_may_take_multiple_string_or_symbol_arguments parent = Class.new { abstract_method :foo, "bar", "baz" } child = Class.new(parent) { def foo :foo_result end def bar :bar_result end } assert_equal :foo_result, child.new.foo assert_equal :bar_result, child.new.bar begin child.new.baz fail "Oops" rescue Exception => ex assert_match /\bbaz\b/, ex.message end end end -- END UNIT TESTS ------------------------------------------------- And here is the implementation that came of that exercise: -- BEGIN ABSTRACT METHOD IMPLEMENTATION --------------------------- class Module def abstract_method(*method_names) end end -- END ABSTRACT METHOD IMPLEMENTATION ----------------------------- -- -- Jim Weirich -- Posted via http://www.ruby-forum.com/.