- Рефакторить существующий метод по правилам чистого кода
- Объяснять пять принципов SOLID на примерах
- Узнавать и писать паттерны Singleton, Factory, Builder, Strategy и Observer
Программу пишут один раз, а читают десятки раз — коллеги или ты сам через полгода. Работающий код — ещё не хороший код: хороший код легко читать, менять и тестировать. В этом уроке ты узнаешь повседневные правила профессиональных Java-разработчиков: принципы чистого кода, SOLID и пять самых употребительных паттернов проектирования.
Правила чистого кода
- Говорящие имена: не
c,r,calc, аpassedCount,average,averageOfPassed. Имя должно говорить, что делает элемент. - Маленькие методы: один метод — одна задача. Если метод не помещается на экран, раздели его.
- Никаких магических чисел: константы
PASS_MARKиBONUS_FACTORвместо50и1.1. - Неизменяемость: где возможно — поля
finalи записиrecord; объект, который не меняется, безопасен и между потоками. - **
Optionalвместоnull**: если результата может не быть, возвращайOptional<Student>, чтобы вызывающий код об этом не забыл. - Не глотай исключения: пустой
catch {}прячет ошибку; либо обработай её, либо передай выше.
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;
}Что в первой версии означает t == 1? Откуда взялось 1.1? Читателю приходится выполнять каждую строку в голове. Во второй версии всё говорят имя, константы и поток, а boolean withBonus заменил загадочный int t. Заметь: логику мы не меняли — это рефакторинг, а тесты, написанные заранее (урок о JUnit!), гарантируют, что поведение сохранилось.
Принципы SOLID
SOLID — аббревиатура пяти принципов объектно-ориентированного проектирования, которые популяризировал Роберт Мартин. Они делают код устойчивым к изменениям: когда приходит новое требование, меняешь одно место, а не десять.
| Буква | Принцип | Коротко |
|---|---|---|
| S | Single Responsibility | У класса должна быть только одна причина для изменения: контроллер занимается HTTP, сервис — правилами, репозиторий — базой. |
| O | Open/Closed | Код открыт для расширения, но закрыт для изменения: новое правило добавляется новым классом без правки старого кода. |
| L | Liskov Substitution | Подкласс должен работать везде, где ожидается родительский класс, не нарушая ожиданий. |
| I | Interface Segregation | Несколько маленьких интерфейсов вместо одного большого: класс не должен реализовывать методы, которые ему не нужны. |
| D | Dependency Inversion | Зависи от абстракций (интерфейсов), а не от конкретных классов — в уроке о JUnit ReportService зависел от интерфейса ScoreRepository. |
Паттерны проектирования
Паттерн проектирования (design pattern) — проверенная схема решения часто встречающейся задачи. В 1994 году «Банда четырёх» (Gang of Four: Гамма, Хелм, Джонсон, Влиссидес) собрала в книге 23 паттерна и разделила их на три группы: порождающие (как создаются объекты), структурные (как объекты объединяются) и поведенческие (как объекты взаимодействуют). Ниже — пять паттернов, которые чаще всего встречаются в 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) и Factory (статический фабричный метод)Singleton (одиночка) гарантирует, что у класса только один объект. В Java самый простой и потокобезопасный способ — enum с одним элементом; бины Spring по умолчанию тоже одиночки. Factory (фабрика) создаёт объект методом, а не через new, и скрывает, какой класс создаётся: вызывающий код знает только интерфейс Notifier. В JDK фабричные методы — это List.of(...), Integer.valueOf(...) и Path.of(...).
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=]
Когда у конструктора четыре-пять параметров, в вызове new Report("Aysel", "Math", 95, "Excellent") трудно понять, какое значение что означает, а для необязательных параметров нужно много конструкторов. Builder (строитель) собирает объект шаг за шагом именованными методами, и в конце build() возвращает готовый неизменяемый объект. Знакомые примеры: 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 (стратегия) превращает алгоритм в объект, который можно передать снаружи: GradeBoard не знает, что внутри бонусного или строгого правила, и чтобы добавить новое правило, его не нужно править — это буква «O» из SOLID. Comparator — тоже стратегия. Observer (наблюдатель) доставляет изменение одного объекта всем подписанным на него: приложение для родителей и журнал учителя получают одно и то же событие, а GradeBoard ничего о них не знает. Так же работают слушатели кнопок в графических интерфейсах и события Spring.
| Паттерн | Какую задачу решает | В JDK и Spring |
|---|---|---|
| Singleton | достаточно одного экземпляра: настройки, кэш | Runtime.getRuntime() |
| Factory | скрыть, какой класс создаётся | List.of(), Path.of() |
| Builder | понятно собрать объект с множеством параметров | StringBuilder, HttpClient.newBuilder() |
| Strategy | менять алгоритм во время работы | Comparator |
| Observer | оповещать многих подписчиков о событии | PropertyChangeListener, ApplicationEventPublisher |
Главное
- Чистый код: говорящие имена, маленькие методы, константы вместо магических чисел, неизменяемость,
Optional, никаких пустыхcatch. - Рефакторинг улучшает структуру, не меняя поведения; тесты делают его безопасным.
- SOLID: одна ответственность, расширение без правки, заменяемость подклассов, маленькие интерфейсы, зависимость от абстракций.
- Порождающие паттерны: Singleton (
enum), Factory (of(...)), Builder (build()). - Поведенческие паттерны: Strategy — сменный алгоритм (
Comparator), Observer — оповещение подписчиков.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.