O : The Open/Closed Principle
You should be able to extend a class’s behavior without modifying it.
A unit of code can be considered “open for extension” when its behaviour can be easily changed without modifying it.
The fact that no actual modification is needed to change the behaviour of a unit of code makes it “closed” for modification.
Being able to extend a class’s behaviour doesn’t mean to actually extend that class by creating a subclass for it. Extension of a class means to influence its behavior from the outside and leave the class, or the entire class hierarchy, untouched.
Example
GenericEncoder
class the branching inside theencodeToFormat()
method is needed to choose the right encoder based on the value of theformat
argument.
to use the
GenericEncoder
to encode data to the Yaml format, an obious solution would be to create aYamlEncoder
class for this purpose and then add an extra condition inside the existingencodeToFormat()
methodeach time to add another format-specific encoder, the
GenericEncoder
class itself needs to be modified. We cannot change behaviour without modifying code. This is why theGenericEncoder
class cannot be considered open for extension and closed for modification.
Recognising 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
Delegate the responsibility of resolving the right encoder for the format to some other class.
This new class can be an implementation of the Abstract Factory design pattern.
The abstractness is represented by the fact that its
create()
method is bound to return an instance of a given interface.We don’t care about its actual class only want to retrieve an object with an
encode($data)
method. So we need an interface for such format-specific encoders.Ensure that the
GenericEncoder
class does not create any format-specific encoders anymore.It should delegate this job to the
EncoderFactory
class, which it receives as a constructor argument
EncoderFactory
class has the creation logic of format-specific encoders
There is still a hard-coded list of supported formats and their corresponding encoders. Since class names are still hard-coded. The EncoderFactory is closed against extension, its behavior can’t be extended without modifying its code.
Refactoring: Making the Abstract Factory Open for Extension
Apply the Dependency Inversion principle by defining an interface for encoder factories.
The EncoderFactory we already have should implement this new interface
the constructor argument of the GenericEncoder should have the interface as its type\
Replacing or Decorating the Encoder Factory
By making
GenericEncoder
depend on an interface instead of a class, we have added a first extension point to it.Users of this class can replace the encoder factory, which is now a proper dependency.
For example, to fetch the encoder for a given format from a service locator and fall back on the default EncoderFactory in case of an unknown format.
Using the interface, they can compose a new factory, which implements the required interface, but receives the original EncoderFactory as a constructor argument.
The new factory “wraps” the old one, technical term for this is “decoration”
Making EncoderFactory Itself Open for Extension
When a new format comes along, to keep using the same old
EncoderFactory
, without touching the code of the class itself is not possible because the creation logic of each of the encoders is hard-coded in the EncoderFactory class.it’s impossible to extend or change the behavior of the
EncoderFactory
class without modifying 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.
A way to solve this is by injecting specialized factories into the
EncoderFactory
For each format it is possible to inject a callable.
This dynamic and extensible implementation allows to add as many format-specific encoders as needed.
Introducing callable factories, have relieved the
EncoderFactory
from the responsibility of providing the right constructor arguments for each encoder.
Prefer Immutable Services
EncoderFactory
became a mutable service on addingaddEncoderFactory()
method to it.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.
It will be impossible to somehow get different results upon subsequent calls.
If 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.
Refactoring: Polymorphism
GenericEncoder
class has the switch statement for preparing the data before it is encoded but this responsibility should be on the format-specific encoders as they know everything about encoding data to their own format.
delegate the “prepare data” logic to the specific encoders by adding a method called
prepareData($data)
to the EncoderInterface and calling it in theencodeToFormat()
not a great solution as it introduces “temporal coupling”: before calling
encode()
always callprepareData()
Make preparing the data part of the actual encoding process inside the format-specific encoder.
Packages and the Open/Closed Principle
Applying the Open/Closed principle to classes will greatly benefit in the implementation of future requirements (or changed requirements).
When the behaviour of a class can be changed from the outside, without modifying its code, people will feel safe to do so.
A package will be used in many different projects and in many different circumstances.
the classes in a package should not be too specific and leave room for the details to be implemented in different ways.
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.
The Open/Closed principle is highly useful and should be applied widely and generously when designing classes that are bound to end up in a reusable package.
In practice, it means allowing classes to be configured by injecting different constructor arguments.
For collaborating objects extracted while applying the Single Responsibility principle, ensure these objects have a published interface, which allows users to decorate existing classes.
Applying the Open/Closed principle will make it possible to change the behaviour of any class in 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 final.
This decreases the number of possible use cases to consider when making a change to the class.
It will help keeping backward compatibility in the future, and give all the freedom to change any implementation detail of the class.
Last updated