© Matthias Noback 2018
Matthias NobackPrinciples of Package Designhttps://doi.org/10.1007/978-1-4842-4119-6_2

2. The Open/Closed Principle

Matthias Noback1 
(1)
Zeist, The Netherlands
 
The Open/Closed principle says that1:

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

Take a look at the GenericEncoder class shown in Listing 2-1 and Figure 2-1. Notice the branching inside the encodeToFormat() method that is needed to choose the right encoder based on the value of the $format argument.
class GenericEncoder
{
    public function encodeToFormat($data, string $format): string
    {
        if ($format === 'json') {
            $encoder = new JsonEncoder();
        } elseif ($format === 'xml') {
            $encoder = new XmlEncoder();
        } else {
            throw new InvalidArgumentException('Unknown format');
        }
        $data = $this->prepareData($data, $format);
        return $encoder->encode($data);
    }
}
Listing 2-1

The GenericEncoder Class

../images/471891_1_En_2_Chapter/471891_1_En_2_Fig1_HTML.png
Figure 2-1

The initial situation

Let’s say you want to use the GenericEncoder to encode data to the Yaml format, which is currently not supported by the encoder. The obvious solution would be to create a YamlEncoder class for this purpose and then add an extra condition inside the existing encodeToFormat() method shown in Listing 2-2.
class GenericEncoder
{
    public function encodeToFormat($data, string $format): string
    {
        if (...) {
            // ...
        } elseif (...) {
            // ...
        } elseif ($format === 'yaml') {
            $encoder = new YamlEncoder();
        } else {
            // ...
        }
        // ...
    }
}
Listing 2-2

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.

Let’s take a look at the prepareData()method of the same class. Just like the encodeToFormat() method, it contains some more format-specific logic (see Listing 2-3).
class GenericEncoder
{
    public function encodeToFormat($data, string $format): string
    {
        // ...
        $data = $this->prepareData($data, $format);
        // ...
    }
    private function prepareData($data, string $format)
    {
        switch ($format) {
            case 'json':
                $data = $this->forceArray($data);
                $data = $this->fixKeys($data);
                // fall through
            case 'xml':
                $data = $this->fixAttributes($data);
                break;
            default:
                throw new InvalidArgumentException(
                    'Format not supported'
                );
        }
        return $data;
    }
}
Listing 2-3

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

This is a list of characteristics of a class that may not be open for extension:
  • 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.

This new class might as well be an implementation of the Abstract Factory design pattern2. 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; we only want to retrieve an object with an encode($data) method. So we need an interface for such format-specific encoders. And then, we make sure every existing format-specific encoder implements this interface (see Listing 2-4 and Figure 2-2).
/**
 * Interface for format-specific encoders
 */
interface EncoderInterface
{
    public function encode($data): string;
}
class JsonEncoder implements EncoderInterface
{
    // ...
}
class XmlEncoder implements EncoderInterface
{
    // ...
}
class YamlEncoder implements EncoderInterface
{
    // ...
}
Listing 2-4

The EncoderInterface and Its Implementation Classes

../images/471891_1_En_2_Chapter/471891_1_En_2_Fig2_HTML.jpg
Figure 2-2

Introducing the EncoderInterface

Now we can move the creation logic of format-specific encoders to a class with just this responsibility. Let’s call it EncoderFactory (see Listing 2-5).
class EncoderFactory
{
    public function createForFormat(
        string $format
    ) : EncoderInterface {
        if ($format === 'json') {
            return new JsonEncoder();
        } elseif ($format === 'xml') {
            return new XmlEncoder();
        } elseif (...) {
            // ...
        }
        throw new InvalidArgumentException('Unknown format');
    }
}
Listing 2-5

The EncoderFactory Class

Then we have to make sure that the GenericEncoder class does not create any format-specific encoders anymore. Instead, it should delegate this job to the EncoderFactory class, which it receives as a constructor argument (see Listing 2-6).
class GenericEncoder
{
    private $encoderFactory;
    public function __construct(
        EncoderFactory $encoderFactory
    ) {
        $this->encoderFactory = $encoderFactory;
    }
    public function encodeToFormat($data, string $format): string
    {
        $encoder = $this->encoderFactory
                        ->createForFormat($format);
        $data = $this->prepareData($data, $format);
        return $encoder->encode($data);
    }
}
Listing 2-6

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?

