Dataslope logoDataslope

Responsibility-Driven Design

How to decide *which* object should know what and do what — the central skill of OOP modeling

You have learned the mechanics of classes, interfaces, inheritance, and polymorphism. None of that helps if you don't know which class should exist or which class should hold which behavior. That deciding skill is responsibility-driven design, and it's the hardest — and most rewarding — part of OOP.

Responsibility-driven design was named by Rebecca Wirfs-Brock in the late 1980s. The whole idea fits in one sentence:

Each object has a small set of responsibilities, and those responsibilities determine its behavior and collaborators.

You stop asking "what fields does this class have?" and start asking "what does this object know, and what does it do, and who does it ask for help?"

Three questions for every object

A healthy object can answer all three in one or two sentences. If the do list is enormous, the object is doing too much. If the know list is enormous, it probably owns data that belongs to someone else. If the collaborate list is enormous, the object is acting as a central coordinator and should probably be broken up.

CRC cards: a 5-minute design tool

Wirfs-Brock and Ward Cunningham popularized CRC cardsClass–Responsibility–Collaborator cards — as a quick paper-based tool for sketching object designs. Each card is the size of an index card and has three fields:

┌────────────────────────────────────────────┐
│ Class:  Order                              │
├────────────────────────────────────────────┤
│ Responsibilities         │ Collaborators   │
│ ─ track its line items   │  LineItem       │
│ ─ compute its total      │                 │
│ ─ know whether it is paid│                 │
│ ─ mark itself as paid    │  Payment        │
└────────────────────────────────────────────┘

You write one card per candidate class. The card forces brevity. If the responsibility list spills off the card, the class is too big.

A worked modeling example

Let's design — on cards, before code — a simple coffee shop order system. From the description:

A customer chooses one or more drinks from the menu. The system creates an order, takes payment, and prints a receipt.

A first naive impulse is to make one giant CoffeeShop class that does everything. Resist it. Look for nouns in the description and make a card for each candidate.

A reasonable set of cards might look like:

ClassResponsibilitiesCollaborators
DrinkKnow its name and price
MenuKnow which drinks are offered; look one up by nameDrink
LineItemKnow a drink and a quantity; compute its subtotalDrink
OrderHold a list of line items; compute total; know if paidLineItem
PaymentCharge an amount; mark the order as paidOrder
ReceiptFormat an order as printed textOrder

Each class owns its own narrow slice of knowledge and behavior.

Now translate to Java. Notice that no class is "in charge of everything". The flow of messages is the conversation.

Code Block
Java 8 (Update 492)

Notice how no class reaches into another class's fields. Receipt asks Order for its items and total. Payment asks Order whether it's paid. LineItem asks Drink for its price. Every interaction is a message. That is responsibility-driven design.

Smells that point at responsibility problems

Whenever a class starts feeling wrong, one of these code smells is usually the cause. Each has a fix rooted in responsibility-driven thinking.

SmellLikely causeLikely fix
God classOne class is doing several jobsSplit out classes per job
Feature envyBehavior is on the wrong classMove the method to the class whose data it uses
Data class / anemic modelBehavior is in other classesMove methods onto the class that owns the data
Long parameter listA concept is implicitIntroduce a class that bundles the parameters

A quick refactor: feature envy

Look at this Util.tax(invoice) method. It looks like it operates on an Invoice, but every line is poking at Invoice's fields. The behavior envies the data — it wants to live on Invoice.

Code Block
Java 8 (Update 492)

The responsibility-driven fix is to move the method onto Invoice, where the data lives, and stop making Invoice a passive bag of fields.

Code Block
Java 8 (Update 492)

Five lines of code moved, but the design is dramatically better. Invoice now owns both its data and the rules about its data. The caller no longer has to know the tax formula. Tomorrow's "Belgian invoices have a different rate" change happens in Invoice — not in every caller.

The "tell, don't ask" rule of thumb

A classic shorthand:

Tell an object to do something. Don't ask it for data so you can do something with the data yourself.

The "tell" form lets the object enforce its own rules. The "ask" form forces callers to re-implement those rules consistently — which they won't.

Practice

Challenge
Java 8 (Update 492)
Move the behavior to the right class

Refactor a small feature envy smell.

You're given two starter classes:

  • Wallet exposes its cents field as public (bad).
  • Buyer decides whether the wallet can afford something and then mutates the wallet directly.

Move all of that logic onto Wallet:

  • Make cents private final-ish: private int cents; initialized via the constructor.
  • Add public boolean buy(int priceCents) that returns false if there isn't enough money (and does not change the balance), or returns true and subtracts the price.
  • Add public int balance().

Rewrite Buyer.shop(...) to just tell the wallet to buy. The Main is provided.

Expected output (with starting balance 100):

bought apple? true
bought yacht? false
balance: 60

Test your understanding

QuestionSelect one

Which best describes responsibility-driven design?

Each class should have exactly one method

Each class should have a small, coherent set of responsibilities, and those determine its behavior and its collaborators

All classes should inherit from a shared base class

Behaviour should be split across as many classes as possible

QuestionSelect one

A method on class Foo does almost nothing except read fields of a passed-in Bar. This is the smell called:

God class

Long parameter list

Feature envy

Anemic model

QuestionSelect one

What does the "tell, don't ask" guideline mean?

Always print debug messages

Always pass parameters by value

Send the object a command to do the thing, rather than pulling out its data and doing the thing yourself

Don't write public methods at all

On this page