
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.