Паттерны проектирования в Java: не просто галочка в резюме

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Паттерны проектирования в Java: не просто галочка в резюме

Каждый Java-разработчик хотя бы раз слышал фразу: "Используй паттерны проектирования". Но часто это звучит как мантра. Мол, если ты не используешь Singleton или Factory — ты плохой программист. На самом деле паттерны проектирования в Java — это не догма и не обязательный элемент каждого проекта. Это инструмент. И как любой инструмент, его нужно применять к месту.

В этой статье мы разберемся, зачем вообще нужны паттерны, когда их применение оправдано, а когда лучше написать простой код без изысков. И конечно, посмотрим на живые примеры на Java.

Что такое паттерны проектирования и почему они важны

Паттерны проектирования — это проверенные решения типовых задач. Их не надо выдумывать заново. Они родились из опыта тысяч разработчиков. Когда вы сталкиваетесь с проблемой, которую кто-то уже решил до вас, паттерн дает готовую схему.

Но есть нюанс. Паттерны проектирования в Java не существуют в вакууме. Они тесно связаны с принципами SOLID. Если вы не понимаете почему нужен интерфейс или зачем выносить логику в отдельный класс, паттерны вам не помогут. Они лишь формализуют эти принципы.

Когда стоит применять паттерны, а когда нет

Золотое правило: паттерн — это решение, а не цель. Не надо внедрять Abstract Factory просто потому, что "так принято". Спросите себя:

  • Решает ли этот паттерн мою текущую проблему?
  • Упростит ли он поддержку кода в будущем?
  • Не усложнит ли он понимание для новичков в команде?

Если на все три ответа "да" — смело используйте. Если хотя бы один "нет" — возможно, стоит поискать более простое решение.

Когда паттерны действительно нужны:

  • Код разрастается и становится трудно поддерживать
  • Требуется гибкость: добавление нового функционала без изменения старого
  • Работа в команде: паттерны — это общий язык

Основные паттерны проектирования в Java с примерами

Рассмотрим четыре самых популярных паттерна из Gang of Four. Они покрывают 80% типовых задач.

1. Singleton: когда объект должен быть один

Назначение: Гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему.

Когда использовать: Логгеры, конфигурации, пулы соединений с БД. Но не злоупотребляйте — Singleton часто критикуют за скрытые зависимости.

Пример в Java:

public class DatabaseConnectionPool {
    private static volatile DatabaseConnectionPool instance;
    private final int maxConnections = 10;

    private DatabaseConnectionPool() {
        // приватный конструктор
    }

    public static DatabaseConnectionPool getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnectionPool.class) {
                if (instance == null) {
                    instance = new DatabaseConnectionPool();
                }
            }
        }
        return instance;
    }

    public Connection getConnection() {
        // логика получения соединения
        return new Connection();
    }
}

Обратите внимание на volatile и двойную проверку. Это потокобезопасный Singleton. Без этого в многопоточной среде можно получить два разных экземпляра.

2. Factory Method: делегируем создание объектов

Назначение: Определяет интерфейс для создания объекта, но позволяет подклассам изменять тип создаваемого объекта.

Когда использовать: Когда заранее неизвестно, какой конкретный класс понадобится. Например, создание разных типов документов (PDF, Word, Excel).

Пример в Java:

// Интерфейс продукта
interface Document {
    void open();
    void save();
}

// Конкретные продукты
class PdfDocument implements Document {
    public void open() { System.out.println("Открываем PDF"); }
    public void save() { System.out.println("Сохраняем PDF"); }
}

class WordDocument implements Document {
    public void open() { System.out.println("Открываем Word"); }
    public void save() { System.out.println("Сохраняем Word"); }
}

// Фабрика
abstract class DocumentFactory {
    public abstract Document createDocument();
}

class PdfFactory extends DocumentFactory {
    public Document createDocument() { return new PdfDocument(); }
}

class WordFactory extends DocumentFactory {
    public Document createDocument() { return new WordDocument(); }
}

// Использование
public class Application {
    public static void main(String[] args) {
        DocumentFactory factory = new PdfFactory();
        Document doc = factory.createDocument();
        doc.open();
    }
}

Теперь, чтобы добавить поддержку Excel, не нужно трогать существующий код. Просто создаем новый класс и фабрику.

3. Builder: собираем сложные объекты по частям

Назначение: Отделяет конструирование сложного объекта от его представления. Позволяет создавать разные конфигурации одного и того же класса.

Когда использовать: Когда у объекта много параметров, особенно необязательных. Классический пример — SQL-запросы или объекты с десятками полей.

Пример в Java:

public class User {
    private final String firstName;  // обязательное
    private final String lastName;   // обязательное
    private final int age;           // опциональное
    private final String phone;      // опциональное
    private final String address;    // опциональное

    private User(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.phone = builder.phone;
        this.address = builder.address;
    }

    public static class Builder {
        private final String firstName;
        private final String lastName;
        private int age = 0;
        private String phone = "";
        private String address = "";

        public Builder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder phone(String phone) {
            this.phone = phone;
            return this;
        }

        public Builder address(String address) {
            this.address = address;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

// Использование
User user = new User.Builder("Иван", "Иванов")
        .age(30)
        .phone("+7-999-123-45-67")
        .build();

Обратите внимание: мы избежали "телескопического конструктора" (когда методов много с разным набором параметров). Код стал читаемым и гибким.

4. Observer: уведомляем всех, кому это нужно

Назначение: Определяет зависимость "один ко многим" между объектами. При изменении состояния одного объекта все зависимые оповещаются автоматически.

Когда использовать: Системы событий, UI-компоненты, логирование, рассылка уведомлений.

Пример в Java:

import java.util.ArrayList;
import java.util.List;

// Наблюдатель
interface Observer {
    void update(String message);
}

// Конкретный наблюдатель
class EmailNotifier implements Observer {
    private String email;

    public EmailNotifier(String email) {
        this.email = email;
    }

    public void update(String message) {
        System.out.println("Отправка email на " + email + ": " + message                

Блоги

Рекомендаций по книгам