It seems there is some low-hanging fruit here. As you may have noticed there is a striking symmetry inside the switch statement: for the json format, a JsonEncoder instance is being returned, for the xml format an XmlEncoder, etc. If your programming language supports dynamic class names, like PHP does, this could be easily refactored into something that is not hard-coded anymore:
$class = ucfirst(strtolower($format)) . 'Encoder';
if (!class_exists($class)) {
    throw new InvalidArgumentException('Unknown format');
}
Yes, this is in fact equivalent code. It’s shorter and it removes the need for a switch statement. It even introduces a bit more flexibility: in order to extend its behavior you don’t need to modify the code anymore. In the case of the new encoder for the Yaml format, we only need to create a new class that follows the naming convention: YamlEncoder. And that’s it. However, using dynamic class names to make a class extensible like this introduces some new problems and doesn’t fix some of the existing problems:
  • 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

A first step we could take is to apply the Dependency Inversion principle (see Chapter 5) by defining an interface for encoder factories. The EncoderFactory we already have should implement this new interface and the constructor argument of the GenericEncoder should have the interface as its type (see Listing 2-7 and Figure 2-3).
interface EncoderFactoryInterface
{
   public function createForFormat(
        string $format
   ): EncoderInterface;
}
class EncoderFactory implements EncoderFactoryInterface
{
    // ...
}
class GenericEncoder
{
    public function __construct(
        EncoderFactoryInterface $encoderFactory
    ) {
        // ...
    }
    // ...
}
Listing 2-7

An Interface for the Factory

../images/471891_1_En_2_Chapter/471891_1_En_2_Fig3_HTML.jpg
Figure 2-3

Introducing the EncoderFactoryInterface

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. It will be easy for users of this class to completely replace the encoder factory, which is now a proper dependency that gets injected as a constructor argument of type EncoderFactoryInterface (see Listing 2-8).
class MyCustomEncoderFactory implements EncoderFactoryInterface
{
    // ...
}
$encoder = new GenericEncoder(new MyCustomEncoderFactory());
Listing 2-8

Replacing the Encoder Factory with a Custom One

