SOLID Cheatsheet¶
Five principles about where dependencies point and how change is absorbed. They are not rules to apply everywhere; they are names for problems you should recognise, and each one has a cost.
Pairs with Module 15 — SOLID and Design Patterns
The five, in one table¶
| Principle | One line | The smell it names |
|---|---|---|
| S — Single Responsibility | A class has one reason to change | A class name with "And", "Manager", "Util"; every function needs three parameters and this |
| O — Open/Closed | Add behaviour by adding code, not by editing working code | A growing switch on a type tag, edited every time a type is added |
| L — Liskov Substitution | A derived type is usable wherever the base is promised, without surprises | A derived override that throws, does nothing, or changes the contract |
| I — Interface Segregation | Several narrow interfaces beat one wide one | An interface with ten functions where each implementer uses three |
| D — Dependency Inversion | Depend on abstractions, not on concrete details | A domain class that constructs its own file, its own clock, its own logger |
S — Single Responsibility¶
// Two reasons to change: the report format, and how orders are stored
class OrderReport { public: std::string toHtml(); void saveToDisk(); };
// One each
class OrderReport { public: std::string toHtml() const; }; // changes when the layout changes
class OrderStore { public: void save(const OrderReport&); }; // changes when storage changes
The test: describe the class in one sentence without "and". If you cannot, it has two responsibilities and will be edited for two unrelated reasons.
The cost: more classes, more files, and more indirection to follow. It is usually worth it, and occasionally it is not — say which, and why.
O — Open/Closed¶
double area(const Shape& s)
{
switch (s.kind) // every new shape edits this function
{
case Kind::Circle: return pi * s.r * s.r;
case Kind::Rectangle: return s.w * s.h;
}
return 0.0;
}
class Shape { public: virtual ~Shape() = default; virtual double area() const = 0; };
// a new shape is a new file; area() is written once and never edited
The test: to add the next variation, how many files must change? If the answer grows with each addition, the design is closed.
The cost: virtual dispatch, and a hierarchy where a value type would have done. Do not open a design for a second variation that has not arrived — that is speculation, not design.
L — Liskov Substitution¶
class Bird { public: virtual void fly(); };
class Penguin : public Bird { public: void fly() override { throw std::logic_error("no"); } };
A Penguin cannot be used wherever a Bird is promised, so the hierarchy is wrong — not the
penguin. The usual repairs: move fly() into a FlyingBird interface, or model flight as a
capability the object holds.
The test: can every caller of the base be correct for every derived type, with no if (dynamic_cast<...>)
or if (type() == ...)? If callers must ask what the object really is, substitution has failed.
Signals: an override that throws "not supported", one that ignores its arguments, or one that
changes the meaning of a return value (false instead of "no opinion").
I — Interface Segregation¶
class Machine { public: virtual void print(); virtual void scan(); virtual void fax(); };
// a printer-only device must implement scan() and fax() as errors or no-ops
class Printer { public: virtual void print() = 0; };
class Scanner { public: virtual void scan() = 0; };
// the multifunction device implements both interfaces; the simple printer implements one
The test: does any implementation return an error, throw, or do nothing for a function it was forced to declare? Then the interface is too wide.
Cost: more interface types. Cheap, compared with the alternative.
D — Dependency Inversion¶
class Library
{
public:
Library() : policy_(new StandardLendingPolicy) {} // constructs its own collaborator
private:
LendingPolicy* policy_;
};
class Library
{
public:
explicit Library(std::unique_ptr<LendingPolicy> policy) : policy_(std::move(policy)) {}
private:
std::unique_ptr<LendingPolicy> policy_; // supplied, replaceable, testable
};
The test: can I run this class with a fake collaborator in a test, without touching the code under test? If not, the dependency points the wrong way.
Cost: injection plumbing, and a composition root (usually main) that has to know how to build
everything. Compile-time decisions (templates, policies) are the cheaper version when the choice is
fixed at build time — see Design Patterns.
Where SOLID meets this course's projects¶
| Requirement in a project brief | The principle it is testing |
|---|---|
| "Add a new kind without editing the report" | Open/Closed |
| "The policy is injected, and two implementations exist" | Dependency Inversion |
| "Every class's responsibility in one sentence" | Single Responsibility |
| "A derived type must be usable wherever the base is" | Liskov Substitution |
| "Several narrow interfaces rather than one wide one" | Interface Segregation |
If a project asks for one of these, it is asking for the principle and the evidence that it holds — usually a file count or a test with a second implementation.
The counterweight: YAGNI¶
Every principle above can be over-applied. The signs:
- An interface with exactly one implementation, and no plan for a second.
- A factory that creates one type.
- A configuration system with one configuration.
- An abstraction whose name ends in
-ableand which nothing else uses.
Rule of thumb: introduce the abstraction when the second variation appears, not when it can be imagined. In the projects, that means building the simple version first, then extracting the interface at the moment the change is actually requested — and saying in your design note what forcing function made you do it.
Checklist¶
- [ ] Can I name each class's single responsibility in one sentence?
- [ ] For the last feature I added, how many files changed? Is that number growing?
- [ ] Does any override throw "not supported" or do nothing?
- [ ] Does any class construct its own external collaborator?
- [ ] Is every interface implemented by more than one type, or planned to be?
- [ ] Have I introduced an abstraction for a variation that does not exist yet?
Related: Design Patterns Quick Reference · Class Design · Projects · Module 15 — SOLID and Design Patterns