Паттерны проектирования GoF в Java: примеры из JDK для разработки
Для кого эта статья:
- Java-разработчики, стремящиеся улучшить свои навыки и самосовершенствоваться
- Программисты, интересующиеся паттернами проектирования и их практическим применением
Студенты и новички в области программирования, желающие обновить свои знания о Java и проектировании систем
Знание паттернов проектирования GoF — не просто строчка в резюме Java-разработчика, а признак зрелости мышления программиста. Но теория без конкретных примеров часто остаётся абстракцией. Интересно, что разработчики JDK уже позаботились о том, чтобы предоставить нам живые примеры всех 23 классических паттернов. Сегодня я раскрою 15 реальных реализаций паттернов GoF, которые вы ежедневно используете в своём коде — даже если никогда не задумывались об этом. 🔍 Эти примеры станут вашей шпаргалкой для создания действительно профессионального кода.
Если вы стремитесь не просто узнать о паттернах, но научиться эффективно применять их в реальных проектах, обратите внимание на Курс Java-разработки от Skypro. Программа включает не только теорию паттернов проектирования, но и глубокое погружение в практику — вы будете разбирать реальный код из JDK, создавать собственные реализации и работать над коммерческими проектами под руководством практикующих разработчиков. Вы не просто выучите паттерны, а научитесь мыслить паттернами.
Значение паттернов GoF в профессиональной Java-разработке
Паттерны проектирования Gang of Four (GoF) — это не просто академические концепции, а испытанные временем решения повторяющихся проблем в разработке программного обеспечения. В экосистеме Java они имеют особенное значение, поскольку сама платформа была спроектирована с их учётом.
Почему знание паттернов критически важно для Java-разработчика:
- Они обеспечивают общий словарь и понимание между разработчиками
- Позволяют избежать "изобретения велосипеда" при решении стандартных проблем
- Существенно повышают поддерживаемость кода и снижают его сложность
- Упрощают рефакторинг и развитие приложения с течением времени
- Часто используются при проектировании API и фреймворков
Взглянем на то, как стандартные библиотеки Java применяют паттерны в реальном коде:
| Категория паттернов | Количество в JDK | Примеры классов | Преимущества использования |
|---|---|---|---|
| Порождающие | 5+ | java.util.Calendar, java.nio.file.Files | Абстрагирование создания объектов |
| Структурные | 7+ | java.util.Collections, java.io.InputStream | Организация классов и объектов |
| Поведенческие | 11+ | java.util.Comparator, java.util.concurrent | Координация алгоритмов и ответственности |
Максим Петров, руководитель отдела разработки
Однажды наша команда получила задачу разработать новый модуль обработки финансовых транзакций. Код рос как снежный ком, а с ним росли проблемы с поддержкой. Кризис наступил, когда из-за плохой структуры кода мы пропустили в продакшн баг, который стоил компании серьезных денег.
После этого случая я организовал серию встреч по изучению паттернов GoF на примерах из JDK. Мы изучали, как стандартные библиотеки решают похожие проблемы. Первым делом переработали код создания различных типов транзакций, применив Factory Method по аналогии с java.util.Calendar — и код сразу стал чище.
За следующие три месяца мы постепенно внедрили еще 6 паттернов, напрямую заимствуя подходы из JDK. Количество багов снизилось на 70%, время на внедрение новых фич сократилось вдвое. Теперь у нас правило — перед решением архитектурной проблемы всегда смотрим, как с этим справляются в JDK.
Важно понимать, что паттерны не являются готовыми решениями, которые можно слепо копировать. Это скорее шаблоны мышления, принципы организации кода. Изучая, как они применяются в стандартных библиотеках Java, вы получаете представление о лучших практиках их использования от экспертов, создавших язык и платформу. 🧩

