Contractive Delegation

By Phlip Plumlee
April 20, 2009

    I found this old essay on good design
    while cleaning up my hard drives; enjoy!

Well-factored code often has many small functions. If each adds value, and doesn't just pass the buck, then what do they all do? Typically, they contract their input by making it more specific. Then they delegate these specific data to a delegatee. For example, this function takes a fileName, augments it with the current document's contents, then passes them both into the File system delegatee:

  def save(fileName)
    string = get_chars(0, get_length())
    File.open(fileName, "w").write(string)
  end

Every short method solves one part of the problem, and delegates the remaining part of the problem to other methods. Every method transforms its input before passing it to other methods, and/or transforms the output of other methods before returning to the caller. Refactoring a big long ugly function — such as via Method Object Refactor — can lead to many small delegates.

Refactoring these to remove duplicated delegation can lead to Contractive Delegation. After implementing a complex feature, and after the first round of refactoring, the design might contain many absurd chains of delegation, where A calls B calls C repeatedly. The second refactoring phase isolates and removes the intermediate B methods. These abstractions can force special cases to decouple from each other efficiently. Refactoring to merge behaviors forces structures to flex. Each delegating method accepts its inputs, contracts them by adding its own special ingredient, then passes the inputs to the next delegate.

New features are very easy to add to code following Contractive Delegation, but of course it can be a little difficult to read. No more big 90-line methods with obvious procedures. End a refactoring session with Rename Method Refactors. Name things after their new intentions.

If you see duplication, but can't imagine how to improve its design without obfuscating what it does (or can't imagine any way at all), move all the duplicating lines next to each other. This practice forms little tables, with columns that are easy to document and scan.

Early refactors often stabilize a design, enabling new features of the same kind without refactors. Suppose you add two abilities, and they generate similar statements. Merge duplication into an abstraction just as soon as two abilities require it. The odds that future abilities will reuse that abstraction are now very high, because applications by nature present users with lists of similar options. This simple technique prevents code from becoming increasingly disordered and hard to change. Good designs approach the "Open Closed Principle", which essentially means, "Reuse me the way others have reused me. Most of the time, don't reuse me by typing on me, adding 'if' statements for special cases, etc. Reuse me by adding to one of my lists — metadata, derived concrete classes, clients, plugged-in classes, etc. I am Open for re-use but Closed to edits."

(Refactor your unit tests too, but follow slightly different rules. Test cases will duplicate much trivial behavior on purpose, to self-document. Merge such duplication to improve readability, and to create fixtures that make new tests easier to write.)

A software forum, out on the Interthing, recently mentioned something called the Law of Demeter. I have not memorized its definition yet, so I will take a guess that this (contrived) code does not violate it:

  model.member.to_s.gsub('foo', 'bar')
That code preserves Contractive Delegation because, from right to left, each returned object's type runs from least to most stable. If that model were of type Model, we would frequently change it to add features. However, the member object is just a lowly String (or even a lower-ranking NilObject). Those types are more stable because lots more code re-uses them. The gsub is similarly stable, so leaving it at the end of that statement is Mostly Harmless. At refactor time, we might indeed find a reason to put the whole statement into a method of Model, but improving Contractive Delegation is not one of them

By contrast, does this statement violate the Law of Demeter?

  model.members.first.send_email('Canceled!')
(Excusing the possibility of no first member), that statement appears to take a "left turn". Model and Member might be considered different modules, so the code does not contract from left to right, from specialized to mundane.

Thoughts? Citations? Emmendments?


You might also be interested in:

News Topics

Recommended for You

Got a Question?