DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
  1. DZone
  2. Coding
  3. Languages
  4. Open/Closed Principle on real world code

Open/Closed Principle on real world code

Giorgio Sironi user avatar by
Giorgio Sironi
·
Jan. 12, 12 · Interview
Like (0)
Save
Tweet
Share
10.72K Views

Join the DZone community and get the full member experience.

Join For Free
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, ...)):

    public function __call($command, $arguments)
    {
        if (count($arguments) == 1) {
            if (is_string($arguments[0])) {
                $jsonParameters = array('url' => $this->baseUrl->addCommand($arguments[0])->getValue());
            } else if (is_array($arguments[0])) {
                $jsonParameters = $arguments[0];
            } else {
                throw new Exception("The argument should be an associative array or a single string.");
            }
            $response = $this->curl('POST', $this->sessionUrl->addCommand($command), $jsonParameters);
        } else if (count($arguments) == 0) {
            $response = $this->curl($this->preferredHttpMethod($command),
                                    $this->sessionUrl->addCommand($command));
        } else {
            throw new Exception('You cannot call a command with multiple method arguments.');
        }
        return $response->getValue();
    }

However this implementation is a mess:

  • 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:

    public function __construct(...)
    {
        $this->commandFactories = array(
            'acceptAlert' => $this->factoryMethod('PHPUnit_Extensions_Selenium2TestCase_SessionCommand_AcceptAlert'),
            'alertText' => $this->factoryMethod('PHPUnit_Extensions_Selenium2TestCase_SessionCommand_GenericAccessor'),
            'dismissAlert' => $this->factoryMethod('PHPUnit_Extensions_Selenium2TestCase_SessionCommand_DismissAlert'),
            'title' => $this->factoryMethod('PHPUnit_Extensions_Selenium2TestCase_SessionCommand_GenericAccessor'),
            'url' => function ($jsonParameters, $commandUrl) use ($baseUrl) {
                return new PHPUnit_Extensions_Selenium2TestCase_SessionCommand_Url($jsonParameters, $commandUrl, $baseUrl);
            }
        );
    }

    /**
     * @params string $commandClass     a class name, descending from
                                        PHPUnit_Extensions_Selenium2TestCase_Command
     * @return callable
     */
    private function factoryMethod($commandClass)
    {
        return function($jsonParameters, $url) use ($commandClass) {
            return new $commandClass($jsonParameters, $url);
        };
    }

    public function __call($commandName, $arguments)
    {
        $jsonParameters = $this->extractJsonParameters($arguments);
        $response = $this->driver->execute($this->newCommand($commandName, $jsonParameters));
        return $response->getValue();
    }

    /**
     * @return string
     */
    private function newCommand($commandName, $arguments)
    {
        if (isset($this->commandFactories[$commandName])) {
            $factoryMethod = $this->commandFactories[$commandName];
            $commandUrl = $this->sessionUrl->addCommand($commandName);
            $commandObject = $factoryMethod($arguments, $commandUrl);
            return $commandObject;
        }
        throw new BadMethodCallException("The command '$commandName' is not existent or not supported.");
    }

$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.

abstract class PHPUnit_Extensions_Selenium2TestCase_Command
{
    protected $jsonParameters;
    private $commandName;

    /**
     * @param array $jsonParameters     null in case of no parameters
     */
    public function __construct($jsonParameters,
                                PHPUnit_Extensions_Selenium2TestCase_URL $url)
    {
        $this->jsonParameters = $jsonParameters;
        $this->url = $url;
    }

    public function url()
    {
        return $this->url;
    }

    /**
     * @return string
     */
    abstract public function httpMethod();

    /**
     * @param array $jsonParameters     null in case of no parameters
     */
    public function jsonParameters()
    {
        return $this->jsonParameters;
    }
}

Note that, at least initially, an abstract class is more flexible as it allows to add methods to all the Command objects in a single place.

Here are some examples of Command classes: the first is the command for accepting an alert box by clicking on Ok.

class PHPUnit_Extensions_Selenium2TestCase_SessionCommand_AcceptAlert
    extends PHPUnit_Extensions_Selenium2TestCase_Command
{
    public function httpMethod()
    {
        return 'POST';
    }
}

There is also the command for modifying the current location, or retrieve it after a redirect or a submit:

class PHPUnit_Extensions_Selenium2TestCase_SessionCommand_Url
    extends PHPUnit_Extensions_Selenium2TestCase_Command
{
    public function __construct($relativeUrl, $commandUrl, $baseUrl)
    {
        if ($relativeUrl !== NULL) {
            $absoluteLocation = $baseUrl->addCommand($relativeUrl)->getValue();
            $jsonParameters = array('url' => $absoluteLocation);
        } else {
            $jsonParameters = NULL;
        }
        parent::__construct($jsonParameters, $commandUrl);
    }

    public function httpMethod()
    {
        if ($this->jsonParameters) {
            return 'POST';
        }
        return 'GET';
    }
}

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.

Command (computing) Object (computer science)

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • A Beginner's Guide to Infrastructure as Code
  • DevOps vs Agile: Which Approach Will Win the Battle for Efficiency?
  • Front-End Troubleshooting Using OpenTelemetry
  • Testing Repository Adapters With Hexagonal Architecture

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: