- Refactor an existing method using clean code rules
- Explain the five SOLID principles with examples
- Recognise and write the Singleton, Factory, Builder, Strategy and Observer patterns
A program is written once but read dozens of times — by teammates, or by you yourself six months later. Working code is not yet good code: good code is easy to read, change and test. In this lesson you will learn the everyday rules of professional Java developers: clean code principles, SOLID and the five most used design patterns.
Rules of clean code
- Meaningful names: not
c,r,calcbutpassedCount,average,averageOfPassed. A name should say what the thing does. - Small methods: one method, one job. If a method does not fit on the screen, split it.
- No magic numbers: the constants
PASS_MARKandBONUS_FACTORinstead of50and1.1. - Immutability:
finalfields andrecords wherever possible; an object that never changes is also safe between threads. - **
Optionalinstead ofnull**: if there may be no result, returnOptional<Student>so that the caller cannot forget about it. - Do not swallow exceptions: an empty
catch {}hides the bug; either handle it or pass it up.
public double calc(List<Integer> l, int t) {
double r = 0;
int c = 0;
for (int i = 0; i < l.size(); i++) {
if (l.get(i) != null) {
if (l.get(i) >= 50) { r = r + l.get(i); c++; }
}
}
if (t == 1) return c == 0 ? 0 : r / c * 1.1;
return c == 0 ? 0 : r / c;
}private static final int PASS_MARK = 50;
private static final double BONUS_FACTOR = 1.1;
public double averageOfPassed(List<Integer> scores, boolean withBonus) {
double average = scores.stream()
.filter(Objects::nonNull)
.filter(score -> score >= PASS_MARK)
.mapToInt(Integer::intValue)
.average()
.orElse(0);
return withBonus ? average * BONUS_FACTOR : average;
}In the first version, what does t == 1 mean? Where does 1.1 come from? The reader has to execute every line in their head. In the second version the name, the constants and the stream tell everything, and boolean withBonus replaces the mysterious int t. Note that we did not change the logic — this is refactoring, and tests written beforehand (the JUnit lesson!) guarantee that the behaviour is preserved.
The SOLID principles
SOLID is an acronym for five object-oriented design principles popularised by Robert C. Martin. They make code resilient to change: when a new requirement arrives, you change one place, not ten.
| Letter | Principle | In short |
|---|---|---|
| S | Single Responsibility | A class should have only one reason to change: the controller deals with HTTP, the service with rules, the repository with the database. |
| O | Open/Closed | Code is open for extension but closed for modification: a new rule is added as a new class without editing old code. |
| L | Liskov Substitution | A subclass must work wherever its parent class is expected, without breaking expectations. |
| I | Interface Segregation | Several small interfaces instead of one big one: a class should not be forced to implement methods it does not use. |
| D | Dependency Inversion | Depend on abstractions (interfaces), not on concrete classes — in the JUnit lesson ReportService depended on the ScoreRepository interface. |
Design patterns
A design pattern is a proven solution scheme for a problem that comes up again and again. In 1994 the “Gang of Four” (Gamma, Helm, Johnson, Vlissides) collected 23 patterns in a book and split them into three groups: creational (how objects are created), structural (how objects are combined) and behavioural (how objects cooperate). Below are the five you will meet most often in Java.
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
AppConfig.INSTANCE.set("school", "School No. 6, Baku");
System.out.println(AppConfig.INSTANCE.get("school"));
System.out.println(AppConfig.INSTANCE == AppConfig.valueOf("INSTANCE"));
for (String type : List.of("email", "sms")) {
Notifier notifier = Notifier.of(type);
notifier.send("Aysel", "Your exam score is 95");
}
}
}
enum AppConfig { // Singleton: exactly one instance
INSTANCE;
private final Map<String, String> values = new HashMap<>();
void set(String key, String value) { values.put(key, value); }
String get(String key) { return values.get(key); }
}
interface Notifier {
void send(String to, String text);
static Notifier of(String type) { // Factory: hides which class is created
return switch (type) {
case "email" -> (to, text) -> System.out.println("Email to " + to + ": " + text);
case "sms" -> (to, text) -> System.out.println("SMS to " + to + ": " + text);
default -> throw new IllegalArgumentException("Unknown type: " + type);
};
}
}School No. 6, Baku true Email to Aysel: Your exam score is 95 SMS to Aysel: Your exam score is 95
enum) and Factory (a static factory method)Singleton ensures that a class has only one object. In Java the simplest thread-safe way is a single-element enum; Spring beans are singletons by default too. Factory creates objects with a method instead of new and hides which class is created: the caller knows only the Notifier interface. In the JDK, List.of(...), Integer.valueOf(...) and Path.of(...) are factory methods.
public class Main {
public static void main(String[] args) {
Report full = new Report.Builder("Aysel")
.subject("Math")
.score(95)
.comment("Excellent work")
.build();
Report minimal = new Report.Builder("Murad").build();
System.out.println(full);
System.out.println(minimal);
}
}
record Report(String student, String subject, int score, String comment) {
static class Builder { // Builder: readable step-by-step creation
private final String student;
private String subject = "General";
private int score;
private String comment = "";
Builder(String student) { this.student = student; }
Builder subject(String value) { subject = value; return this; }
Builder score(int value) { score = value; return this; }
Builder comment(String value) { comment = value; return this; }
Report build() { return new Report(student, subject, score, comment); }
}
}Report[student=Aysel, subject=Math, score=95, comment=Excellent work] Report[student=Murad, subject=General, score=0, comment=]
When a constructor has four or five parameters, it is hard to tell which value is which in new Report("Aysel", "Math", 95, "Excellent"), and optional parameters need many constructors. Builder assembles the object step by step with named methods, and at the end build() returns a finished, immutable object. Familiar examples: StringBuilder, HttpClient.newBuilder(), Stream.builder().
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
GradingRule strict = raw -> raw; // Strategy 1
GradingRule withBonus = raw -> Math.min(100, raw + 5); // Strategy 2
GradeBoard board = new GradeBoard(withBonus);
board.subscribe((name, score) -> System.out.println("[parent app] " + name + " got " + score));
board.subscribe((name, score) -> System.out.println("[teacher log] " + name + ": " + score));
board.addScore("Leyla", 88);
board.setRule(strict);
board.addScore("Elvin", 88);
}
}
interface GradingRule { int apply(int raw); }
interface ScoreListener { void onScore(String name, int score); }
class GradeBoard {
private GradingRule rule;
private final List<ScoreListener> listeners = new ArrayList<>();
GradeBoard(GradingRule rule) { this.rule = rule; }
void setRule(GradingRule rule) { this.rule = rule; }
void subscribe(ScoreListener listener) { listeners.add(listener); }
void addScore(String name, int raw) {
int score = rule.apply(raw);
listeners.forEach(l -> l.onScore(name, score)); // Observer: notify everyone
}
}[parent app] Leyla got 93 [teacher log] Leyla: 93 [parent app] Elvin got 88 [teacher log] Elvin: 88
Strategy turns an algorithm into an object that can be passed in from outside: GradeBoard does not know what is inside the bonus or the strict rule, and adding a new rule does not require editing it — this is the “O” of SOLID. Comparator is a strategy too. Observer delivers one object's change to every object subscribed to it: the parent app and the teacher log receive the same event, while GradeBoard knows nothing about them. Button listeners in graphical interfaces and Spring's events work the same way.
| Pattern | Problem it solves | In the JDK and Spring |
|---|---|---|
| Singleton | one instance is enough: configuration, a cache | Runtime.getRuntime() |
| Factory | hiding which class gets created | List.of(), Path.of() |
| Builder | building an object with many parameters clearly | StringBuilder, HttpClient.newBuilder() |
| Strategy | swapping an algorithm at run time | Comparator |
| Observer | telling many subscribers about an event | PropertyChangeListener, ApplicationEventPublisher |
Key points
- Clean code: meaningful names, small methods, constants instead of magic numbers, immutability,
Optional, no emptycatch. - Refactoring improves structure without changing behaviour; tests make it safe.
- SOLID: one responsibility, extension without editing, substitutable subclasses, small interfaces, depending on abstractions.
- Creational patterns: Singleton (
enum), Factory (of(...)), Builder (build()). - Behavioural patterns: Strategy — a swappable algorithm (
Comparator), Observer — notifying subscribers.
Check yourself
10 questions. Every correct answer earns XP.