Порождающие паттерны: от Singleton в Runtime до Builder
Порождающие паттерны в Java SDK демонстрируют изящные решения для контроля процесса создания объектов. Давайте рассмотрим наиболее яркие примеры их реализации в базовых библиотеках. 🏗️
1. Singleton в классе Runtime
Класс java.lang.Runtime — классический пример паттерна Singleton. Вы не можете создать экземпляр этого класса напрямую, вместо этого используется метод getRuntime():
// Нельзя создать напрямую – конструктор приватный
// Runtime runtime = new Runtime(); // Ошибка компиляции
// Получаем единственный экземпляр
Runtime runtime = Runtime.getRuntime();
Реализация в JDK защищена от многопоточности и отражает лучшие практики:
public class Runtime {
private static final Runtime currentRuntime = new Runtime();
private Runtime() {} // Приватный конструктор
public static Runtime getRuntime() {
return currentRuntime;
}
// ...
}
2. Factory Method в коллекциях
Класс Collections предоставляет множество фабричных методов для создания специализированных коллекций:
// Фабричный метод для создания неизменяемого списка
List<String> immutableList = Collections.unmodifiableList(originalList);
// Фабричный метод для создания синхронизированной карты
Map<String, Integer> syncMap = Collections.synchronizedMap(originalMap);
3. Abstract Factory в javax.xml.parsers
Пакет javax.xml.parsers содержит абстрактные фабрики для создания XML-парсеров:
// Получаем фабрику для DOM-парсеров
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// Используем фабрику для создания конкретного парсера
DocumentBuilder builder = factory.newDocumentBuilder();
4. Builder в StringBuilder и документах
Паттерн Builder изящно реализован в java.lang.StringBuilder и java.lang.StringBuffer:
StringBuilder builder = new StringBuilder()
.append("Привет")
.append(", ")
.append("мир")
.append("!")
.append(" Число: ")
.append(42);
String result = builder.toString(); // "Привет, мир! Число: 42"
Также яркий пример — DocumentBuilder для построения XML-документов.
5. Prototype в клонировании объектов
Интерфейс java.lang.Cloneable и метод clone() — пример паттерна Prototype:
ArrayList<String> original = new ArrayList<>();
original.add("item");
// Создаем копию через клонирование
ArrayList<String> copy = (ArrayList<String>) original.clone();
| Порождающий паттерн | Класс в Java SDK | Ключевые методы | Когда использовать |
|---|---|---|---|
| Singleton | java.lang.Runtime<br>java.awt.Desktop | getRuntime()<br>getDesktop() | Когда нужен ровно один экземпляр объекта |
| Factory Method | java.util.Calendar<br>java.util.Collections | getInstance()<br>synchronizedList() | Для создания объектов без указания конкретных классов |
| Abstract Factory | javax.xml.parsers<br>javax.sql | newInstance()<br>getConnection() | Когда нужны семейства связанных объектов |
| Builder | java.lang.StringBuilder<br>javax.swing | append()<br>add() | Для пошагового конструирования сложных объектов |
| Prototype | java.lang.Object<br>java.util.ArrayList | clone()<br>clone() | Когда создание объекта дорого или сложно |
Глубокое понимание этих паттернов не просто обогатит ваш арсенал, но и позволит вам увидеть логику проектирования стандартной библиотеки Java, а значит, эффективнее использовать её возможности.
Структурные паттерны: Adapter, Composite и Proxy в JDK
Структурные паттерны определяют способы организации классов и объектов для формирования более крупных структур. JDK предоставляет превосходные примеры этих паттернов, которые мы используем ежедневно. 🏛️
1. Adapter в коллекциях и потоках ввода-вывода
Паттерн Adapter великолепно представлен в Java SDK в различных контекстах:
Arrays.asList() — адаптирует массив к интерфейсу List:
String[] array = {"Java", "Python", "C++"};
List<String> list = Arrays.asList(array);
// Теперь можно работать с массивом через интерфейс List
InputStreamReader и OutputStreamWriter — адаптируют байтовые потоки к символьным:
// Адаптер для преобразования байтового потока в символьный
InputStream is = new FileInputStream("file.txt");
Reader reader = new InputStreamReader(is, "UTF-8");
2. Decorator в потоках ввода-вывода
Вся архитектура потоков ввода-вывода в Java построена на паттерне Decorator:
// Базовый компонент
InputStream fileStream = new FileInputStream("data.txt");
// Декорируем функциональностью буферизации
InputStream bufferedStream = new BufferedInputStream(fileStream);
// Декорируем функциональностью сжатия
InputStream compressedStream = new GZIPInputStream(bufferedStream);
Этот подход позволяет гибко комбинировать функциональность потоков, добавляя новые возможности без изменения исходных классов.
3. Composite в пользовательском интерфейсе
Пакет java.awt и javax.swing используют паттерн Composite для построения иерархии графических компонентов:
// Контейнер (composite)
JPanel panel = new JPanel();
// Добавляем компоненты (листья)
panel.add(new JButton("Нажми меня"));
panel.add(new JTextField(20));
// Добавляем вложенный контейнер (composite)
JPanel innerPanel = new JPanel();
innerPanel.add(new JCheckBox("Опция"));
panel.add(innerPanel);
4. Proxy в java.lang.reflect и RMI
Java SDK активно использует паттерн Proxy в различных формах:
java.lang.reflect.Proxy — для создания динамических прокси:
// Создание прокси для интерфейса MyInterface
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class<?>[] { MyInterface.class },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Дополнительная логика до вызова
Object result = method.invoke(realObject, args);
// Дополнительная логика после вызова
return result;
}
}
);
Java RMI (Remote Method Invocation) также основан на паттерне Proxy, скрывая сложности сетевого взаимодействия:
// Клиентский код обращается к прокси, который выглядит как обычный объект
MyRemoteInterface remoteObject = (MyRemoteInterface) Naming.lookup("rmi://server/MyRemoteObject");
// Но на самом деле вызов происходит на удалённой машине
remoteObject.remoteMethod();
5. Facade в высокоуровневых API
Многие высокоуровневые API в Java SDK используют паттерн Facade для упрощения сложных подсистем:
// javax.imageio предоставляет фасад для работы с изображениями
BufferedImage image = ImageIO.read(new File("image.jpg"));
ImageIO.write(image, "png", new File("output.png"));
// java.nio.file.Files – фасад для файловых операций
Path path = Paths.get("data.txt");
List<String> lines = Files.readAllLines(path);
Files.write(path, lines);
Антон Сидоров, архитектор ПО
Проблема возникла в крупном проекте для банка, когда нам потребовалось интегрировать новую систему обработки платежей. Старый код использовал один интерфейс, а новая система — совершенно другой. Переписывать всю логику было нереалистично — слишком много зависимостей.
Я вспомнил, как в JDK решается подобная проблема. Взять хотя бы InputStreamReader, который адаптирует байтовые потоки к символьным. И мы реализовали адаптер:
JavaСкопировать кодpublic class NewPaymentSystemAdapter implements OldPaymentInterface { private NewPaymentSystem newSystem; public NewPaymentSystemAdapter(NewPaymentSystem newSystem) { this.newSystem = newSystem; } @Override public boolean processPayment(Payment payment) { // Преобразование старого формата в новый NewPaymentRequest request = convertToNewFormat(payment); // Делегирование вызова новой системе PaymentResponse response = newSystem.sendPaymentRequest(request); // Преобразование результата обратно return convertResponseToOldFormat(response); } private NewPaymentRequest convertToNewFormat(Payment payment) { // Логика преобразования } private boolean convertResponseToOldFormat(PaymentResponse response) { // Логика преобразования } }Это решение позволило нам постепенно переводить систему на новую архитектуру без прерывания работы. Со временем мы переписали большую часть кода, но некоторые адаптеры до сих пор работают — они стали частью нашей системы наследия.
Поведенческие паттерны: Observer и Strategy внутри Java
Поведенческие паттерны определяют способы взаимодействия между объектами, распределения ответственности и координации алгоритмов. В JDK мы находим превосходные примеры их практического применения. 🔄
1. Observer/Observable (устаревший, но показательный)
Хотя java.util.Observable и java.util.Observer устарели с Java 9, они наглядно демонстрируют паттерн Observer:
// Класс, за которым наблюдают (издатель)
public class DataModel extends Observable {
private int value;
public void setValue(int value) {
this.value = value;
setChanged(); // Помечаем, что произошло изменение
notifyObservers(); // Уведомляем наблюдателей
}
public int getValue() {
return value;
}
}
// Наблюдатель (подписчик)
public class DataView implements Observer {
@Override
public void update(Observable o, Object arg) {
if (o instanceof DataModel) {
System.out.println("Новое значение: " + ((DataModel)o).getValue());
}
}
}
// Использование
DataModel model = new DataModel();
DataView view = new DataView();
model.addObserver(view);
model.setValue(42); // Выведет: "Новое значение: 42"
Современная альтернатива — PropertyChangeListener и PropertyChangeSupport.
2. Strategy в Comparator и Collections.sort()
Интерфейс java.util.Comparator — прекрасный пример паттерна Strategy:
List<Person> people = Arrays.asList(
new Person("Алексей", 30),
new Person("Мария", 25),
new Person("Иван", 35)
);
// Стратегия сортировки по имени
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().compareTo(p2.getName());
}
});
// Альтернативная стратегия сортировки по возрасту
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return Integer.compare(p1.getAge(), p2.getAge());
}
});
// С лямбда-выражениями (Java 8+)
Collections.sort(people, (p1, p2) -> p1.getName().compareTo(p2.getName()));
Collections.sort(people, Comparator.comparing(Person::getAge));
3. Template Method в AbstractList и других абстрактных классах
java.util.AbstractList определяет шаблонный метод iterator(), который опирается на абстрактный метод get():
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
// Шаблонный метод, определяющий алгоритм
public Iterator<E> iterator() {
return new Iterator<E>() {
private int cursor = 0;
public boolean hasNext() {
return cursor < size();
}
public E next() {
return get(cursor++); // Вызов абстрактного метода
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
// Абстрактные методы, которые должны быть реализованы подклассами
public abstract E get(int index);
public abstract int size();
}
При наследовании достаточно реализовать методы get() и size(), а iterator() будет работать автоматически.
4. Command в java.lang.Runnable
Интерфейс java.lang.Runnable — чистый пример паттерна Command:
// Команда для выполнения
Runnable command = new Runnable() {
@Override
public void run() {
System.out.println("Выполнение команды");
}
};
// Вызывающий код
Thread thread = new Thread(command);
thread.start();
// С Java 8 можно использовать лямбда-выражение
Runnable lambdaCommand = () -> System.out.println("Выполнение команды через лямбду");
new Thread(lambdaCommand).start();
5. Chain of Responsibility в обработке исключений и Servlet фильтрах
Java использует паттерн Chain of Responsibility в механизме обработки исключений:
try {
// Код, который может выбросить исключение
int result = divide(10, 0);
} catch (ArithmeticException e) {
// Обработчик для ArithmeticException
System.out.println("Ошибка арифметики: " + e.getMessage());
} catch (Exception e) {
// Обработчик для всех других исключений
System.out.println("Общая ошибка: " + e.getMessage());
} finally {
// Код, выполняемый в любом случае
System.out.println("Завершение обработки");
}
Также в Java EE этот паттерн применяется в цепочке фильтров Servlet:
public class AuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// Аутентификация пользователя
if (isAuthenticated(request)) {
// Передача запроса следующему фильтру в цепочке
chain.doFilter(request, response);
} else {
// Прерывание цепочки и отправка ошибки
response.getWriter().write("Доступ запрещен");
}
}
// ...
}
| Поведенческий паттерн | Примеры в JDK | Основное применение |
|---|---|---|
| Observer | java.util.Observable (устаревший)<br>PropertyChangeListener | Рассылка уведомлений множеству объектов |
| Strategy | java.util.Comparator<br>java.util.concurrent.Executor | Выбор алгоритма во время выполнения |
| Template Method | java.util.AbstractList<br>java.util.AbstractSet | Определение скелета алгоритма в базовом классе |
| Command | java.lang.Runnable<br>javax.swing.Action | Инкапсуляция действия как объекта |
| Chain of Responsibility | Exception Handling<br>Servlet Filters | Передача запроса по цепочке обработчиков |
| Iterator | java.util.Iterator<br>java.util.Enumeration | Последовательный доступ к элементам коллекции |
| State | javax.faces.lifecycle<br>TCP Connection | Изменение поведения объекта при изменении состояния |
Практическое применение паттернов из библиотек в своих проектах
Изучение паттернов проектирования на примерах JDK — лишь первый шаг. Настоящая ценность возникает, когда вы начинаете осознанно применять эти паттерны в собственных проектах. 🚀
Когда и как использовать паттерны из JDK
Успешное применение паттернов проектирования требует понимания, когда их следует использовать:
- При возникновении знакомых проблем: Если вы узнаете ситуацию, для которой существует паттерн, используйте проверенное решение вместо изобретения своего
- При необходимости совместимости: Если ваш код должен взаимодействовать с API, использующим определённый паттерн
- При рефакторинге: Когда существующий код становится сложным или трудным для поддержки
- На этапе проектирования: При планировании архитектуры сложных систем
Практические шаги по применению паттернов из JDK
Вот поэтапный процесс адаптации паттернов из JDK к вашим проектам:
- Идентификация проблемы: Определите архитектурную проблему, которую необходимо решить
- Сопоставление с паттернами: Найдите паттерн, который лучше всего соответствует проблеме
- Изучение эталонной реализации: Исследуйте, как этот паттерн реализован в JDK
- Адаптация к своим потребностям: Измените решение в соответствии с контекстом вашего проекта
- Тестирование и оценка: Убедитесь, что новое решение работает корректно и действительно улучшает дизайн
Примеры адаптации паттернов из JDK к собственным проектам
Рассмотрим несколько конкретных примеров применения паттернов из JDK:
1. Создание собственного Builder, вдохновлённого StringBuilder:
public class QueryBuilder {
private StringBuilder query = new StringBuilder();
private boolean whereAdded = false;
public QueryBuilder select(String... columns) {
query.append("SELECT ");
query.append(String.join(", ", columns));
return this;
}
public QueryBuilder from(String table) {
query.append(" FROM ").append(table);
return this;
}
public QueryBuilder where(String condition) {
if (!whereAdded) {
query.append(" WHERE ");
whereAdded = true;
} else {
query.append(" AND ");
}
query.append(condition);
return this;
}
@Override
public String toString() {
return query.toString();
}
}
// Использование
String sql = new QueryBuilder()
.select("id", "name", "email")
.from("users")
.where("status = 'active'")
.where("age > 18")
.toString();
// SQL: "SELECT id, name, email FROM users WHERE status = 'active' AND age > 18"
2. Применение паттерна Decorator для логирования вызовов сервиса:
// Базовый интерфейс сервиса
public interface UserService {
User findById(Long id);
void save(User user);
}
// Реальная реализация
public class UserServiceImpl implements UserService {
@Override
public User findById(Long id) {
// Реальная логика получения пользователя
return userRepository.findById(id);
}
@Override
public void save(User user) {
// Реальная логика сохранения пользователя
userRepository.save(user);
}
}
// Декоратор для добавления логирования
public class LoggingUserService implements UserService {
private final UserService userService;
private final Logger logger = LoggerFactory.getLogger(LoggingUserService.class);
public LoggingUserService(UserService userService) {
this.userService = userService;
}
@Override
public User findById(Long id) {
logger.info("Запрос пользователя с id: {}", id);
try {
User user = userService.findById(id);
logger.info("Пользователь найден: {}", user);
return user;
} catch (Exception e) {
logger.error("Ошибка при поиске пользователя: {}", e.getMessage());
throw e;
}
}
@Override
public void save(User user) {
logger.info("Сохранение пользователя: {}", user);
try {
userService.save(user);
logger.info("Пользователь успешно сохранен");
} catch (Exception e) {
logger.error("Ошибка при сохранении пользователя: {}", e.getMessage());
throw e;
}
}
}
// Использование
UserService userService = new UserServiceImpl();
UserService loggingService = new LoggingUserService(userService);
// Теперь все вызовы логируются
User user = loggingService.findById(42L);
3. Создание собственного итератора, вдохновленного Iterator из коллекций:
public class TreeNodeIterator implements Iterator<TreeNode> {
private final Queue<TreeNode> queue = new LinkedList<>();
public TreeNodeIterator(TreeNode root) {
if (root != null) {
queue.add(root);
}
}
@Override
public boolean hasNext() {
return !queue.isEmpty();
}
@Override
public TreeNode next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
TreeNode current = queue.poll();
if (current.getLeft() != null) {
queue.add(current.getLeft());
}
if (current.getRight() != null) {
queue.add(current.getRight());
}
return current;
}
}
// Использование
TreeNode root = buildTree();
Iterator<TreeNode> iterator = new TreeNodeIterator(root);
while (iterator.hasNext()) {
TreeNode node = iterator.next();
System.out.println(node.getValue());
}
Избегаем антипаттернов при использовании паттернов
При использовании паттернов проектирования важно не впадать в крайности:
- "Паттермомания": Не используйте паттерны только потому, что они существуют, если простое решение достаточно
- Неуместное применение: Тщательно анализируйте, подходит ли выбранный паттерн для вашей конкретной ситуации
- Чрезмерное усложнение: Паттерны должны упрощать, а не усложнять ваш дизайн
- Игнорирование контекста: Адаптируйте паттерны к вашим конкретным потребностям, а не слепо копируйте
Изучение паттернов проектирования через призму стандартных библиотек Java — мощный инструмент роста для любого разработчика. Вы не просто заучиваете абстрактные концепции, но видите их успешное применение в коде, написанном лучшими инженерами. Постепенно эти паттерны становятся частью вашего мышления, позволяя проектировать гибкие и поддерживаемые системы. Но настоящее мастерство приходит только тогда, когда вы начинаете осознанно распознавать ситуации, требующие определенного паттерна, и применять подходящее решение — не потому что вы можете, а потому что это действительно нужно.