You should be able to extend a class’s behavior without modifying it.
Again, a small linguistic jump has to be made from the name of the principle to its explanation: a unit of code can be considered “open for extension” when its behavior can be easily changed without modifying it. The fact that no actual modification is needed to change the behavior of a unit of code makes it “closed” for modification.
It should be noted that being able to extend a class’s behavior doesn’t mean you should actually extend that class by creating a subclass for it. Extension of a class means that you can influence its behavior from the outside and leave the class, or the entire class hierarchy, untouched.
A Class That Is Closed for Extension
The GenericEncoder Class
Adding Another Encoding Format
As you can imagine, each time you want to add another format-specific encoder, the GenericEncoder class itself needs to be modified: you cannot change its behavior without modifying its code. This is why the GenericEncoder class cannot be considered open for extension and closed for modification.
The prepareData() Method
The prepareData() method is another good example of code that is closed for extension since it is impossible to add support for another format without modifying the code itself. Besides, these kind of switch statements are not good for maintainability. When you would have to modify this code, for instance when you introduce a new format, it is likely that you would either introduce some code duplication or simply make a mistake because you overlooked the “fall-through” case.
Recognizing Classes that Violate the Open/Closed Principle
It contains conditions to determine a strategy.
Conditions using the same variables or constants are recurring inside the class or related classes.
The class contains hard-coded references to other classes or class names.
Inside the class, objects are being created using the new operator.
The class has protected properties or methods, to allow changing its behavior by overriding state or behavior.
Refactoring: Abstract Factory
We’d like to fix this bad design, which requires us to constantly dive into the GenericEncoder class to modify format-specific behavior. We first need to delegate the responsibility of resolving the right encoder for the format to some other class. When you think of responsibilities as reasons to change (see Chapter 1), this makes perfect sense: the logic for finding the right format-specific encoder is something which is likely to change, so it would be good to transfer this responsibility to another class.
The EncoderInterface and Its Implementation Classes
The EncoderFactory Class
The GenericEncoder Class Now Uses EncoderFactory
By leaving the responsibility of creating the right encoder to the encoder factory, the GenericEncoder now conforms to the Single Responsibility principle.
Using the encoder factory for fetching the right encoder for a given format means that adding an extra format-specific encoder does not require us to modify the GenericEncoder class anymore. We need to modify the EncoderFactory class instead.
But when we look at the EncoderFactory class, there is still an ugly hard-coded list of supported formats and their corresponding encoders. Even worse, class names are still hard-coded. This means that now the EncoderFactory is closed against extension. That is, its behavior can’t be extended without modifying its code. It thereby violates the Open/Closed principle.
Quick Refactoring Opportunity: Dynamic Class Names?
Introducing a naming convention only offers some flexibility for you as the maintainer of the code. When someone else wants to add support for a new format, they have to put a class in your namespace, which is possible, but not really user-friendly.
A much bigger issue: creation logic is still being reduced to new ...(). If, for instance, an encoder class has some dependencies, there is no way to inject them (e.g., as constructor arguments). We will address this issue next.
Refactoring: Making the Abstract Factory Open for Extension
An Interface for the Factory
Replacing or Decorating the Encoder Factory
Replacing the Encoder Factory with a Custom One
Decorating the Original EncoderFactory
Making EncoderFactory Itself Open for Extension
It’s great that users can now implement their own instance of EncoderFactoryInterface or decorate an existing instance. However, forcing the user to re-implement EncoderFactoryInterface just to add support for another format seems a bit inefficient. When a new format comes along, we want to keep using the same old EncoderFactory, but we want to support the new format without touching the code of the class itself. Also, if one of the encoders would need another object to fulfill its task, it’s currently not possible to provide that object as a constructor argument, because the creation logic of each of the encoders is hard-coded in the EncoderFactory class.
In other words, it’s impossible to extend or change the behavior of the EncoderFactory class without modifying it: the logic by which the encoder factory decides which encoder it should create and how it should do that for any given format can’t be changed from the outside. But it’s quite easy to move this logic out of the EncoderFactory class, thereby making the class open for extension.
Injecting Specialized Factories
For each format it is possible to inject a callable3. The createForFormat() method takes that callable, calls it, and uses its return value as the actual encoder for the given format.
Dynamic Definition of Encoder Factories
By introducing callable factories, we have relieved the EncoderFactory from the responsibility of providing the right constructor arguments for each encoder. In other words, we pushed knowledge about creation logic outside of the EncoderFactory , which makes it at once adhere to both the Single Responsibility principle and the Open/Closed principle.
Prefer Immutable Services
As you may have noticed, EncoderFactory suddenly became a mutable service when we add the addEncoderFactory() method to it. This was a convenient thing to do, but in practice it’ll be a smart to design a service to be immutable. Apply the following rule to achieve this:
After instantiation, it shouldn’t be possible to change any of a service’s properties.
The biggest advantage of a service being immutable is that its behavior won’t change on subsequent calls. It will be fully configured before its first usage. And it will be impossible to somehow get different results upon subsequent calls.
If you still prefer having separate methods to configure an object, make sure to not make these methods part of the published interface for the class. They are there only for clients that need to configure the object, not for clients actually using the objects. For example, a dependency injection container will call addEncoderFactory() while setting up a new instance of EncoderFactory, but regular clients, like GenericEncoder itself, will only call createForFormat().
Refactoring: Polymorphism
Revisiting prepareData()
Where should we put this format-specific data preparation logic? In other words, whose responsibility would it be to prepare data before encoding it? Is it something the GenericEncoder should do? No, because preparing the data is format-specific, not generic. Is it the EncoderFactory? No, because it only knows about creating encoders. Is it one of the format-specific encoders? Yes! They know everything about encoding data to their own format.
Moving prepareData() to EncoderInterface
An Example of a prepareData() Implementation
This is not a great solution, because it introduces something called “temporal coupling”: before calling encode() you always have to call prepareData(). If you don’t, your data may be invalid and not ready to be encoded.
Making the Preparation of the Data Part of the encode() Method
The EncoderInterface
Summarizing, the GenericEncoder we started with at the beginning of this chapter was quite specific. Everything was hard-coded, so it was impossible to change its behavior without modifying it. We first moved out the responsibility of creating the format-specific encoders to an encoder factory. Next we applied a bit of dependency inversion by introducing an interface for the encoder factory. Finally, we made the encoder factory completely dynamic: we allowed new format-specific encoder factories to be injected from the outside, i.e., without modifying the code of the encoder factory itself.
The GenericEncoder May No Longer Deserve to be a Class
Packages and the Open/Closed Principle
Applying the Open/Closed principle to classes in your project will greatly benefit the implementation of future requirements (or changed requirements) for that project. When the behavior of a class can be changed from the outside, without modifying its code, people will feel safe to do so. They won’t need to be afraid that they will break something. They won’t even need to modify existing unit tests for the class.
When it comes to packages, the Open/Closed principle is important for another reason. A package will be used in many different projects and in many different circumstances. This means that the classes in a package should not be too specific and leave room for the details to be implemented in different ways. And when behavior has to be specific (at some point a package has to be opinionated about something), it should be possible to change that behavior without actually modifying the code. Especially since most of the time that code cannot be changed by its users without cloning and maintaining the entire package themselves.
This is why the Open/Closed principle is highly useful and should be applied widely and generously when you are designing classes that are bound to end up in a reusable package. In practice, this means you allow classes to be configured by injecting different constructor arguments (also known as dependency injection ). For collaborating objects that you may have extracted while applying the Single Responsibility principle , make sure these objects have a published interface, which allows users to decorate existing classes.
Applying the Open/Closed principle everywhere will make it possible to change the behavior of any class in your package by switching out or decorating constructor arguments only. Since users should never have to rely on subclassing to override a class’s behavior anymore, this gives you the powerful option to mark all of them as final4. If you do this, you make it impossible for users to create subclasses for them. This decreases the number of possible use cases you need to consider when you make a change to the class. Effectively it will help you keep backward compatibility in the future, and give you all the freedom to change any implementation detail of the class.
Conclusion
Usually, if you want to change the behavior of a class, you’d need to modify its code. To prevent a class from being modified, in particular if that class is part of a package, you should build in options for changing the behavior of a class from the outside. In other words, it should be possible to extend its behavior without modifying any part of its code.
Apply the Single Responsibility principle and extract collaborating objects.
Inject collaborating objects as constructor arguments (dependency injection).
Provide interfaces for collaborating objects, thereby allowing the user to replace dependencies, or decorate them.
Mark classes as final, to make it impossible for the user to change the behavior of a class by extending it.