I was recently asked whether Ruby had a way of honoring the Interface Segregation Principle (ISP). ISP says that a client of an API should only depend on the methods it needs/cares about. In languages like Java, ISP is easy to manage with static typing and judicious use of Interfaces. Ruby doesn't have the same thing, though. Consider the following example:
class Drone | |
def launch_missiles_at(x, y, z) | |
end | |
def take_picture_of(x, y, z) | |
end | |
def take_off | |
puts 'taking off...' | |
end | |
def land | |
puts 'landing...' | |
end | |
def fly_to(x, y, z) | |
puts "flying to x: #{x} y: #{y} z: #{z}" | |
end | |
end |
Let's say that you want to put the controls to fly and land a drone in the hands of someone with Secret clearance, but restrict access to operations like taking pictures and launching missiles to a client with a different security clearance. The problem with the Drone class is that the methods for doing all of the operations are visible to anyone with a reference to a Drone instance. ISP says that the lower-level clearance client should only be able to see a subset of all of the methods on a Drone instance. We can't control this on the instance itself very easily because the class is defined to expose all methods to anyone with a reference to an instance of it.
What you can do in Ruby is present access to an object via a proxy to it using the Forwardable module. In the example below, the Pilotable class tells Forwardable where to send invocations of the methods "take_off", "land", and "fly_to". Any other method invocation will blow up. An object of Pilotable class doesn't expose the reference to an aircraft it is wrapping, instead it simply provides a means of invoking a subset of its methods.
require 'forwardable' | |
class Pilotable | |
extend Forwardable | |
def_delegators :@aircraft, :take_off, :land, :fly_to | |
def initialize(aircraft) | |
@aircraft = aircraft | |
end | |
end | |
client_of_drone = Pilotable.new(Drone.new) | |
client_of_drone.take_off # works | |
client_of_drone.fly_to 10, 201, 20 # works | |
client_of_drone.launch_missiles_at 10, 201, 0 #(NoMethodError): undefined method `launch_missiles_at' |
This is the essence of the Interface Segregation Principle: provide a client with an interface limited only to the methods needed in the context of the client's interaction.