Open / Closed Principle
This is the second post on SOLID principles series and today we will talk about Open / Closed Principle. This rule was introduced in 1988 by Bertrand Meyer, who is also the author of the Eiffel programming language and the idea of design by contract. It means that any software entity, like classes, modules or functions, should be open for extension and closed for modification. In other words, we should be able to add new features without the necessity of changing the existing code.
Practical example
“Code should be open for extension and closed for modification” sounds a little cryptic. When I was learning about this I wasn’t able to fully grasp it. It’s very simple principle and I believe that showing you an example is the best way of explaining it.
Let’s imagine we are are generating some kinds of reports in our application. For now, we are generating XML and HTML reports, but we got a new requirement that PDF version is needed as well.
The naive approach to implementing this will be having a ReportGenerator class where something like this could be found:
if :xml
generate_xml_report
elsif :html
generate_html_report
else
....
To add a PDF version we would have to add a new elsif block to the existing class. This design does not comply with the OCP (open/closed principle) because we are forced to change existing class in order to add new features.
How can we make this code better? What are the techniques to make this code open for extension and closed for modification?
Inheritance
Historically the solution to this problem was to use inheritance. We would have basic Report class and the XMLReport and HTMLReport classes that inherit from it. If we wanted to add PDF report, we would create another subclass of a Report class called PDFReport. We achieved our goal here - we added a new feature and we didn’t have to change existing code.
This way of solving things in not actually recommended anymore. Inheritance entails some consequences and we should be very conservative using it.
What are the drawbacks of inheritance?
The biggest threat of overusing inheritance is having a lot of layers of abstractions that hampers transparency. Subclasses are tightly coupled with the superclass so the flexibility of the code decreases. Some modern languages, like GO, gave up inheritance completely and they rely on type composition exclusively.
Object composition
More flexible way of making our code OCP-compliant without the drawbacks of inheritance is object composition. There is a design pattern that solves this problem and is called Strategy. In this pattern, we encapsulate particular algorithm within a separate class. Client can choose any of the strategies available.
In our case we will have only one *Report* class and a couple of strategies:
- HTMLFormatter
- XMLFormatter
- PDFFormatter
- etc.
To generate a specific type of report we will have pass strategy we want to use to the Report class (this is called Dependency Injection) and call a generate_report method. The code might look like this:
class HTLMFormatter
def generate
generate_html_report
end
end
class XMLFormatter
def generate
generate_xml_report
end
end
class PDFFormatter
def generate
generate_pdf_report
end
end
class Report
def initialize(strategy)
@strategy = strategy
end
def genetate_report
@strategy.generate
end
end
Report.new(HTMLFormatter.new).generate_report
Summary
The way of generating report is decoupled from the report itself, so now we can add new kinds of reports without changing the existing code. It means that we are honouring the Open / Closed Principle.
Written on June 15, 2017