Enforcing Interfaces in Ruby

Ruby doesn't have the concept of an interface. Unlike say, PHP. In PHP you can specify that a class has to act like or implement specific methods. If your class fails to honor the contract of that interface and does not implement the methods of the interface, you get an error during runtime. Ruby does not have this. But we can ensure our code honors the contract of an interface by writing unit tests for our classes. I think about it in terms of "acts as". My class needs to act as a "notifier" and thus has to respond to calls to send_notification, for example.

The issue here is that you do not get a run time error if your class is supposed to act like a thing, but does not honor the contract of that thing by implementing those specific methods. If you assume that you can call a method on a class but fail to implement the method you will only get an error when your code tries to call a method that does not exist. While in PHP the contract is enforced during runtime, in Ruby you enforce it with testing.

Take for example a system that defines an array of notification objects and iterates over them and calls a method named send_notification on each of those objects. Each of those notification objects is assumed to act as a notififer and has to implement send_notification. To ensure that our notification classes act as notifiers we create tests to confirm that the contract is honored and the interface is implemented.

The first version of an interface test for two of our notification classes might look like:

class TestSlack < Minitest::Test
  def setup
    @slack = MyModule::Slack.new
  end

  def test_implements_the_sender_interface
    assert_respond_to(@slack, :send_notification)
  end
end

class TestSns < Minitest::Test
  def setup
    @sns = MyModule::Sns.new
  end

  def test_implements_the_sender_interface
    assert_respond_to(@sns, :send_notification)
  end
end

Executing our tests show that our classes correctly implement our interface and "acts as" a notifier. Our tests ensure that our notification objects implement the notificaiton sender interface.

A refactor of this would be to create a shared test module that we can include in our test classes. This is kinda like defining an interface and "implementing" that interface in our class.

Ruby is a very flexible object oriented programming language. We can extend the functionality of our classes through mixins, or Ruby modules. This applies to our test classes as well. This is called class composition and is really the Ruby way. Simply, you can include a module in a class and that class now has access to those methods defined in the module.

module NotificationSenderInterfaceTest
  def test_implements_the_sender_interface
    assert_respond_to(@object, :send_notification)
  end
end

class TestSlack < Minitest::Test
  include NotificationSenderInterfaceTest

  def setup
    @slack = @object = MyModule::Slack.new
  end
end

class TestSns < Minitest::Test
  include NotificationSenderInterfaceTest

  def setup
    @sns = @object = MyModule::Sns.new
  end
end

When I wish to create a new notification tool I first create a test class and include the interface tests required.

A shared set of test modules helps me to ensure my classes correctly implement an interface and act as the things they need to act as. It also keeps my tests DRY, and is also a pretty good way to document code through the tests.

Did you like this post? Let me know by sending me a message. Is there a topic you would like me to cover? Let me know about that too. I look forward to hearing from you!

Let's Connect!