Two approaches to interfaces in Ruby:
Using inheritance in a cleaner way. It allows abstract classes, but needs a custom inheritance chain and only supports 1 interface.
module Interface
attr_accessor :is_abstract
def abstract
@is_abstract = true
end
def is_interface
!@is_abstract && self.singleton_class.included_modules.include?(Interface)
end
# returns the list of implemented methods in its inheritance chain
def implemented_methods
result = self.instance_methods(false)
parent = self.ancestors[1]
result += parent.implemented_methods unless parent.is_interface
result
end
# applies both to interfaces and abstract classes
def inherited(subclass)
# look for first parent non abstract class
# assuming it represents an interface
candidate = self
while !candidate.is_interface
candidate = candidate.ancestors[1]
end
intmethods = candidate.instance_methods(false)
TracePoint.trace(:end) do |trace|
if trace.self == subclass
trace.disable
actualmethods = subclass.implemented_methods
intmethods.each do |name|
unless actualmethods.include?(name) || subclass.is_abstract
raise "No method '#{name}' defined in #{subclass}, required by #{candidate}"
end
end
end
end
end
end
Defining an interface
class Beer
extend Interface
def cold; end
def tasty; end
end
Abstract implementation
class BaseBeer < Beer
abstract
# nothing happens because we are abstract
def cold
puts 'hello'
end
end
> :cold
Uncomplete implementation class:
class Mahou < Beer
end
> RuntimeError: No method 'cold' defined in Mahou, required by Beer
Uncomplete child class
class Heineken < BaseBeer
# it should complain in 'tasty', as 'cold' has been already defined in parent class
end
> RuntimeError: No method 'tasty' defined in Cruzcampo, required by Beer
Complete implementation:
class Cruzcampo < BaseBeer
def tasty
true
end
end
Using a custom DSL implements
to define implemented interfaces. Gives more flexibility, allowing to specify multiple interfaces, and not forcing any custom inheritance:
module InterfaceSupport
attr_accessor :is_abstract, :interfaces
def implements(*interfaces)
@interfaces ||= []
@interfaces = (@interfaces + interfaces).uniq
TracePoint.trace(:end) do |trace|
if trace.self == self
trace.disable
validate_interfaces(self)
end
end
end
# validate interfaces to child classes
def inherited(subclass)
subclass.interfaces ||= []
TracePoint.trace(:end) do |trace|
if trace.self == subclass
trace.disable
validate_interfaces(subclass)
end
end
end
def validate_interfaces(clazz)
actualmethods = clazz.implemented_methods
implemented = clazz.interfaces
implemented.each do |interface|
intmethods = interface.instance_methods(false)
intmethods.each do |name|
unless actualmethods.include?(name) || clazz.is_abstract
raise "No method '#{name}' defined in #{clazz}, required by #{interface}"
end
end
end
end
# returns the list of implemented methods in its inheritance chain
def implemented_methods
result = self.instance_methods - Object.methods
parent = self.ancestors[1]
result += parent.implemented_methods unless parent == Object
result.uniq
end
# returns the list of implemented interfaces in its inheritance chain
def interfaces(include_ancestors = true)
result = @interfaces || []
return result unless include_ancestors
parent = self.ancestors[1]
result += parent.interfaces unless parent == Object
result.uniq
end
def implements?(interface)
interfaces.include?(interface)
end
def abstract
@is_abstract = true
end
end
Define an interface
class Beer
def cold; end
def tasty; end
end
Abstract implementation
class BaseBeer
extend InterfaceSupport
implements(Beer)
abstract
def cold
true
end
end
Uncomplete class:
class Heineken < BaseBeer
# it should complain in 'tasty', as 'cold' has been already defined in parent class
end
=> RuntimeError: No method 'tasty' defined in Heineken, required by Beer
Complete implementation:
class Cruzcampo < BaseBeer
def tasty
true
end
end
Multiple interfaces:
module Bottlelled
def recycle; end
end
class Mahou < BaseBeer
implements(Bottlelled)
end
=> RuntimeError: No method 'recycle' defined in Mahou, required by Bottlelled
Let's define it, we still need to define the missing method in BaseBeer:
class Mahou < BaseBeer
implements(Bottlelled)
def recycle
true
end
end
> RuntimeError: No method 'tasty' defined in Mahou, required by Beer
Mahou.interfaces(false)
=> [Bottlelled]
Mahou.interfaces
=> [Bottlelled, Beer]