Engineers build business. See why software teams at Atlassian, PayPal, TripAdvisor, Adobe, and more use GitPrime to be more data-driven. Request a demo today.

This article shows an example of how the application of the Open/Closed Principle improved the design of a real project, the open source library
PHPUnit_Selenium. These design concepts apply to every object-oriented language, including Java, Ruby or even C++.

Some theory

The Open Closed Principle, part of SOLID set, states that software should be open for extension and at the same time closed for modification. Implementations of this principle in OO languages usually use inheritance from interfaces or classes to support the addition of new features via the addition of new classes.

Since the alternative to the addition of classes is the modification of existing code, OCP leads us to touching the existing application as less as possible. You cannot break things if you do not modify them.

Background for this example

PHPUnit_Selenium has a Session object representing a browser opened by Selenium and that can be used to perform tests. There are many commands to support, from title that retrieves the <title> of the page, to url which may be called with or without arguments (for accessing or mutating the current location)

There are details related to each command: since the PHP process communicates with Selenium with a REST-like API, it may have to use a POST or GET request, depending on the command type. And the parameters may be processed differently:

sometimes you have to pass a complex array with options.

sometimes a single argument, but Selenium accepts it only as a complex array. For example, url must be specified as an array with a single element: array('url' => ...). A set of characters to type is even more difficult to manage, as a string like 'Hi' has to be posted as array('H', 'i').

Before the application of OCP

To avoid writing a full method for each new command to support, they are supported with __call() as magic methods on the Session object (this would be equivalent to a method callCommand($commandName, ...)):

there are several branches that depend on the number of arguments: 0 means a GET command, while at least one argument (a complex array) results in a POST one.

other branches depend on what is the command: url is special and should wrap its only parameter into an array.

In general, this solution doesn't scale to add more commands, as the __call() method will grow to hundreds of lines. Every time a new command is added, it would gain another branch and maybe break the previous commands cases: even with a test suite in place, I'd rather avoid regressions, if only for the time they take to be fixed.

After the application of OCP

The Session class now lists the available commands as an array of methods that can create a Command object:

$this->commandFactories is an array of anonymous functions indexed by command name. Each of these functions can create the relevant command object with two parameters: the $jsonParameters containing configuration for Selenium, and the command URL which is used as a target for execution (making an HTTP request to /session/123/title).

This fields can be injected, or substituted with a CommandFactory object to outsource completely the command list concern.

Initially all these anonymous Factory Methods were identical, with only the class name changing. However, the SessionCommand_Url class has an additional parameter (the base url of the website we're on) and so I felt including the more general solution in this article would be more complete. It's obvious that as more and more commands are added it becomes more likely that some of them require different arguments, and so the Command object creation cannot be identical for all cases.

The base Command class is extended by all Command objects. An interface would be less coupled, and I will go for that in the case third-party Command objects have to be supported.

Conclusion

The code is not a mess anymore: adding a command means writing a separate new class, and adding a single line in Session in the list of Factory Methods.

Some conditionals are still there, for example to decide when a command should use POST or GET; however, they are confined in the environment of a single command and this simplifies them a lot (only a single branch is needed.)

Finally, remember that while applying this version of the Command pattern, you can usually start by having just a list of class names to instantiate; after a while you can add indirection to allow for a clean creation of them (in my case having the Url command being passed the additional parameter instead of pulling it from some singleton).

All in all, you can add IFs until a class explodes, or you can extract some interface or abstract class to manage new features with small, brand new objects.

Engineers build business. See why software teams at Atlassian, PayPal, TripAdvisor, Adobe, and more use GitPrime to be more data-driven. Request a demo today.