Введение в шаблоны проектирования Java
Введение в шаблоны проектирования Java — что это такое и как использовать наиболее распространённые из них.
Шаблон проектирования — это именованное, многократно используемое решение задачи, которая регулярно возникает при разработке программного обеспечения. Шаблоны — это не библиотеки для импорта и не код для копирования: это формы организации классов и объектов, которые опытные разработчики выработали со временем. Изучение шаблонов даёт вам общий словарь: скажите «здесь используем Factory» — и другой Java-разработчик сразу поймёт, что вы имеете в виду.
На этой странице рассказывается, что такое шаблоны проектирования, какие три семейства они образуют, и разбираются три самых распространённых — Strategy, Factory и Singleton — с запускаемым примером, который объединяет их. Предполагается, что вы уже уверенно работаете с интерфейсами и полиморфизмом, поскольку почти каждый шаблон опирается на них.
Шаблоны стали популярны благодаря книге 1994 года Design Patterns «Банды четырёх», в которой описано 23 шаблона. Не нужно знать все 23, чтобы начать. Несколько из них, применённых в нужный момент, делают код проще в изменении без риска что-то сломать.
Три семейства
Классический каталог делит шаблоны на три группы по тому, с чем они помогают:
| Семейство | Задача | Примеры |
|---|---|---|
| Порождающие | Как создаются объекты | Singleton, Factory, Builder |
| Структурные | Как объекты компонуются | Adapter, Decorator, Facade |
| Поведенческие | Как объекты взаимодействуют | Strategy, Observer, Iterator |
В самом Java таких шаблонов множество. StringBuilder — это Builder, Iterator — шаблон Iterator, а java.util.logging использует Singleton. Вы уже применяли шаблоны, не называя их по именам.
Strategy: замена алгоритма
Шаблон Strategy объединяет семейство взаимозаменяемых алгоритмов за общим интерфейсом, чтобы вызывающий код мог переключаться между ними без изменения. Определите интерфейс, реализуйте каждый вариант как отдельный класс и позвольте контексту хранить нужный.
interface DiscountStrategy {
double apply(double price);
}
class PercentOff implements DiscountStrategy {
private final double percent;
PercentOff(double percent) { this.percent = percent; }
public double apply(double price) { return price * (1 - percent / 100); }
}Класс контекста хранит один DiscountStrategy и делегирует ему работу вместо цепочки if/switch. Добавление нового вида скидки означает добавление класса — без изменения существующего кода. (Полный контекст Checkout, а также варианты NoDiscount и FlatOff представлены в запускаемом примере ниже.)
Factory: централизованное создание
Factory — это единственный метод (или класс), отвечающий за выбор конкретного типа для создания экземпляра. Вызывающий код запрашивает объект по описанию и получает его, реализующий интерфейс, не зная точного класса.
static DiscountStrategy forCustomer(String tier) {
return switch (tier) {
case "gold" -> new PercentOff(20);
case "silver" -> new PercentOff(10);
default -> new NoDiscount();
};
}Логика создания сосредоточена в одном месте. При изменении правил достаточно отредактировать фабрику — все вызывающие части кода продолжат работать без изменений.
Singleton: ровно один экземпляр
Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. В современном Java самый простой и потокобезопасный способ — использовать enum: JVM гарантирует, что его константы создаются ровно один раз. Варианты с ленивой инициализацией и двойной проверкой блокировки рассматриваются в разделе Шаблон Singleton.
enum Config {
INSTANCE;
private final String env = "production";
public String env() { return env; }
}
// usage
String e = Config.INSTANCE.env();Используйте Singleton умеренно — он вводит глобальное состояние, что усложняет тестирование. Нередко лучшим выбором оказывается передача единственного объекта через конструктор (внедрение зависимостей).
Когда не стоит применять шаблон
Шаблоны добавляют структуру, а структура имеет свою цену. switch с двумя вариантами не нуждается в шаблоне Strategy; класс, создаваемый один раз, не нуждается в Factory. Обращайтесь к шаблону тогда, когда ощущаете ту боль, которую он решает — дублирующиеся ветвления, разбросанные вызовы new, запутанные зависимости объектов, — а не прежде. Чрезмерное применение шаблонов порождает код, который сложнее читать, чем исходная задача.
Strategy и Factory вместе
Приведённый ниже запускаемый пример объединяет два шаблона. DiscountStrategy — это интерфейс Strategy с тремя реализациями; forCustomer — Factory, которая выбирает одну из них. Контекст Checkout делегирует работу той стратегии, которую хранит, поэтому его код не изменяется по мере смены ценового поведения.
Что следует вынести из запуска:
- Каждый уровень выводит разную итоговую сумму —
regularостаётся $100.00,silverснижается до $90.00,goldдо $80.00,couponдо $95.00 — хотяCheckout.totalвызывает один и тот же единственный метод. - Контекст
Checkoutникогда не ветвится по уровню сам; он просто делегирует работу тойDiscountStrategy, которую хранит в данный момент. - Factory
forCustomer— единственное место, которое знает, какой конкретный класс соответствует какому уровню, поэтому логика выбора сосредоточена в одном месте. - Смена поведения — это просто
setStrategy(...)во время выполнения: добавление четвёртой скидки потребует нового класса и одного case в фабрике без каких-либо изменений вCheckout. - Последние строки подтверждают активную стратегию: после повторного выбора
goldполитика показывает20.0% off, а итог равен $80.00, что доказывает: контекст отражает последнюю установленную стратегию.