Friday, July 05, 2013

Interface Segregation Principle in Ruby

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
view raw drone.rb hosted with ❤ by GitHub

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'
view raw pilotable.rb hosted with ❤ by GitHub

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.