Extension without Modification
Build objects that are easily extensible and resilient to change
Building software often requires changing and extending existing behavior. This need presents itself in different ways. One example is the need to take different actions depending on some input. This can be a painful process if our code is not prepared for change.
Let’s look at a concrete example. Imagine we are working on an application in charge of sending notifications to users via an API. When there is only one API to interact with, the following class meets our needs:
If our application is successful, we’ll be required to support multiple channels of communication. No problem, we can modify our class to support multiple channels:
Our changes satisfy the new requirements. But, our class has taken on new responsibilities. It used to be in charge of one thing only: notifying users via an API. Now, it is also in charge of building the correct API object for the given channel.
In order to build these objects, our class knows what each one of them requires. Adding support for additional channels will increase the complexity and brittleness of this class. It has too many reasons to change.
We can improve our design by applying SOLID object-oriented principles and design patterns. First, let’s extract the API specifics into small objects, each with a single responsibility:
Each Messenger
object is in charge of interfacing with one API. Now we can change UserNotifier
to use these objects:
Extracting Messenger
objects decouples theUserNotifier
from the specifics of the APIs.
UserNotifier
no longer knows how to build API objects. It has fewer reasons to change than before. Still, adding new messaging channels results in changes to this class.
A pattern that’s very helpful when dealing with this type of structure is the Gang-of-Four factory method pattern. Let’s apply it to our code and see what it reveals:
Extracting the factory method makes it clear UserNotifier
has two responsibilities:
• Mapping channel
types to Messenger
objects.
• Asking the Messenger
to deliver a message.
Since we are trying to minimize the reasons for UserNotifier
to change, let’s apply the Single Responsibility Principle and extract the factory method into an object:
Then, UserNotifier
can delegate to MessengerFactory
to build the correct Messenger
object:
The UserNotifier
no longer knows how to build a Messenger
object or what channel
they correspond to. It only knows MessengerFactory
will return an object that responds to deliver
. This is an example of the Dependency Inversion Principle, where you rely on abstractions, not concretions.
The main benefit of extracting MessengerFactory
is that UserNotifier
no longer needs to change when adding new channels: Adding support for WhatsApp would require a new entry in MessengerFactory::MESSENGER_CLASS
and a WhatsAppMessenger
class.
All Messenger
classes have the same initialization requirement. Adding a BaseMessenger
class removes this duplication, simplifying the addition of new Messenger
types:
Let’s take a look at what we’ve done so far.
Our UserNotifier
class is in better shape now. However, all we’ve done is move the reason to change from UserNotifier
to MessengerFactory
. We can do better and completely remove the need for these classes to change when adding new channels.
A common approach in Ruby is to introduce a convention and use metaprogramming to build factory objects:
This approach works for our current requirements, but it has a few pitfalls:
• It prevents a global search from finding all references to a class.
• New keys or classes might not fit nicely with our convention: whats_app -> WhatsAppMessenger
.
• Changing a class name requires us to change all references to its corresponding factory key.
There is a different approach: Factory self-registration. Let’s generalize MessengerFactory
so that it no longer references Messenger
classes directly:
The main change to the MessengerFactory
is that it now allows classes to be registered on it. It no longer cares about what type of object it’s building. If it has a corresponding key in its registry, MessengerFactory
will build it.
Now, we want our Messenger
classes to register themselves on the factory. An elegant way to give our Messenger
classes this ability is via a module:
We can include MessengerFactoryRegistration
in the BaseMessenger
and configure a key on each Messenger
class:
Calling the corresponds_to
method registers the class on the factory, under the provided key. The correspondence between a class and its key is explicitly stated in the class definition.
Our MessengerFactory
is now open/closed: we can extend its functionality without having to modify it. Adding support for WhatsApp requires only a new class:
This approach provides the following benefits:
• It does not require a convention.
• Adding a new Messenger
class does not require other classes to change.
• Class names are decoupled from their factory keys. Renaming either one does not require the other to change.
My team and I use this pattern. Adding new functionality to our application usually only requires adding a new class and its corresponding tests. In fact, we have gotten so much value out of this pattern that we decided to extract it into an open source gem: Industrialist.
Industrialist provides the factory and self-registration functionality. All you have to do is configure your base class:
That’s it! We started by adding complexity to a simple class, giving it multiple responsibilities and multiple reasons to change. Then, we applied the single responsibility principle and the factory pattern to produce a design that’s resilient to changes and easily extensible. I bet each of these classes would be a joy to test as well!
This is the way my team writes software. If you’d like to know more about Entelo or our Engineering Team, please visit SourceCode, our blog about engineering in the recruiting industry.
I’d like to thank Alan Ridlehoover for introducing me to this pattern and pairing with me on it. I’d also like to thank Vijay Ram for his clarifying questions and comments.