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
Figure 11.10: The Report class depends on a base class Formatter with subclasses HtmlFormatter and PdfFormatter. Because of the explicit dispatch on the report format, adding a new type of report output requires modifying Report#output, and probably requires changing other methods of Report that have similar logic—so-called shotgun surgery.

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
Figure 11.11: Ruby’s metaprogramming and duck typing enable an elegant implementation of the abstract factory pattern. classify is provided by Rails to convert snake_case to UpperCamelCase. constantize is syntactic sugar provided by Rails that calls the Ruby introspection method Object#const_get on the receiver. We also handle the case of an invalid value of the formatter class, which the bad code doesn’t.

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.

11.12
Figure 11.12: In Template Method (left), the extension points are header, body, and footer, since the Report#output method calls @formatter.header, @formatter.body, and so on, each of which delegates to a specialized counterpart in the appropriate subclass. (Light gray type indicates methods that just delegate to a subclass.) In Strategy (right), the extension point is the output method itself, which delegates the entire task to a subclass. Delegation is such a common ingredient of composition that some people refer to it as the delegation pattern.
11.13
Figure 11.13: (Left) The multiplication of subclasses resulting from trying to solve the Formatter problem using inheritance shows why your class designs should “prefer composition over inheritance.” (Right) A more elegant solution uses the Decorator design pattern.

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))
Figure 11.14: To apply Decorator to a class, we “wrap” class by creating a subclass (to follow the Liskov Substitution Principle, as we’ll learn in Section 11.5). The subclass delegates to the original method or class for functionality that isn’t changed, and implements the extra methods that extend the functionality. We can then easily “build up” just the version of PdfFormatter we need by “stacking” decorators.

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
Figure 11.15: To decorate an existing method Mailer#send_email, we reopen its class and use alias_method_chain to decorate it. Without changing any classes that call send_email, all calls now use the decorated version that sends email and copies the sender.

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.