11.7. Demeter Principle¶
The Demeter Principle or Law of Demeter states informally: “Talk to your friends—don’t get intimate with strangers.” Specifically, a method can call other methods in its own class, and methods on the classes of its own instance variables; everything else is taboo. Demeter isn’t originally part of the SOLID guidelines, as Figure 11.4 explains, but we include it here since it is highly applicable to Ruby and SaaS, and we opportunistically hijack the D in SOLID to represent it.
The Demeter Principle is easily illustrated by example. Suppose RottenPotatoes has made deals with movie theaters so that moviegoers can buy movie tickets directly via RottenPotatoes by maintaining a credit balance (for example, by receiving movie theater gift cards).
Figure 11.21 shows an implementation of this behavior that contains a Demeter Principle
violation. A problem arises if we ever change the implementation of Wallet—for example, if we
change credit_balance
to cash_balance
, or add points_balance
to allow moviegoers to accumulate
PotatoPoints by becoming top reviewers. All of a sudden, the MovieTheater
class, which is
“twice removed” from Wallet
, would have to change.
Two design smells can tip us off to possible Demeter violations. One is inappropriate intimacy:
the collect_money
method manipulates the credit_balance
attribute of Wallet
directly, even
though managing that attribute is the Wallet
class’s responsibility. (When the same kind of
inappropriate intimacy occurs repeatedly throughout a class, it’s sometimes called feature envy,
because Moviegoer
“wishes it had access to” the features managed by Wallet
.) Another smell that
arises in tests is the mock trainwreck, which occurs in lines 25–27 of Figure 11.21: to test
code that violates Demeter, we find ourselves setting up a “chain” of mocks that will be used
when we call the method under test.
1# Better: delegate credit_balance so MovieTheater only accesses Moviegoer
2class Moviegoer
3 def credit_balance
4 self.wallet.credit_balance # delegation
5 end
6end
7class MovieTheater
8 def collect_money(moviegoer,amount)
9 if moviegoer.credit_balance >= amount
10 moviegoer.credit_balance -= due_amount
11 @collected_amount += due_amount
12 else
13 raise InsufficientFundsError
14 end
15 end
16end
1class Wallet
2 attr_reader :credit_balance # no longer attr_accessor!
3 def withdraw(amount)
4 raise InsufficientFundsError if amount > @credit_balance
5 @credit_balance -= amount
6 amount
7 end
8end
9class Moviegoer
10 # behavior delegation
11 def pay(amount)
12 wallet.withdraw(amount)
13 end
14end
15class MovieTheater
16 def collect_money(moviegoer, amount)
17 @collected_amount += moviegoer.pay(amount)
18 end
19end
Once again, delegation comes to the rescue. A simple improvement comes from delegating the
credit_balance
attribute, as Figure 11.22 (top) shows. But the best delegation is that in
Figure 11.22 (bottom), since now the behavior of payment is entirely encapsulated within
Wallet
, as is the decision of when to raise an error for failed payments.
Inappropriate intimacy and Demeter violations can arise in any situation where you feel you are “reaching through” an interface to get some task done, thereby exposing yourself to dependency on implementation details of a class that should really be none of your business. Three design patterns address common scenarios that could otherwise lead to Demeter violations. One is the Visitor pattern, in which a data structure is traversed and you provide a callback method to execute for each member of the data structure, allowing you to “visit” each element while remaining ignorant of the way the data structure is organized. Indeed, the “data structure” could even be materialized lazily as you visit the different nodes, rather than existing statically all at once. An example of this pattern in the wild is the Nokogiri gem, which supports traversal of HTML and XML documents organized as a tree: in addition to searching for a specific element in a document, you can have Nokogiri traverse the document and call a visitor method you provide at each document node.
A simple special case of Visitor is the Iterator pattern, which is so pervasive in Ruby
(you use it anytime you use each
) that many Rubyists hardly think of it as a pattern. Iterator
separates the implementation of traversing a collection from the behavior you want to apply
to each collection element. Without iterators, the behavior would have to “reach into” the
collection, thereby knowing inappropriately intimate details of how the collection is organized.
The last design pattern that can help with some cases of Demeter violations is the Observer pattern, which is used when one class (the observer) wants to be kept aware of what another class is doing (the subject) without knowing the details of the subject’s implementation. The Observer design pattern provides a canonical way for the subject to maintain a list of its observers and notify them automatically of any state changes in which they have indicated interest, using a narrow interface to separate the concept of observation from the specifics of what each observer does with the information.
While the Ruby standard library includes a mixin called Observable
, Rails’ ActiveSupport
provides a more concise Observer that lets you observe any model’s ActiveRecord lifecycle hooks
(after_save
and so on), introduced in Section 5.1. Figure 11.23 shows how easy it is to add
an EmailList
class to RottenPotatoes that “subscribes” to two kinds of state changes:
1class EmailList
2 observe Review
3 def after_create(review)
4 moviegoers = review.moviegoers # from has_many :through, remember?
5 self.email(moviegoers, "A new review for #{review.movie} is up.")
6 end
7 observe Moviegoer
8 def after_create(moviegoer)
9 self.email([moviegoer], "Welcome, #{moviegoer.name}!")
10 end
11 def self.email ; ... ; end
12end
When a new review is added, it emails all moviegoers who have already reviewed that same movie.
When a new moviegoer signs up, it sends her a “Welcome” email.
In addition to ActiveRecord lifecycle hooks, Rails caching, which we will encounter in Chapter 12, is another example of the Observer pattern in the wild: the cache for each type of ActiveRecord model observes the model instance in order to know when model instances become stale and should be removed from the cache. The observer doesn’t have to know the implementation details of the observed class—it just gets called at the right time, like Iterator and Visitor.
To close out this section, it’s worth pointing out an example that looks like it violates
Demeter, but really doesn’t. It’s common in Rails views (say, for a Review
) to see code such
<p> Review of: <%= @review.movie.title %> </p>
<p> Written by: <%= @review.moviegoer.name %> </p>
Aren’t these Demeter violations? It’s a judgment call: strictly speaking, a review
shouldn’t
know the implementation details of movie
, but it’s hard to argue that creating delegate methods
Review#movie_title
and Review#moviegoer_name
would enhance readability in this case. The
general opinion in the Rails community is that it’s acceptable for views whose purpose is to
display object relationships to also expose those relationships in the view code, so examples
like this are usually allowed to stand.
Self-Check 11.7.1. Ben Bitdiddle is a purist about Demeter violations, and
he objects to the expression @movie.reviews.average_rating
in the movie details view,
which shows a movie’s average review score. How would you placate Ben and fix this Demeter
violation?
1# naive way: 2class Movie 3 has_many :reviews 4 def average_rating 5 self.reviews.average_rating # delegate to Review#average_rating 6 end 7end 8# Rails shortcut: 9class Movie 10 has_many :reviews 11 delegate :average_rating, :to => :review 12end
Self-Check 11.7.2. Notwithstanding that “delegation is the key mechanism” for resolving Demeter violations, why should you be concerned if you find yourself delegating many methods from class A to class B just to resolve Demeter violations present in class C?
You might ask yourself whether there should be a direct relationship between class C and class B, or whether class A has “feature envy” for class B, indicating that the division of responsibilities between A and B might need to be reengineered.