By introducing the interface, we’ve provided the user with another very powerful option. Maybe they aren’t looking to completely replace the existing EncoderFactory, but they just want to enhance its behavior. For example, let’s say they want 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 (see Listing 2-9). You could say that the new factory “wraps” the old one. The technical term for this is “decoration”.
class MyCustomEncoderFactory implements EncoderFactoryInterface
{
    private $fallbackFactory;
    private $serviceLocator;
    public function __construct(
        ServiceLocatorInterface $serviceLocator,
        EncoderFactoryInterface $fallbackFactory
    ) {
        $this->serviceLocator = $serviceLocator;
        $this->fallbackFactory = $fallbackFactory;
    }
    public function createForFormat($format): EncoderInterface
    {
        if ($this->serviceLocator->has($format . '.encoder') {
            return $this->serviceLocator
                        ->get($format . '.encoder');
        }
        return $this->fallbackFactory->createForFormat($format);
    }
}
Listing 2-9

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.

There are several ways to make a factory like EncoderFactory open for extension. I’ve chosen to inject specialized factories into the EncoderFactory, as shown in Listing 2-10.
class EncoderFactory implements EncoderFactoryInterface
{
    private $factories = [];
    /**
     * Register a callable that returns an instance of
     * EncoderInterface for the given format.
     *
     * @param string $format
     * @param callable $factory
     */
    public function addEncoderFactory(
        string $format,
        callable $factory
    ): void {
        $this->factories[$format] = $factory;
    }
    public function createForFormat(
        string $format
    ): EncoderInterface {
        $factory = $this->factories[$format];
        // the factory is a callable
        $encoder = $factory();
        return $encoder;
    }
}
Listing 2-10

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.

This fully dynamic and extensible implementation allows developers to add as many format-specific encoders as they want. Listing 2-11 shows what injecting the format-specific encoders looks like.
$encoderFactory = new EncoderFactory();
$encoderFactory->addEncoderFactory(
    'xml',
    function () {
        return new XmlEncoder();
    }
);
$encoderFactory->addEncoderFactory(
    'json',
    function () {
        return new JsonEncoder();
    }
);
$genericEncoder = new GenericEncoder($encoderFactory);
$data = ...;
$jsonEncodedData = $genericEncoder->encode($data, 'json');
Listing 2-11

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

We have put some effort into implementing a nice abstract factory for encoders, but the GenericEncoder class still has this ugly switch statement for preparing the data before it is encoded (see Listing 2-12).
class GenericEncoder
{
    private function prepareData($data, string $format)
    {
        switch ($format) {
            case 'json':
                $data = $this->forceArray($data);
                $data = $this->fixKeys($data);
                // fall through
            case 'xml':
                $data = $this->fixAttributes($data);
                break;
            default:
                throw new InvalidArgumentException(
                    'Format not supported'
                );
        }
        return $data;
    }
}
Listing 2-12

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.

So let’s delegate the “prepare data” logic to the specific encoders by adding a method called prepareData($data) to the EncoderInterface and calling it in the encodeToFormat() method of the GenericEncoder (see Listing 2-13).
interface EncoderInterface
{
    public function encode($data);
    /**
     * Do anything that is required to prepare the data for
     * encoding it.
     *
     * @param mixed $data
     * @return mixed
     */
    public function prepareData($data);
}
class GenericEncoder
{
    public function encodeToFormat($data, string $format): string
    {
        $encoder = $this->encoderFactory
                        ->createForFormat($format);
        /*
         * Preparing the data is now a responsibility of the
         * format-specific encoder
         */
        $data = $encoder->prepareData($data);
        return $encoder->encode($data);
    }
}
Listing 2-13

Moving prepareData() to EncoderInterface

In the case of the JsonEncoder class , this would look like Listing 2-14.
class JsonEncoder implements EncoderInterface
{
    public function encode($data): string
    {
        // ...
    }
    public function prepareData($data)
    {
        $data = $this->forceArray($data);
        $data = $this->fixKeys($data);
        return $data;
    }
}
Listing 2-14

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.

So instead, we should make preparing the data part of the actual encoding process inside the format-specific encoder. Each encoder should decide for itself if and how it needs to prepare the provided data before encoding it. Listing 2-15 shows what this looks like for the JSON encoder.
class JsonEncoder implements EncoderInterface
{
    public function encode($data): string
    {
        $data = $this->prepareData($data);
        return json_encode($data);
    }
    private function prepareData($data)
    {
        // ...
        return $data;
    }
}
Listing 2-15

Making the Preparation of the Data Part of the encode() Method

In this scenario, the prepareData() method is a private method. It is not part of the public interface of format-specific encoders, because it will only be used internally. The GenericEncoder is not supposed to call it anymore. We only have to remove it from the EncoderInterface , which now exposes a very clean API (see Listing 2-16).
interface EncoderInterface
{
    public function encode($data): string;
}
Listing 2-16

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.

By doing all this, we made GenericEncoder actually generic. When we want to add support for another format we don’t need to modify its code anymore. We only need to inject another callable in the encoder factory. This makes both classes (GenericEncoder and EncoderFactory) open for extension and closed for modification. In fact, maybe there is no longer a need for a GenericEncoder class anymore, if you consider what it looks like now (see Listing 2-17). We might ask users to directly call the encoder factory themselves.
class GenericEncoder
{
    public function encodeToFormat($data, string $format): string
    {
        return $this->encoderFactory
                    ->createForFormat($format)
                    ->encode($data);
    }
}
Listing 2-17

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.

This is what it means to apply the Open/Closed principle: we make sure that objects are open for extension, but closed for modification. Several techniques to accomplish this have been discussed:
  • 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.46.36