As promised, here’s the first installment of my Fractal Design series. I’d like to kick things off with what might seem to be an odd choice for Rubyists: the interface segregation principle.
A Brief Introduction to the ISP
Interface Segregation is one of the SOLID design principles, and states:
Clients should not be forced to depend upon interfaces that they do not use. (source)
The idea is that when a class implements an interface, it should only get methods that it needs. When followed, you get higher cohesion and less coupling in your code.
In Ruby, we don’t have explicit interface constructs – but we do have plenty of violations of this principle.
I’ve seen (and/or written) any number of medium- or larger-sized Ruby applications where there’s a utility module, filled with all sorts of methods used here or there in the application. Such modules are created with the best of intentions – people just want to stay DRY, after all, so they extract methods that appear in multiple classes or views, but they end up dropping those into an ever-expanding, unfocused module that then has to be included all over the place even when only a fraction of it is used in any particular context.
Sure, many classes that include Toolbox might use the log method – but how many of them need to format credit card numbers, or select a random element from an array? Here’s a better approach:
Granted, there’s more code now, but this ISP-following refactoring allows us to include just the pieces of Toolbox that we need where we need them. The result is a cleaner, more comprehensible, and more testable system.
For a very long while, the biggest ISP offender in Ruby was ActiveSupport. It was the utility module pattern writ large – including it anywhere brought in a huge amount of code, the vast majority of which was completely irrelevant to the reason you included it somewhere.
In Rails 3, ActiveSupport was refactored and modularized so that you could include just the pieces you need as you needed them:
And millions of gems, non-Rails applications, and more breathed a sigh of relief.
Sticking with Rails, there’s one violation of ISP still present in the current releases by default: automatic controller-based helper generation:
I can count on one hand the number of controllers I’ve seen that use the same helper methods across even a majority of their actions, much less all of them. It’s a clear violation – controller-scoped helpers are automatically included in all of that controller’s views, regardless of whether they are needed or not.
Luckily, this is easy to fix. In your config/application.rb, add a config.generators block:
And voila, the offending helpers are no longer created:
Of course, many of you have probably been ignoring the auto-generated helpers for years, creating smaller, more focused, more ISP-compliant ones the whole time. To you, I say: Good show!
Libraries and Applications
When you move from classes and modules to the larger scales of libraries and applications, the ISP doesn’t seem to map that closely to the code – we move from discussing implementing interfaces to providing and consuming them. Nevertheless, I think there’s still some good to come out of thinking this way.
And ISP-friendly gem, for instance, should provide a single chunk of related functionality, without a ton of extraneous stuff thrown in (hey, old ActiveSupport – there you are again!). An ISP-friendly system of applications would pass around APIs where each application did something relatively specific… but at a certain point this approach transitions from being an example of interface segregation to illustrating the Single Responsibility Principle (which we’ll be getting to soon enough).