Перейти к содержанию
Educora
Продвинутый22 мин18 / 18

Чистый код, SOLID и паттерны проектирования

Изучи правила читаемого кода, принципы SOLID и пять основных паттернов проектирования — Singleton, Factory, Builder, Strategy, Observer — на коротких примерах на Java.

Проверь себя
В этом уроке ты узнаешь
  • Рефакторить существующий метод по правилам чистого кода
  • Объяснять пять принципов 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 — аббревиатура пяти принципов объектно-ориентированного проектирования, которые популяризировал Роберт Мартин. Они делают код устойчивым к изменениям: когда приходит новое требование, меняешь одно место, а не десять.

БукваПринципКоротко
SSingle ResponsibilityУ класса должна быть только одна причина для изменения: контроллер занимается HTTP, сервис — правилами, репозиторий — базой.
OOpen/ClosedКод открыт для расширения, но закрыт для изменения: новое правило добавляется новым классом без правки старого кода.
LLiskov SubstitutionПодкласс должен работать везде, где ожидается родительский класс, не нарушая ожиданий.
IInterface SegregationНесколько маленьких интерфейсов вместо одного большого: класс не должен реализовывать методы, которые ему не нужны.
DDependency InversionЗависи от абстракций (интерфейсов), а не от конкретных классов — в уроке о JUnit ReportService зависел от интерфейса ScoreRepository.

Паттерны проектирования

Паттерн проектирования (design pattern) — проверенная схема решения часто встречающейся задачи. В 1994 году «Банда четырёх» (Gang of Four: Гамма, Хелм, Джонсон, Влиссидес) собрала в книге 23 паттерна и разделила их на три группы: порождающие (как создаются объекты), структурные (как объекты объединяются) и поведенческие (как объекты взаимодействуют). Ниже — пять паттернов, которые чаще всего встречаются в Java.

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
Singleton (через enum) и Factory (статический фабричный метод)

Singleton (одиночка) гарантирует, что у класса только один объект. В Java самый простой и потокобезопасный способ — enum с одним элементом; бины Spring по умолчанию тоже одиночки. Factory (фабрика) создаёт объект методом, а не через new, и скрывает, какой класс создаётся: вызывающий код знает только интерфейс Notifier. В JDK фабричные методы — это List.of(...), Integer.valueOf(...) и Path.of(...).

Java
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=]
Builder: для множества параметров, часть из которых необязательна

Когда у конструктора четыре-пять параметров, в вызове new Report("Aysel", "Math", 95, "Excellent") трудно понять, какое значение что означает, а для необязательных параметров нужно много конструкторов. Builder (строитель) собирает объект шаг за шагом именованными методами, и в конце build() возвращает готовый неизменяемый объект. Знакомые примеры: StringBuilder, HttpClient.newBuilder(), Stream.builder().

Java
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 (сменное правило оценивания) и Observer (оповещение подписчиков)

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.

1 / 10
Как в Java просто и потокобезопасно написать Singleton?