11.4. Open/Closed Principle¶
The Open/Closed Principle (OCP) of SOLID states that classes should be “open for extension, but closed against modification.” That is, it should be possible to extend the behavior of classes without modifying existing code on which other classes or apps depend.
While adding subclasses that inherit from a base class is one way to extend existing
classes, it’s often not enough by itself. Figure 11.10 shows why the presence of case
-based
dispatching logic—one variant of the Case Statement design smell—suggests a possible OCP violation.
1class Report
2 def output
3 formatter =
4 case @format
5 when :html
6 HtmlFormatter.new(self)
7 when :pdf
8 PdfFormatter.new(self)
9 # ... etc
10 end
11 end
12end
Depending on the specific case, various design patterns can help. One problem that the smelly
code in Figure 11.10 is trying to solve is that the desired subclass of Formatter
isn’t known
until runtime, when it is stored in the @format
instance variable. The abstract factory pattern
provides a common interface for instantiating an object whose subclass may not be known until
runtime. Ruby’s duck typing and metaprogramming enable a particularly elegant implementation
of this pattern, as Figure 11.11 shows. (In statically-typed languages, to “work around” the
type system, we have to create a factory method for each subclass and have them all implement
a common interface—hence the name of the pattern.)
1class Report
2 def output
3 formatter_class =
4 begin
5 @format.to_s.classify.constantize
6 rescue NameError
7 # ...handle 'invalid formatter type'
8 end
9 formatter = formatter_class.send(:new, self)
10 # etc
11 end
12end
Another approach is to take advantage of the Strategy pattern or Template Method pattern. Both support the case in which there is a general approach to doing a task but many possible variants. The difference between the two is the level at which commonality is captured. With Template Method, although the implementation of each step may differ, the set of steps is the same for all variants; hence it is usually implemented using inheritance. With Strategy, the overall task is the same, but the set of steps may be different in each variant; hence it is usually implemented using composition. Figure 11.12 shows how either pattern could be applied to the report formatter. If every kind of formatter followed the same high- level steps—for example, generate the header, generate the report body, and then generate the footer—we could use Template Method. On the other hand, if the steps themselves were quite different, it would make more sense to use Strategy.
An example of the Strategy pattern in the wild is OmniAuth (Section 5.2): many apps need third-party authentication, and the steps are quite different depending on the auth provider, but the API to all of them is the same. Indeed, OmniAuth even refers to its plug-ins as “strategies.”
A different kind of OCP violation arises when we want to add behaviors to an existing class
and discover that we cannot do so without modifying it. For example, PDF files can be
generated with or without password protection and with or without a “Draft” watermark across
the background. Both features amount to “tacking on” some extra behavior to what PdfFormatter
already does. If you’ve done a lot of object-oriented programming, your first thought might
therefore be to solve the problem using inheritance, as the UML diagram in Figure 11.13 (left)
shows, but there are four permutations of features so you’d end up with four subclasses with
duplication across them—hardly DRY. Fortunately, the decorator pattern can help: we “decorate”
a class or method by wrapping it in an enhanced version that has the same API, allowing us to
compose multiple decorations as needed. Figure 11.14 shows the code corresponding to the more
elegant decorator-based design of the PDF format-er shown in Figure 11.13 (right).
1class PdfFormatter
2 def initialize ; ... ; end
3 def output ; ... ; end
4end
5class PdfWithPasswordFormatter < PdfFormatter
6 def initialize(base) ; @base = base ; end
7 def protect_with_password(original_output) ; ... ; end
8 def output ; protect_with_password @base.output ; end
9 end
10class PdfWithWatermarkFormatter < PdfFormatter
11 def initialize(base) ; @base = base ; end
12 def add_watermark(original_output) ; ... ; end
13 def output ; add_watermark @base.output ; end
14end
15# If we just want a plain PDF
16formatter = PdfFormatter.new
17# If we want a "draft" watermark
18formatter = PdfWithWatermarkFormatter.new(PdfFormatter.new)
19# Both password protection and watermark
20formatter = PdfWithWatermarkFormatter.new(
21PdfWithPasswordFormatter.new(PdfFormatter.new))
In the wild, the ActiveSupport module of Rails provides method-level decoration via
alias_method_chain
, which is very useful in conjunction with Ruby’s open classes, as Figure
11.15 shows. A more interesting example of Decorator in the wild is the Rack application server
we’ve been using since Chapter 3. The heart of Rack is a “middleware” module that receives an
HTTP request and returns a three-element array consisting of an HTTP response code, HTTP headers,
and a response body. A Rack-based application spec- ifies a “stack” of middleware components
that all requests traverse: to add a behavior to an HTTP request (for example, to intercept
certain requests as OmniAuth does to initiate an authentication flow), we decorate the basic
HTTP request behavior. Additional decorators add support for SSL (Secure Sockets Layer),
measuring app performance, and some types of HTTP caching.
1# reopen Mailer class and decorate its send_email method.
2class Mailer
3 alias_method_chain :send_email, :cc
4 def send_email_with_cc(recipient,body) # this is our new method
5 send_email_without_cc(recipient,body) # will call original method
6 copy_sender(body)
7 end
8end
9# now we have two methods:
10send_email(...) # calls send_email_with_cc
11send_email_with_cc(...) # same thing
12send_email_without_cc(...) # call (renamed) original method
Self-Check 11.4.1. Here are two statements about delegation:
1. A subclass delegates a behavior to an ancestor class
2. A class delegates a behavior to a descendant class
Looking at the examples of the Template Method, Strategy, and Decorator patterns (Figures 11.12 and 11.13), which statement best describes how each pattern uses delegation?
In Template Method and Strategy, the ancestor class provides the “basic game plan” which is customized by delegating specific behaviors to different subclasses. In Decorator, each subclass provides special functionality of its own, but delegates back to the ancestor class for the “basic” functionality.