Class Diagrams
Object-Oriented Design with UML
Course 1 · Chapter 5 · Class Diagrams in Depth
Chapter 4's domain model used the class diagram's simplest features: boxes, attributes, plain lines. A design class diagram uses the rest: visibility, operations, interfaces, and six kinds of arrow that each make a different promise about code. This chapter teaches the notation and, for each relationship, shows the Python it becomes and runs it, so that you can tell what a diagram is claiming and check that code keeps the claim.
Anatomy of a Class Box
A class is a box with up to three compartments: name, attributes, operations. In a domain model you show only the first two; in a design you show what the reader needs, and no more. This diagram uses every piece of box notation this chapter covers:
Mermaid. Top to bottom: a class with all four visibility marks and a static operation; an interface; an abstract class with an abstract operation; an enumeration.
| Notation | Meaning | In Python |
|---|---|---|
+ public | Anyone may use it | An ordinary name |
- private | Only this class | __name (name-mangled), or _name by convention |
# protected | This class and its subclasses | _name, by convention only |
~ package | Same package or module | No direct equivalent: a leading underscore on a module-level name, or a convention |
Underlined ($ in Mermaid) | Static: belongs to the class, not an instance | @staticmethod, @classmethod, a class attribute |
Italic name or operation (*) | Abstract: cannot be instantiated; must be implemented by a subclass | ABC with @abstractmethod |
«interface» | A contract: operations without an implementation | A typing.Protocol (structural) or an ABC (nominal) |
«enumeration» | A fixed set of named values | enum.Enum |
Python enforces much less than the notation promises. The run below (the first sections of the output) shows what the language actually does with each mark:
The visibility section is the one to read twice. -pin is "private" in the diagram and in the code (__pin), yet it is reachable as _Member__pin: Python's privacy is politeness. When you draw - you are stating the design intent; in Java or C# the compiler would enforce it, in Python a test or a reviewer must. The abstract class and the enumeration, by contrast, are enforced: Policy() and CopyStatus("lost") both fail.
The Six Relationships
Every line between classes is one of these. They run, roughly, from the weakest dependency to the strongest coupling. Here they are in the library, rendered:
Mermaid. Each label names the relationship drawn. Check the shapes: an open arrow, a filled diamond at the whole, a hollow diamond at the whole, a hollow triangle on a solid line, a hollow triangle on a dotted line, and a dotted line with an open arrow.
| Relationship | Drawn as | Test question | In code |
|---|---|---|---|
| Dependency | Dotted line, open arrow, to the thing used | Does it use the other only briefly: a parameter, a local variable, a call? | No field. LoanDesk.close(..., calc) takes the calculator as an argument and keeps nothing |
| Association | Solid line, usually an open arrow showing who can navigate to whom | Does it know about the other over time? | A field: Member.loans |
| Aggregation | Solid line, hollow diamond at the whole | "Has a", and the parts live on without the whole? | A field holding a collection; the parts have a life elsewhere (a Shelf of Copys) |
| Composition | Solid line, filled diamond at the whole | If the whole dies, do the parts die with it, and can a part belong to only one whole? | The whole creates and owns the parts (Title.add_copy); the part cannot be built without it |
| Generalisation | Solid line, hollow triangle at the parent | Can the child be used anywhere the parent is expected, and behave as promised? | Subclassing: StandardPolicy(Policy) |
| Realisation | Dotted line, hollow triangle at the interface | Does the class fulfil the contract of an interface? | Implementing a Protocol/ABC: DailyRateFine for FineCalculator |
The same run's output for each relationship (read the lines under each heading in the output above):
- Dependency.
fields stored on LoanDesk: {}. It computed a fine withDailyRateFine(30p for 3 days) and withNoFine(0) and remembered neither. Dependency is the cheapest coupling: change the calculator and nothing that merely passes it in is affected. - Association with navigability. A member reaches its loans;
hasattr(loan, "member")isFalse. See the next section. - Composition.
Copy("B-9")alone fails for want of its title, and withdrawing the title withdraws its copies. Honesty about the demonstration: Python garbage-collects, so "lifetime" here is bookkeeping I wrote (marking copies withdrawn), not a language guarantee. - Aggregation.
del shelfand the copies are still there. That is true of any plain reference, which is why the next point matters. - Generalisation and realisation. Abstract
Policycannot be created;StandardPolicy(limit 5) andJuniorPolicy(limit 3) can.DailyRateFinesatisfiesFineCalculatorwith no inheritance at all:isinstanceis true, and its inheritance chain is justDailyRateFineandobject. That is realisation in Python: a Protocol matches by shape.
Aggregation: Usually Not Worth Drawing
Aggregation is the relationship people argue about, and the argument is the lesson. As I remember it, the UML specification says aggregation (the hollow diamond) carries no precise meaning beyond "association", and Fowler's UML Distilled advises not using it. The code agrees: a Shelf holding Copy references is indistinguishable from any association with a list. A practical rule: draw an association unless the filled diamond (composition) is genuinely true. If you feel you must use the hollow diamond, say in a note what you mean by it.
Composition is meaningful, because it makes two promises: exclusive ownership (a part belongs to one whole) and shared lifetime. Use it when both are true. The library's "a Copy exists only as a copy of a Title" qualifies, if the library's policy is that withdrawing a title withdraws its copies; if copies could be re-catalogued under a different title, it would not.
Multiplicity and Navigability
Multiplicity works as in chapter 4. Navigability is the arrowhead: it says which class holds the reference. In the code above Member has loans, but Loan has no member: the diagram's arrow from Member to Loan is exactly that, one way. A line with no arrowheads says "undecided" (chapter 4's model) or "both ways"; be sure which you mean. Navigability has real costs:
- One way is simpler. From a loan you cannot reach its member without searching every member. If you need that, add a reference, not a hope.
- Two ways must be kept in step. If
Member.loansandLoan.memberboth exist, every place that creates or removes a loan must update both, and a mismatch is a bug that tests rarely catch. Decide who owns the link, and have only that side change it.
Association Classes
Sometimes a fact belongs to the relationship, not to either end: a reservation's date belongs to the pairing of a member and a title. The notation is a class attached by a dashed line to the middle of the association. Mermaid cannot draw it (I tried: Lexical error on line 6. Unrecognized text.), so this one is PlantUML:
PlantUML. Reservation holds the facts about one (Member, Title) link.
The last two lines of the notation demo show the catch. An association class means one instance per pair: a member cannot have two reservations for the same title, and the second attempt fails. If your domain allows it (reserve again after a lapsed hold), the thing is an ordinary class with its own identity and two associations, not an association class. Choose the notation by the rule you mean.
Inheritance for Reuse: The Classic Mistake
Generalisation is the most overused arrow. The question is not "does a librarian have a name and a list of loans like a member?" (reuse) but "can a librarian be used anywhere a member is expected and still behave as the member's code assumes?" (substitutability). Clean Code, SOLID & Refactoring 6 and 7 cover the Liskov Substitution Principle properly; here is the failure it prevents, run:
can_borrow was written for members and trusts that loan_limit() returns a number. A librarian-as-member returns None ("no limit") and the check crashes: the subclass broke the parent's promise. The alternative in the last lines, a librarian who has an ordinary membership, tells no lie, so no code is surprised. On a diagram this is the difference between a hollow triangle and an association.
Design Patterns chapters on Strategy and State show the usual way to vary behaviour without subclassing the owner.
Reading a Class Diagram Quickly
- Find the centre. The class with the most lines is usually the heart (here,
LoanorRepairJob). - Read the nouns first, then the diamonds and triangles: ownership and "is a" are the strong claims.
- Follow one arrow in words: "A Member holds zero or more Loans; a Loan cannot find its Member."
- Check each multiplicity against reality: exactly-one and zero-or-more are where wrong assumptions hide.
- Look for what is missing: the thing in the use cases that has no box; the arrow you would need to answer a question the system must answer.
Usual Mistakes
| Mistake | Why it hurts | Fix |
|---|---|---|
| Inheritance to share code | Subclass breaks the parent's promises; fragile hierarchies | Composition and delegation; inheritance only for true substitutability |
| Hollow diamond everywhere | It adds no meaning; readers guess | Association; composition only when ownership and lifetime are real |
| Diamond on the wrong end | Says the part owns the whole | The diamond sits at the whole |
| Arrows both ways by default | Two references to keep in step | One direction unless both are needed |
| Every attribute and operation shown | The diagram stops being readable and goes out of date at once | Show what the reader needs; omit getters and setters |
| Interface and implementation arrows confused | Solid triangle line means inheritance; dotted means implementation | Check solid versus dotted |
| Drawing the database | Join tables and keys are not classes | Keep the domain model; map later |
- A shell command that wasted a step. I wrote the demonstration files with a shell here-document whose text contained
<<enumeration>>and similar, and the shell tried to parse them as redirections, failing with a syntax error before anything ran. How to avoid it: write source files with a file-writing tool, not a here-document, whenever the text contains angle brackets or quotes of its own. - My first notation diagram was tiny. Mermaid draws class boxes in a small font, so four stacked boxes occupied a corner of the picture. Raising the font size with an
initdirective made it legible. Takeaway: look at a diagram at the size the reader will see it. - A demonstration that proves less than it appears to. Python's garbage collection means
del shelfleaves copies alive for any reason at all, and "withdrawing a title" is bookkeeping I wrote. The page now says so beside the output. Takeaway: when a diagram promise (lifetime) is not something the language enforces, say what the demo does and does not show. - A claim tested only by trying it. I believed Mermaid could not draw association classes and verified it by running a diagram (it fails). I still cannot say it never will: it is a limit of Mermaid 12.1.0 as run here.
Hands-On Exercises
Read the bike shop diagram in the solution file (do not render it first) and answer: (a) when a RepairJob is deleted, what happens to its JobParts and to the Parts? (b) can you get from a Mechanic to their jobs? (c) can a Bike exist without a Customer? (d) which line is a dependency, and what does that say about RepairJob's fields? (e) can you create a Pricing object?
📄 View solutionHere is some Python (in the solution file). Draw its class diagram in Mermaid, choosing for each link whether it is an association, composition, realisation or dependency, and say how you decided. Check by running the code.
📄 View solutionA mechanic can be certified for several bike types and a bike type has several certified mechanics; each certification has a date. Draw it with an association class in PlantUML, implement it in Python with tests (many-to-many, a fact on the link, one certification per pair), and say when an association class is the wrong choice.
📄 View solutionChapter 5 Quick Reference
- Class box: name / attributes / operations;
+ - # ~visibility (intent only in Python); underlined = static; italic = abstract;«interface»,«enumeration» - Dependency (dotted, open arrow): used briefly, no field. Association (solid, open arrow): a field. Composition (filled diamond at the whole): owns, shared lifetime. Aggregation (hollow diamond): rarely worth it. Generalisation (solid, hollow triangle): substitutable. Realisation (dotted, hollow triangle): fulfils an interface
- Navigability = the arrowhead = who holds the reference; two-way links must be kept in step
- Association class: a fact about a pair; implies one instance per pair; Mermaid cannot draw it, PlantUML can
- Inheritance is for substitutability, not reuse; prefer composition when the child would break the parent's promises
- Read a diagram: find the centre, read nouns then diamonds and triangles, follow one arrow in words, check multiplicities, look for what is missing