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 cards — Class–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:
| Class | Responsibilities | Collaborators |
|---|---|---|
Drink | Know its name and price | — |
Menu | Know which drinks are offered; look one up by name | Drink |
LineItem | Know a drink and a quantity; compute its subtotal | Drink |
Order | Hold a list of line items; compute total; know if paid | LineItem |
Payment | Charge an amount; mark the order as paid | Order |
Receipt | Format an order as printed text | Order |
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.
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.
| Smell | Likely cause | Likely fix |
|---|---|---|
| God class | One class is doing several jobs | Split out classes per job |
| Feature envy | Behavior is on the wrong class | Move the method to the class whose data it uses |
| Data class / anemic model | Behavior is in other classes | Move methods onto the class that owns the data |
| Long parameter list | A concept is implicit | Introduce 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.
The responsibility-driven fix is to move the method onto Invoice,
where the data lives, and stop making Invoice a passive bag of
fields.
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
Refactor a small feature envy smell.
You're given two starter classes:
Walletexposes itscentsfield aspublic(bad).Buyerdecides whether the wallet can afford something and then mutates the wallet directly.
Move all of that logic onto Wallet:
- Make
centsprivate final-ish:private int cents;initialized via the constructor. - Add
public boolean buy(int priceCents)that returnsfalseif there isn't enough money (and does not change the balance), or returnstrueand 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
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
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
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