Lesson 31Phase 3: Abstraction and design
Designing classes well
Composition over inheritance, single responsibility, programming to interfaces, a few classic patterns, and drawing classes as UML diagrams.
By BasithUpdated 55 min
- composition
- single responsibility
- open/closed
- dependency injection
- strategy pattern
- factory
You can now write every kind of class Java offers: abstract classes, interfaces, enums, records, sealed families and lambdas. This lesson is about fitting them together. Two programs can print exactly the same thing, yet one takes a minute to extend and the other takes an afternoon of careful edits. The difference usually comes from a few design habits, and you will try each one on a small program you can run. At the end you will learn the standard way to draw a design on paper.
- Build a class out of other objects (composition), and explain when that beats
extends. - Split a class that does several jobs into classes that each have one reason to change.
- Hand a class what it needs through its constructor, typed as an interface, and check it with a fake.
- Add new behavior without editing working code, using the strategy pattern with classes and with lambdas.
- Create objects with a static factory method and a builder, and make a class immutable with final fields and defensive copies.
- Read and draw a UML class diagram: boxes, visibility marks, relationship lines and multiplicities.
extends from inheritance, interfaces from interfaces and interface vs abstract class, final from static and final, static nested classes from inner classes, lambdas from enums and lambdas, and records and sealed types. The programs also keep things in an ArrayList. Hardly any new syntax appears. The new part is deciding which tool fits.Has-a: building objects from other objects#
A real car is built from parts: an engine, four wheels, seats, a steering wheel. Nobody says a car is an engine. A car has an engine, and when you press the accelerator, the car hands the hard work to it.
In Java, “has” means a field. A class that keeps other objects in its fields and builds its own behavior out of theirs is using composition. Press Run and watch the Engine object living inside the Car.
public class Main { public static void main(String[] args) { Car car = new Car("Swift"); car.drive(); car.drive(); System.out.println("Engine started " + car.engineStarts() + " times"); }} class Engine { private int starts = 0; void start() { starts++; System.out.println("Engine: vroom"); } int getStarts() { return starts; }} class Car { private final String model; private final Engine engine = new Engine(); Car(String model) { this.model = model; } void drive() { engine.start(); System.out.println(model + " is moving"); } int engineStarts() { return engine.getStarts(); }}It prints Engine: vroom and Swift is moving twice, then Engine started 2 times. The car never starts anything itself. Its drive() method calls engine.start() and then does its own part. Handing a piece of a job to an object you hold is called delegation. The engine keeps its count in a private field, and the car can only read it through getStarts().
start(). It would also claim that a car is a kind of engine, and it would give everyone who uses a Carall of the engine's methods as if they were the car's. The test from inheritancestill decides: say “a Car is an Engine” out loud. It is false, so extends is the wrong tool.The classic mistake: a stack that is a list
Some cases are harder to spot, and this one has caught experienced programmers, including the people who wrote Java's own library. A stack is a pile where you only touch the top, like plates in a canteen. You push a plate onto the top, and you pop the top plate off, so the last plate in is the first one out.
An ArrayList already keeps items in order and can add and remove at the end, so it is tempting to extend it and add push and pop:
import java.util.ArrayList; public class Main { public static void main(String[] args) { PlateStack plates = new PlateStack(); plates.push("blue plate"); plates.push("red plate"); plates.push("green plate"); plates.add(0, "sneaky plate"); // inherited from ArrayList plates.remove(1); // inherited too System.out.println(plates.pop()); System.out.println(plates); }} class PlateStack extends ArrayList<String> { void push(String plate) { add(plate); } String pop() { return remove(size() - 1); }}Step through it and watch the memory view: the PlateStack object carries a whole ArrayList inside it. It prints green plate and then [sneaky plate, red plate].
The pop worked. The two lines before it are the problem. Because PlateStack extends ArrayList, it inherited every public list method. add(0, ...) slipped a plate in at the bottom, and remove(1) pulled the blue plate out of the middle of the pile. A stack that allows that is no longer a stack. Once a class extends ArrayList, all of its public methods come along, and there is no way to remove one.
With composition, the list becomes a private part, and the class offers only what a stack should do:
import java.util.ArrayList; public class Main { public static void main(String[] args) { PlateStack plates = new PlateStack(); plates.push("blue plate"); plates.push("red plate"); plates.push("green plate"); System.out.println(plates.pop()); System.out.println(plates.peek()); System.out.println(plates.size() + " plates left"); }} class PlateStack { private final ArrayList<String> plates = new ArrayList<>(); void push(String plate) { plates.add(plate); } String pop() { return plates.remove(plates.size() - 1); } String peek() { return plates.get(plates.size() - 1); } int size() { return plates.size(); }}It prints green plate, red plate and 2 plates left. Code outside the class can push, pop, peek and ask the size, and nothing else. plates.add(0, "sneaky plate") would not even compile, because this PlateStack has no add method. The class could later store its plates in something other than an ArrayList, and no caller would notice.
Circle is a Shape.Car has an Engine.extends.Key ideaPrefer has-a. Use
extendsonly when the child really is a kind of the parent and can keep every promise the parent makes.
Playlist keeps songs in order and lets people add a song or play the next one. Which design fits best?One class, one job#
Composition builds a class from parts. The next question is how much each part should do. This report card class works:
public class Main { public static void main(String[] args) { ReportCard card = new ReportCard("Asha", new int[] { 80, 92, 74 }); card.send(); }} class ReportCard { private final String student; private final int[] marks; ReportCard(String student, int[] marks) { this.student = student; this.marks = marks; } // Job 1: work out the result double average() { int total = 0; for (int mark : marks) { total += mark; } return (double) total / marks.length; } // Job 2: lay out the text String format() { return "Report for " + student + ": average " + average(); } // Job 3: deliver it void send() { System.out.println("Emailing parents of " + student); System.out.println(format()); }}It prints Emailing parents of Asha and then Report for Asha: average 82.0. Now picture the next few months. The school decides the average should leave out each student's lowest mark. The office wants the report laid out differently. Parents ask for a text message instead of an email. Each request comes from a different person for a different reason, and every one of them means opening ReportCard. An edit for one job can break the others, because they all live in one class.
The single responsibility principle says a class should have one job. To test a class, ask how many different reasons could make you change it. If the answer is more than one, split it, one class per reason:
public class Main { public static void main(String[] args) { Grades grades = new Grades(new int[] { 80, 92, 74 }); ReportFormatter formatter = new ReportFormatter(); ReportSender sender = new ReportSender(); String text = formatter.format("Asha", grades); sender.send("Asha", text); }} class Grades { private final int[] marks; Grades(int[] marks) { this.marks = marks; } double average() { int total = 0; for (int mark : marks) { total += mark; } return (double) total / marks.length; }} class ReportFormatter { String format(String student, Grades grades) { return "Report for " + student + ": average " + grades.average(); }} class ReportSender { void send(String student, String text) { System.out.println("Emailing parents of " + student); System.out.println(text); }}The output is exactly the same. What changed is where each decision lives. Grades works out the average and knows nothing about text. ReportFormatter decides the layout and knows nothing about email. ReportSender handles delivery. When parents want text messages, you change ReportSender and leave the other two alone, and main wires the three together.
Grades could also have highest() and lowest(), because all of them change for the same reason: the school's marking rules.Ask for what you need#
A shop sends a message when an order ships. The first version of its notification service creates its own email sender:
class NotificationService { private final EmailSender sender = new EmailSender(); void shipped(String customer) { sender.send(customer, "Your order is on its way"); }}That new EmailSender() welds the two classes together. If the shop wants text messages, someone has to edit NotificationService. There is also no way to check the service without sending a real email, so every test run would land in somebody's inbox.
The fix has two parts. Describe what the service needs as an interface, something that can send a message, and let whoever creates the service hand one in through the constructor:
public class Main { public static void main(String[] args) { NotificationService byEmail = new NotificationService(new EmailSender()); byEmail.shipped("asha@example.com"); NotificationService byText = new NotificationService(new TextSender()); byText.shipped("07700 900123"); }} interface MessageSender { void send(String to, String text);} class EmailSender implements MessageSender { public void send(String to, String text) { System.out.println("Email to " + to + ": " + text); }} class TextSender implements MessageSender { public void send(String to, String text) { System.out.println("Text to " + to + ": " + text); }} class NotificationService { private final MessageSender sender; NotificationService(MessageSender sender) { this.sender = sender; } void shipped(String customer) { sender.send(customer, "Your order is on its way"); }}It prints Email to asha@example.com: Your order is on its way and then Text to 07700 900123: Your order is on its way. NotificationService mentions only MessageSender. Whether a message goes by email, by text or by carrier pigeon is decided in main, and the service never finds out.
An object that a class needs to do its job is called a dependency: the sender is a dependency of the service. Giving a class its dependencies from outside, instead of letting it create them, is called dependency injection. Passing them to the constructor, as here, is constructor injection, the most common form. Behind the long name is a plain idea: the dependency is a constructor parameter.
Notice that the field and the parameter have the interface type, MessageSender, and never EmailSender. That habit is called programming to an interface. You can use it anywhere a variable is declared: List<String> names = new ArrayList<>(); promises only some kind of list, so switching to a different kind later changes one word.
Key ideaAsk for what you need through the constructor, typed as an interface. Let the caller decide which real object to pass in.
Checking the service with a fake
Now the service can be checked without sending anything. A fake is a stand-in written for checking: it implements the same interface, and instead of sending a message it writes down what it was asked to send. The check below has the usual shape of an automated test: arrange the objects, act by calling the method, then assert that the result is what you expected.
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { // Arrange: a fake sender, handed to the real service FakeSender fake = new FakeSender(); NotificationService service = new NotificationService(fake); // Act service.shipped("asha@example.com"); // Assert: exactly one message, to the right person String expected = "asha@example.com: Your order is on its way"; if (fake.sent.size() == 1 && fake.sent.get(0).equals(expected)) { System.out.println("PASS"); } else { System.out.println("FAIL: " + fake.sent); } }} interface MessageSender { void send(String to, String text);} class FakeSender implements MessageSender { final List<String> sent = new ArrayList<>(); public void send(String to, String text) { sent.add(to + ": " + text); }} class NotificationService { private final MessageSender sender; NotificationService(MessageSender sender) { this.sender = sender; } void shipped(String customer) { sender.send(customer, "Your order is on its way"); }}It prints PASS, and no email left the building. The service ran its real code, and the fake caught exactly what it tried to send. With JUnit the if would become an assertEquals. Either way, the design is what made the check possible: the first version, the one that built its own EmailSender, cannot be checked like this at all.
Notifier whose sender is a lambda that adds each message to the list), act, then assert. The right order prints PASS.Tap a line, then tap where it should go. Or use the arrows.
main would spend pages creating objects and passing them into constructors. Frameworks such as Spring do that wiring for you: you write constructors that ask for what they need, and the framework creates the objects and passes them in. You will see it in your first web API with Spring Boot.Adding behavior without editing old code#
A shop's checkout gives discounts. The first version picks one with an if/else chain on its name:
public class Main { public static void main(String[] args) { Checkout till = new Checkout(); till.ring(400, "none"); till.ring(400, "student"); till.ring(400, "welcome"); }} class Checkout { void ring(int price, String discountType) { int toPay; if (discountType.equals("student")) { toPay = price - price * 10 / 100; } else if (discountType.equals("welcome")) { toPay = Math.max(0, price - 50); } else { toPay = price; } System.out.println("Price " + price + ", you pay " + toPay); }}It prints Price 400, you pay 400, then 360, then 350. Every new offer means opening ring and adding another branch, for a Black Friday sale or a staff discount. The method keeps growing, each edit puts the branches that already worked at risk, and a typo such as "Student" with a capital S quietly charges full price.
The open/closed principle states the goal: code should be open for extension and closed for modification. In plain words, you should be able to add a discount by adding new code, without editing ring at all.
The usual tool is the strategy pattern. Each way of doing a job (each strategy) goes in its own object behind a shared interface, and the code that needs the job accepts any of them:
public class Main { public static void main(String[] args) { Checkout till = new Checkout(); till.ring(400, new NoDiscount()); till.ring(400, new PercentOff(10)); till.ring(400, new AmountOff(50)); }} interface Discount { int apply(int price);} class NoDiscount implements Discount { public int apply(int price) { return price; }} class PercentOff implements Discount { private final int percent; PercentOff(int percent) { this.percent = percent; } public int apply(int price) { return price - price * percent / 100; }} class AmountOff implements Discount { private final int amount; AmountOff(int amount) { this.amount = amount; } public int apply(int price) { return Math.max(0, price - amount); }} class Checkout { void ring(int price, Discount discount) { System.out.println("Price " + price + ", you pay " + discount.apply(price)); }}The three lines are the same as before. ring now takes a Discount and calls apply, with no idea which discounts exist. A new offer is a new class, and Checkout, PercentOff and AmountOff stay exactly as they are, already tested.
Strategies as lambdas
Discount has exactly one abstract method, so it is a functional interface, and a lambda can be a strategy (enums, lambdas and functional interfaces). A short rule does not need a class of its own:
public class Main { public static void main(String[] args) { Discount half = price -> price / 2; Discount loyalty = price -> price >= 300 ? price - 30 : price; Checkout till = new Checkout(); till.ring(400, half); till.ring(400, loyalty); till.ring(250, loyalty); till.ring(400, new PercentOff(10)); }} interface Discount { int apply(int price);} class PercentOff implements Discount { private final int percent; PercentOff(int percent) { this.percent = percent; } public int apply(int price) { return price - price * percent / 100; }} class Checkout { void ring(int price, Discount discount) { System.out.println("Price " + price + ", you pay " + discount.apply(price)); }}The 400 order costs 200 at half price, 370 with the loyalty rule and 360 with ten percent off. The 250 order is too small for the loyalty rule and stays at 250. The lambdas half and loyalty sit next to a PercentOff object, and ring cannot tell them apart. Use a lambda when the rule is short and needs no fields. Use a class when it needs fields of its own, or a name you will reuse in many places.
Key ideaWhen one job can be done in several ways, give each way its own object behind one interface. New ways arrive as new classes or lambdas, and the code that uses them stays untouched.
switch. Both designs deal with many kinds of thing. A sealed type fits when the list of kinds is fixed and known, because the compiler then checks that every switch covers every kind. A strategy interface fits when new kinds keep arriving, because anyone can write a new implementation without touching your code.BigSpender so it takes 50 off any price of 300 or more and leaves smaller prices alone. Do not change Checkout: the whole point is that it never needs to know about the new discount.Patterns for making objects#
The checkout still needs someone to write new PercentOff(10). In a real shop the customer types a coupon code, and some code has to turn "STUDENT" into the right Discount. If that decision is copied into every screen that accepts codes, adding a code means hunting down every copy.
A static factory method
A static factory method is a static method whose job is to create an object, including the decision about which class to create. The caller gets back the interface type and never sees the choice. An interface may contain static methods (since Java 8), and you call them on the interface name, so the factory can live inside Discount itself:
public class Main { public static void main(String[] args) { String[] codes = { "STUDENT", "WELCOME", "BOGUS" }; for (String code : codes) { Discount d = Discount.forCode(code); System.out.println(code + " -> " + d.getClass().getSimpleName() + " -> " + d.apply(400)); } }} interface Discount { int apply(int price); static Discount forCode(String code) { return switch (code) { case "STUDENT" -> new PercentOff(10); case "WELCOME" -> new AmountOff(50); default -> new NoDiscount(); }; }} class NoDiscount implements Discount { public int apply(int price) { return price; }} class PercentOff implements Discount { private final int percent; PercentOff(int percent) { this.percent = percent; } public int apply(int price) { return price - price * percent / 100; }} class AmountOff implements Discount { private final int amount; AmountOff(int amount) { this.amount = amount; } public int apply(int price) { return Math.max(0, price - amount); }}It prints STUDENT -> PercentOff -> 360, WELCOME -> AmountOff -> 350 and BOGUS -> NoDiscount -> 400. The call getClass().getSimpleName() asks an object for the name of its real class, and it shows that the factory chose a different class for each code. An unknown code gets a NoDiscount, so a mistyped coupon charges full price instead of crashing the checkout. The rule that STUDENT means ten percent off now lives in one place, and a new coupon is one new line in forCode.
You have used static factories from the library already. List.of(...) is a static method on the List interface that hands back a list whose class you never name, and String.valueOf(42) makes a String from a number. Compared with a constructor, a factory method can have a name that says what it does, can return any subclass of its return type, and can hand back an object it made earlier instead of a new one. You will see that last trick in the next lesson, with Integer.valueOf in wrapper classes and autoboxing.
A builder
Some objects have many settings, most of them optional. A pizza has a size, maybe extra cheese, maybe a stuffed crust, and any number of toppings. With constructors alone, calls start to look like this:
Pizza dinner = new Pizza("large", true, false, true, "olives");Which true is the cheese? Swap two of them by mistake and the code still compiles, and someone gets a crust they did not order. The builder pattern fixes this. A helper object collects the settings one named method at a time, and a final build() call creates the real object:
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { Pizza dinner = new Pizza.Builder("large") .extraCheese() .topping("mushrooms") .topping("olives") .build(); Pizza snack = new Pizza.Builder("small").build(); System.out.println(dinner.describe()); System.out.println(snack.describe()); }} class Pizza { private final String size; private final boolean extraCheese; private final List<String> toppings; private Pizza(Builder b) { this.size = b.size; this.extraCheese = b.extraCheese; this.toppings = List.copyOf(b.toppings); } String describe() { String text = size + " pizza"; if (extraCheese) { text += " with extra cheese"; } if (!toppings.isEmpty()) { text += ", topped with " + String.join(" and ", toppings); } return text; } static class Builder { private final String size; private boolean extraCheese = false; private final List<String> toppings = new ArrayList<>(); Builder(String size) { this.size = size; } Builder extraCheese() { extraCheese = true; return this; } Builder topping(String name) { toppings.add(name); return this; } Pizza build() { return new Pizza(this); } }}Read main like a sentence: start a large pizza, add extra cheese, add mushrooms, add olives, build it. It prints large pizza with extra cheese, topped with mushrooms and olives and then small pizza. (String.joinglues the toppings together with “ and ” between them.) The chain works because every builder method ends with return this;, handing back the same builder so the next call can follow with a dot. StringBuilder's append works the same way (strings).
Pizza's constructor is private, so the only way to get a pizza is through Pizza.Builder, a static nested class. That makes build() the natural place to check the settings before the object exists, for example refusing a size that is not on the menu. The pizza also copies the toppings list with List.copyOf; the next section explains why that copy matters.
The JDK uses builders where objects have many options. The HTTP client added in Java 11 builds a web request like this: HttpRequest.newBuilder(URI.create("https://example.com")).timeout(Duration.ofSeconds(10)).GET().build(). You will write that for real in talking to the web.
Objects that never change#
A String never changes once it exists: toUpperCase() hands back a new string and leaves the old one alone (strings). You can write classes that behave the same way. An immutable object gets all of its values in its constructor and keeps them for the rest of its life. The recipe:
- Make every field
privateandfinal, so nothing can reassign it after the constructor. - Write no setters, and no other method that changes a field.
- Mark the class
final, so no subclass can add methods that change things. - If a field holds something that can change, such as a list, copy it on the way in, and never hand out a version that others can change.
The last rule is the one people forget. This playlist follows the first three:
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { List<String> songs = new ArrayList<>(); songs.add("Intro"); songs.add("Ocean"); Playlist trip = new Playlist("Road trip", songs); songs.add("Uninvited"); System.out.println(trip.describe()); }} final class Playlist { private final String name; private final List<String> songs; Playlist(String name, List<String> songs) { this.name = name; this.songs = songs; } String describe() { return name + ": " + songs; }}Playlist has only private final fields and no setters. After building it, main adds a song to its own list. What does the program print?
It prints Road trip: [Intro, Ocean, Uninvited]. finalstopped the field from being pointed at a different list, but the playlist's field and main's songs variable point at one and the same ArrayList (object references), so a change made through one name shows through the other. This is the subtlety that static and final promised: final freezes the reference, not the object it points at.
The fix is a defensive copy, a private copy that nobody outside can reach:
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { List<String> songs = new ArrayList<>(); songs.add("Intro"); songs.add("Ocean"); Playlist trip = new Playlist("Road trip", songs); songs.add("Uninvited"); Playlist longer = trip.withSong("Finale"); System.out.println(trip.describe()); System.out.println(longer.describe()); }} final class Playlist { private final String name; private final List<String> songs; Playlist(String name, List<String> songs) { this.name = name; this.songs = List.copyOf(songs); } List<String> songs() { return songs; } Playlist withSong(String song) { List<String> more = new ArrayList<>(songs); more.add(song); return new Playlist(name, more); } String describe() { return name + ": " + songs; }}It prints Road trip: [Intro, Ocean], and then Road trip: [Intro, Ocean, Finale] for the longer playlist. The constructor stores List.copyOf(songs), so adding Uninvited afterwards changes only the caller's list. List.copyOf (Java 10) makes a copy that cannot be changed, like the lists that List.of makes, which is why songs() can return the field itself: calling add on it would stop the program with an UnsupportedOperationException. To “change” a playlist you call withSong, which builds a new Playlist and leaves the old one as it was, the same way toUpperCase() treats a string.
Why immutable objects are easy to share
Nobody can change an immutable object, so you can hand the same one to every part of a program that wants it. No part needs a private copy, and no part has to worry that another will change it behind its back. It works like a printed timetable pinned to a wall: everyone reads the same sheet at once, and it tells all of them the same thing.
That pays off twice later in the course. A key in a HashMap must not change after it goes in, or the map can no longer find it (HashMap and HashSet). And objects shared between threads need no locks when they cannot change (safe sharing).
A record gives you most of the recipe for free: the class is final, its fields are private and final, and it has no setters. It cannot know that a component is a list you meant to copy, though, so the copy goes in a compact constructor:
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { List<String> songs = new ArrayList<>(List.of("Intro", "Ocean")); Playlist trip = new Playlist("Road trip", songs); songs.add("Uninvited"); System.out.println(trip); System.out.println(trip.songs().size()); }} record Playlist(String name, List<String> songs) { Playlist { songs = List.copyOf(songs); }}It prints Playlist[name=Road trip, songs=[Intro, Ocean]] and 2. The song added afterwards never reached the record.
One builder, two pizzas
Back to the pizza builder, trimmed down to its toppings to keep it short. This time one builder is used twice, and the second order adds a topping before building again.
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { Pizza.Builder order = new Pizza.Builder("small").topping("ham"); Pizza first = order.build(); Pizza second = order.topping("pineapple").build(); System.out.println(first.describe()); System.out.println(second.describe()); }} class Pizza { private final String size; private final List<String> toppings; private Pizza(Builder b) { this.size = b.size; this.toppings = List.copyOf(b.toppings); } String describe() { return size + " pizza, topped with " + String.join(" and ", toppings); } static class Builder { private final String size; private final List<String> toppings = new ArrayList<>(); Builder(String size) { this.size = size; } Builder topping(String name) { toppings.add(name); return this; } Pizza build() { return new Pizza(this); } }}The same order builder makes both pizzas. What do the two lines say?
The first pizza has only ham, and the second has ham and pineapple. The builder keeps its toppings list between builds, so the second order still holds the ham. The first pizza is untouched because its constructor copied the list with List.copyOfat the moment it was built. Without that copy both pizzas would share the builder's list, and both would print ham and pineapple.
Team immutable. The constructor should keep its own copy of the players, and withPlayer should leave the team as it is and return a new, bigger team. When you are done, the program prints the original two players on the first line and three on the second.SOLID, and knowing when to stop#
Programmers often sum up these habits with one word, SOLID, made from the first letters of five principles. You have used most of them on this page, so one plain sentence each is enough:
| Letter | Principle | In one sentence | On this page |
|---|---|---|---|
| S | Single responsibility | A class has one job, so only one kind of change makes you edit it. | Splitting ReportCard into three classes. |
| O | Open/closed | You add behavior by writing new code, without editing code that already works. | New Discount classes and lambdas, with Checkout untouched. |
| L | Liskov substitution | A subclass works anywhere its parent is expected, keeping every promise the parent makes. | Any Circle can stand in for a Shape. |
| I | Interface segregation | Several small interfaces beat one big one, so no class has to write methods it does not need. | MessageSender asks for a single method. |
| D | Dependency inversion | Important classes depend on interfaces, and the concrete details plug in from outside. | NotificationService depends on MessageSender, never on EmailSender. |
Patterns are tools for real problems
Each pattern on this page answers a specific problem: a strategy for a job done in several ways, a factory for a choice of class, a builder for many optional settings, an interface for a part you need to swap or fake. Without the problem, a pattern is only extra code. Here is a greeting written by someone who has just learned all of them:
public class Main { public static void main(String[] args) { GreetingStrategy strategy = GreetingStrategyFactory.create("english"); GreeterService service = new GreeterService(strategy); System.out.println(service.greet("Asha")); }} interface GreetingStrategy { String greet(String name);} class EnglishGreeting implements GreetingStrategy { public String greet(String name) { return "Hello, " + name; }} class GreetingStrategyFactory { static GreetingStrategy create(String language) { return new EnglishGreeting(); }} class GreeterService { private final GreetingStrategy strategy; GreeterService(GreetingStrategy strategy) { this.strategy = strategy; } String greet(String name) { return strategy.greet(name); }}Four types, and a factory that ignores its own parameter, to print what System.out.println("Hello, Asha"); prints in one line. Every reader now has to open four types to discover that nothing interesting happens. These are the usual signs that a design has gone the same way:
- an interface with one implementation, no second one planned, and no fake in any test
- a factory that can only ever build one class
- a builder for a class with two fields
- a strategy for a rule that has never changed and that nobody has asked to change
A good habit is to write the plain version first. When a second kind of discount actually turns up, or a test actually needs a fake, reshape the code toward the pattern then. Programmers call this YAGNI, short for “you aren't gonna need it”.
Key ideaUse a pattern when you can name the problem it solves in your program today.
Drawing classes: UML class diagrams#
When programmers plan a design or explain one, they sketch it. The standard way to sketch classes is UML, the Unified Modeling Language, looked after by a standards group called the Object Management Group. UML has many kinds of diagram. The one you will meet most often, on whiteboards, in design documents and in exams, is the class diagram: one box per class, and lines that show how the classes relate.
One box per class
A class box has up to three compartments, stacked from top to bottom:
| Compartment | What goes in it | In the Circle box below |
|---|---|---|
| Name | The class name. An abstract class has its name in italics, often with {abstract} under it. An interface has «interface» above its name. | Circle |
| Fields | One line per field: a visibility mark, the name, a colon and the type. | - radius : double |
| Methods | One line per method: a visibility mark, the name, the parameters written as name : type, then a colon and the return type. No return type means void. | + area() : double |
UML writes the type after the name, radius : double, the opposite of Java's double radius. If you show a constructor, it looks like a method named after the class, such as + Circle(radius : double). The mark at the start of each line is the visibility:
| Mark | Visibility | In Java |
|---|---|---|
+ | public | public |
- | private | private |
# | protected | protected |
~ | package | no keyword at all (package-private, the default) |
A static member is underlined, and an abstract method is written in italics. Diagrams usually leave out the obvious, such as getters and setters, so that the parts that matter stand out. Here is the shapes family from earlier lessons, with a small Drawable interface on top. Choose each line under the drawing to read it in words.
READ THE LINES
Choose a line, here or in the drawing, to read it in plain words.
Shape's italic name and {abstract} tell you that new Shape(...) is not allowed, and its italic area() is the blank each subclass fills in. Circle and Rectangle list only what they add or replace. They inherit describe() and draw() without repeating them.
- balance : double and + deposit(amount : double). Which Java matches?Lines between boxes
Each kind of relationship has its own line. The triangle and the diamond always sit at the more general or larger end: the parent, the interface or the whole.
| Relationship | Drawn as | In Java | Read it as |
|---|---|---|---|
| Inheritance | Solid line, hollow triangle at the parent | class Circle extends Shape | Circle is a Shape |
| Realization | Dashed line, hollow triangle at the interface | class Shape implements Drawable | Shape does what Drawable promises |
| Association | Solid line; an open arrowhead, when drawn, points at the class that can be reached | A field holding an object the class works with | NotificationService knows its MessageSender |
| Aggregation | Solid line, hollow diamond at the whole | A field holding parts that were made elsewhere and live on their own | A Garage holds Cars |
| Composition | Solid line, filled diamond at the whole | The whole creates its parts and never shares them | A Car is made with an Engine |
| Dependency | Dashed line, open arrowhead | Uses the other class only briefly, as a parameter or a local variable | Checkout uses a Discount in ring |
Has-a in two strengths, and how many
UML splits has-a into two strengths, and the word composition comes back with a stricter meaning. The garage below holds cars, and each car is built from an engine and wheels.
READ THE LINES
Choose a line, here or in the drawing, to read it in plain words.
In composition (a filled diamond), the whole owns its parts. A part belongs to one whole at a time, and when the whole goes, its parts go with it. In Java that usually means the whole creates the part itself, like the Car at the start of this lesson with its new Engine(), and never hands it out. When nothing refers to the car any more, nothing refers to its engine either. In aggregation (a hollow diamond), the parts have lives of their own: cars exist before they park, and they drive away when the garage closes. In Java that looks like a list of objects that were created elsewhere and passed in.
The numbers near the ends are multiplicities. Each one says how many objects at that end go with one object at the other end, so you start reading from the box at the other end: one Car has 1 Engine, and one Garage holds 0..* cars.
| Multiplicity | Means | Often, in Java |
|---|---|---|
1 | exactly one | a field that is never null |
0..1 | zero or one | a field that may be null |
* or 0..* | any number, including none | a list |
1..* | at least one | a list that is never empty |
4 | exactly four | an array of four, or four fields |
Order and OrderLine. It has a filled diamond at the Order end and 1..* at the OrderLine end. What does it say?Your own design, drawn
Here is the notification service from earlier in this lesson.
READ THE LINES
Choose a line, here or in the drawing, to read it in plain words.
The service's sender field is drawn as the line to MessageSender, labelled with its name, - sender, and its multiplicity, 1. A field whose type is another class in the diagram is usually drawn as a line like this instead of being written in the box, which is why the service's fields compartment is empty.
EmailSender shows only its name. A diagram may hide compartments that add nothing, and the dashed line already says it has a send method. FakeSender's field starts with ~ because in the code it has no access keyword, so it is package-private.
Taken as a whole, the picture shows the design at a glance: the service points only at the interface, never at a concrete sender. That arrangement is dependency inversion, the D in SOLID.
Drawing one yourself
- 1Find the classes: the main nouns in the problem. For a library they might be
Library,Book,MemberandLoan. - 2Give each box the fields and methods that matter for what you want to show. Leave out getters, setters and anything obvious.
- 3Draw the is-a lines: solid lines to parent classes, dashed lines to interfaces, each with a hollow triangle.
- 4Draw the has-a lines: a filled diamond when the whole creates and owns the part, a plain line or a hollow diamond when the part lives on its own.
- 5Add a multiplicity to every has-a line, and check each one by reading it aloud, starting from the box at the other end.
Try it
Answer each one in your head or on paper first, then open the answer.
Library keeps a collection of books. Should Library extend ArrayList<Book>, or hold a list in a field? Why?Payroll class works out salaries, prints payslips, and saves them to a database. How many reasons to change does it have, and how would you split it?NotificationService be checked without sending a real email, while the first one cannot?+ withSong(song : String) : Playlist into a Java method header. What should the method give back in an immutable Playlist?Playlist, why is it safe for songs() to return the field itself, without copying it again?Practice
Work through these on your own. Each one builds on this page.
Library, Book, Member and Loan. Decide which lines are composition, aggregation or a plain association, and put a multiplicity on every has-a line. Then write the class skeletons in Java with the fields your diagram shows.Grades in the single responsibility section stores the caller's array as it is. Write a main that changes the array after building a Grades and shows the average changing. Then fix Grades with a defensive copy and show that the average stays put.Email class with a builder. to is required and goes in the builder's constructor; subject, any number of cc addresses and an urgent flag are optional. Give Email a describe() method and build three different emails in main.ShippingCost interface with int cost(int orderTotal) and three strategies: standard (always 50), express (always 120), and free over 1000 (0 when the total is over 1000, otherwise 50). Add a static factory ShippingCost.forName(String name). Write a checkout that prints the shipping for an order and never mentions the three classes by name.Further reading#
Spotted a mistake, or a part that did not make sense? Report it by email