Modular Programming with PHP 7
上QQ阅读APP看书,第一时间看更新

Open/closed principle

The open/closed principle states that a class should be open for extension but closed for modification, as per the definition found on Wikipedia:

"software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"

The open for extension part means that we should design our classes so that new functionality can be added if needed. The closed for modification part means that this new functionality should fit in without modifying the original class. The class should only be modified in case of a bug fix, not for adding new functionality.

The following is an example of a class that violates the open/closed principle:

class CsvExporter {
    public function export($data) {
        // Implementation...
    }
}

class XmlExporter {
    public function export($data) {
        // Implementation...
    }
}

class GenericExporter {
    public function exportToFormat($data, $format) {
        if ('csv' === $format) {
            $exporter = new CsvExporter();
        } elseif ('xml' === $format) {
            $exporter = new XmlExporter();
        } else {
            throw new \Exception('Unknown export format!');
        }
        return $exporter->export($data);
    }
}

Here we have two concrete classes, CsvExporter and XmlExporter, each with a single responsibility. Then we have a GenericExporter with its exportToFormat method that actually triggers the export function on a proper instance type. The problem here is that we cannot add a new type of exporter to the mix without modifying the GenericExporter class. To put it in other words, GenericExporter is not open for extension and closed for modification.

The following is an example of refactored implementation, which complies with OCP:

interface ExporterFactoryInterface {
    public function buildForFormat($format);
}

interface ExporterInterface {
    public function export($data);
}

class CsvExporter implements ExporterInterface {
    public function export($data) {
        // Implementation...
    }
}

class XmlExporter implements ExporterInterface {
    public function export($data) {
        // Implementation...
    }
}

class ExporterFactory implements ExporterFactoryInterface {
    private $factories = array();

    public function addExporterFactory($format, callable $factory) {
          $this->factories[$format] = $factory;
    }

    public function buildForFormat($format) {
        $factory = $this->factories[$format];
        $exporter = $factory(); // the factory is a callable

        return $exporter;
    }
}

class GenericExporter {
    private $exporterFactory;

    public function __construct(ExporterFactoryInterface $exporterFactory) {
        $this->exporterFactory = $exporterFactory;
    }

    public function exportToFormat($data, $format) {
        $exporter = $this->exporterFactory->buildForFormat($format);
        return $exporter->export($data);
    }
}

// Client
$exporterFactory = new ExporterFactory();

$exporterFactory->addExporterFactory(
'xml',
    function () {
        return new XmlExporter();
    }
);

$exporterFactory->addExporterFactory(
'csv',
    function () {
        return new CsvExporter();
    }
);

$data = array(/* ... some export data ... */);
$genericExporter = new GenericExporter($exporterFactory);
$csvEncodedData = $genericExporter->exportToFormat($data, 'csv');

Here we added two interfaces, ExporterFactoryInterface and ExporterInterface. We then modified the CsvExporter and XmlExporter to implement that interface. The ExporterFactory was added, implementing the ExporterFactoryInterface. Its main role is defined by the buildForFormat method, which returns the exporter as a callback function. Finally, the GenericExporter was rewritten to accept the ExporterFactoryInterface via its constructor, and its exportToFormat method now builds the exporter by use of an exporter factory and calls the execute method on it.

The client itself has taken a more robust role now, by first instantiating the ExporterFactory and adding two exporters to it, which it then passed onto GenericExporter. Adding a new export format to GenericExporter now, no longer requires modifying it, therefore making it open for extension and closed for modification. Again, this is by no means a universal formula, rather a concept of possible approach towards satisfying the OCP.