11.3. Single Responsibility Principle

The Single Responsibility Principle (SRP) of SOLID states that a class should have one and only one responsibility—that is, only one reason to change. For example, in Section 5.2, when we added single sign-on to RottenPotatoes, we created a new SessionsController to handle the sign-on interaction. An alternate strategy would be to augment MoviegoersController, since sign-on is an action associated with moviegoers. Indeed, before the single sign-on approach described in Chapter 5, this was the recommended way to implementing password-based authentication in earlier versions of Rails. But such a scheme would require changing the Moviegoer model and controller whenever we wanted to change the authentication strategy, even though the “essence” of a Moviegoer doesn’t really depend on how they sign in. In MVC, each controller should specialize in dealing with one resource; an authenticated user session is a distinct resource from the user himself, and deserves its own RESTful actions and model methods. As a rule of thumb, if you cannot describe the responsibility of a class in 25 words or less, it may have more than one responsibility, and the new ones should be split out into their own classes.

In statically typed compiled languages, the cost of violating SRP is obvious: any change to a class requires recompilation and may also trigger recompilation or relinking of other classes that depend on it. Because we don’t pay this price in interpreted dynamic languages, it’s easy to let classes get too large and violate SRP. One tip-off is lack of cohesion, which is the degree to which the elements of a single logical entity, in this case a class, are related. Two methods are related if they access the same subset of instance or class variables or if one calls the other. The LCOM metric, for Lack of Cohesion Of Methods, measures cohesion for a class: in particular, it warns you if the class consists of multiple “clusters” in which methods within a cluster are related, but methods in one cluster aren’t strongly related to methods in other clusters. Figure 11.7 shows two of the most commonly used variants of the LCOM metric.

The Data Clumps design smell is one warning sign that a good class is evolving toward the “multiple responsibilities” antipattern. A Data Clump is a group of variables or values that are always passed together as arguments to a method or returned together as a set of results from a method. This “traveling together” is a sign that the values might really need their own class. Another symptom is that something that used to be a “simple” data value acquires new behaviors. For example, suppose a Moviegoer has attributes phone_number and zipcode, and you want to add the ability to check the zip code for accuracy or canonicalize the formatting of the phone number. If you add these methods to Moviegoer, they will reduce its cohesion because they form a “clique” of methods that only deal with specific instance variables. The alternative is to use the Extract Class refactoring to put these methods into a new Address class, as Figure 11.8 shows.

 1class Moviegoer
 2    attr_accessor :name, :street, :phone_number, :zipcode
 3    validates :phone_number, # ...
 4    validates :zipcode, # ...
 5    def format_phone_number ; ... ; end
 6    def verify_zipcode ; ... ; end
 7    def format_address(street, phone_number, zipcode) # data clump
 8        # do formatting, calling format_phone_number and verify_zipcode
 9    end
10end
11# After applying Extract Class:
12class Moviegoer
13    attr_accessor :name
14    has_one :address
15end
16class Address
17    belongs_to :moviegoer
18    attr_accessor :phone_number, :zipcode
19    validates :phone_number, # ...
20    validates :zipcode, # ...
21    def format_address ; ... ; end # no arguments - operates on 'self'
22    private  # no need to expose these now:
23    def format_phone_number ; ... ; end
24    def verify_zipcode ; ... ; end
25end
Figure 11.8: To perform Extract Class, we identify the group of methods that shares a responsibility distinct from that of the rest of the class, move those methods into a new class, make the “traveling together” data items on which they operate into instance variables of the class, and arrange to pass an instance of the class around rather than the individual items.

Self-Check 11.3.1. Draw the UML class diagrams showing class architecture before and after the refactoring in Figure 11.8.

Figure 11.9 shows the UML diagrams.

11.49
Figure 11.9: UML class diagrams before (left) and after (right) extracting the Address class from Moviegoer.