Skip to content

Instantly share code, notes, and snippets.

@jiparis
Last active April 26, 2021 16:01
Show Gist options
  • Save jiparis/6cca5d60bc1a7c47d91121fc575f82aa to your computer and use it in GitHub Desktop.
Save jiparis/6cca5d60bc1a7c47d91121fc575f82aa to your computer and use it in GitHub Desktop.
Interfaces and abstract classes in Ruby

Two approaches to interfaces in Ruby:

First aproach

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

Second approach

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]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment