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.