protocol icon indicating copy to clipboard operation
protocol copied to clipboard

Method Protocols for Ruby Classes

== Protocol - Method Protocol Specifications in Ruby

=== Author

Florian Frank mailto:[email protected]

=== License

This is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License Version 2 as published by the Free Software Foundation: www.gnu.org/copyleft/gpl.html

=== Download

The latest version of protocol can be found at

  • http://rubyforge.org/frs/?group_id=4778

The homepage of this library is located at

  • http://protocol.rubyforge.org

=== Description

This library offers an implementation of protocols against which you can check the conformity of your classes or instances of your classes. They are a bit like Java Interfaces, but as mixin modules they can also contain already implemented methods. Additionally you can define preconditions/postconditions for methods specified in a protocol.

=== Usage

This defines a protocol named +Enumerating+:

Enumerating = Protocol do

Iterate over each element of this Enumerating class and pass it to the

+block+.

def each(&block) end

include Enumerable end

Every class, that conforms to this protocol, has to implement the understood messages (+each+ in this example - with no ordinary arguments and a block argument). The following would be an equivalent protocol definition:

Enumerating = Protocol do

Iterate over each element of this Enumerating class and pass it to the

+block+.

understand :each, 0, true

include Enumerable end

An example of a conforming class is the class +Ary+: class Ary def initialize @ary = [1, 2, 3] end

def each(&block) @ary.each(&block) end

conform_to Enumerating end

The last line (this command being the last line of the class definition is important!) of class +Ary+ conform_to Enumerating checks the conformance of +Ary+ to the +Enumerating+ protocol. If the +each+ method were not implemented in +Ary+ a CheckFailed exception would have been thrown, containing all the offending CheckError instances.

It also mixes in all the methods that were included in protocol +Enumerating+ (+Enumerable+'s instance methods). More examples of this can be seen in the examples sub directory of the source distribution of this library in file examples/enumerating.rb.

==== Template Method Pattern

It's also possible to mix protocol specification and behaviour implementation like this:

Locking = Protocol do specification # not necessary, because Protocol defaults to specification # mode already

def lock() end

def unlock() end

implementation

def synchronize
  lock
  begin
    yield
  ensure
    unlock
  end
end

end

This specifies a Locking protocol against which several class implementations can be checked against for conformance. Here's a FileMutex implementation:

class FileMutex def initialize @tempfile = Tempfile.new 'file-mutex' end

def path @tempfile.path end

def lock puts "Locking '#{path}'." @tempfile.flock File::LOCK_EX end

def unlock puts "Unlocking '#{path}'." @tempfile.flock File::LOCK_UN end

conform_to Locking end

The Locking#synchronize method is a template method (see http://en.wikipedia.org/wiki/Template_method_pattern), that uses the implemtented methods, to make block based locking possbile:

mutex = FileMutex.new mutex.synchronize do puts "Synchronized with '#{file.path}'." end

Now it's easy to swap the implementation to a memory based mutex implementation instead:

class MemoryMutex def initialize @mutex = Mutex.new end

def lock @mutex.lock end

def unlock @mutex.unlock end

conform_to Locking # actually Mutex itself would conform as well ;) end

To check an +object+ for conformity to the Locking protocol call Locking.check +object+ and rescue a CheckFailed. Here's an example class

class MyClass def initialize @mutex = FileMutex.new end

attr_reader :mutex

def mutex=(mutex) Locking.check mutex @mutex = mutex end end

This causes a CheckFailed exception to be thrown: obj.mutex = Object.new

This would not raise an exception: obj.mutex = MemoryMutex.new

And neither would this

obj.mutex = Mutex.new # => #<Mutex:0xb799a4f0 @locked=false, @waiting=[]>

because coincidentally this is true

Mutex.conform_to? Locking # => true

and thus Locking.check doesn't throw an exception. See the examples/locking.rb file for code.

==== Preconditions and Postconditions

You can add additional runtime checks for method arguments and results by specifying pre- and postconditions. Here is the classical stack example, that shows how:

StackProtocol = Protocol do def push(x) postcondition { top === x } postcondition { result === myself } end

def top() end

def size() end

def empty?() postcondition { size === 0 ? result : !result } end

def pop() s = size precondition { not empty? } postcondition { size === s - 1 } end end

Defining protocols and checking against conformance doesn't get in the way of Ruby's duck typing, but you can still use protocols to define, document, and check implementations that you expect from client code.