Foundation - Open Closed Principle
The acronym SOLID is quite popular in the OOP realm, but I tend to notice that the “O” which stands for Open Closed Principle tends to get overlooked.
What is this principle?
The Open-Closed Principle (OCP) states that software entities (classes, modules, methods, etc.) should be open for extension, but closed for modification.
Enough about theory let’s see an example.
class ReportService
att_reader :user
def initialize(user)
@user = user
end
def generate
if user.with_premium_subscription?
send_report(extensive_report)
else
send_report(basic_report)
end
end
private
def basic_report
# generate a limited report version for the last 30 days
end
def extensive_report
# generate an extensive report version for the last 90 days
end
def send_report(data)
# send content to `user.email_address`
end
end
I’m generating and sending a report based on the user subscription model in this service class. The new requirement introduced a new type of membership, let’s name it “VIP”. The acceptance criteria are:
For users with a “VIP” membership, add a suggestion section to the report and send an SMS notification with the email.
The typical way of handling this new change would be to go and edit the generate
method by adding a new if
condition. That’s how the OCP principle isn’t respected as it should be closed for modification but open to extensions. To achieve that we can use the strategy pattern.
class UserReport
attr_reader :user
def initialize(user)
@user = user
end
def send_report
raise NotImplementedError
end
def prepare_data
raise NotImplementedError
end
private
def send_email_to(email_address, content)
# format content for the email template
# send the email to the specified address
end
end
class BasicReport < UserReport
def send_report
send_email_to(user.email_address, prepare_data)
end
def prepare_data
# generate a limited report version for the last 30 days
end
end
class PremiumReport < UserReport
def send_report
send_email_to(user.email_address, prepare_data)
end
def prepare_data
# generate an extensive report version for the last 90 days
end
end
class VIPReport < UserReport
def send_report
send_email_to(user.email_address, prepare_data)
send_sms_notification_to(user.mobile_number)
end
def prepare_data
# generate an extensive report version for the last 90 days with a suggestion section
end
private
def send_sms_notification_to(mobile_nbr)
# send a notification sms with a customizable text content
end
end
class ReportService
attr_reader :user_report
def initialize(user_report)
@user_report = user_report
end
def generate
user_report.send_report
end
end
## Usage example
report_service = ReportService.new(VIPReport.new(vip_user))
report_service.generate
This is a simplified example but in real-life cases, you will notice a lot of if
or case
conditions based on a specific field or type. These conditions tend to affect most methods until they start spiraling out of control.
That’s it, so have fun and keep coding 🙂