Паттерны GOF
Порождающие паттерны (Creational Patterns)
1. Singleton (Одиночка)
Назначение: Гарантирует единственный экземпляр класса в приложении и предоставляет глобальный доступ к нему.
Когда использовать: Подключения к БД, логгеры, кэши, настройки конфигурации.
Ключевые особенности:
- Приватный конструктор
- Статический метод получения экземпляра
- Ленивая инициализация (lazy loading)
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private Connection connection;
private DatabaseConnection() {
// Инициализация соединения
}
public static DatabaseConnection getInstance() {
if (instance == null) {
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
}
Плюсы: Контролируемый доступ, экономия памяти Минусы: Сложность тестирования, нарушение принципа единственной ответственности
2. Factory Method (Фабричный метод)
Назначение: Создание объектов без указания конкретного класса, делегируя решение подклассам.
Когда использовать: Когда заранее неизвестно, какие объекты нужно создавать, или когда создание объекта сложное.
abstract class PaymentProcessor {
abstract Payment createPayment(String type);
public void processPayment(String type, double amount) {
Payment payment = createPayment(type);
payment.pay(amount);
}
}
class CreditCardProcessor extends PaymentProcessor {
@Override
Payment createPayment(String type) {
return new CreditCardPayment();
}
}
Применение в Java: Calendar.getInstance()
, NumberFormat.getInstance()
3. Builder (Строитель)
Назначение: Пошаговое создание сложных объектов с множеством параметров.
Когда использовать: Много параметров конструктора, необязательные параметры, сложная логика создания.
public class HttpRequest {
private String url;
private String method;
private Map<String, String> headers;
private String body;
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = builder.headers;
this.body = builder.body;
}
public static class Builder {
private String url;
private String method = "GET";
private Map<String, String> headers = new HashMap<>();
private String body;
public Builder url(String url) {
this.url = url;
return this;
}
public Builder method(String method) {
this.method = method;
return this;
}
public Builder header(String key, String value) {
headers.put(key, value);
return this;
}
public HttpRequest build() {
return new HttpRequest(this);
}
}
}
Применение: StringBuilder
, Stream API
, Lombok @Builder
Структурные паттерны (Structural Patterns)
4. Adapter (Адаптер)
Назначение: Позволяет объектам с несовместимыми интерфейсами работать вместе.
Когда использовать: Интеграция legacy кода, работа с внешними API, несовместимые интерфейсы.
// Существующий интерфейс
interface MediaPlayer {
void play(String filename);
}
// Внешняя библиотека
class AdvancedMediaPlayer {
void playVlc(String filename) { /* ... */ }
void playMp4(String filename) { /* ... */ }
}
// Адаптер
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;
public MediaAdapter(String audioType) {
advancedPlayer = new AdvancedMediaPlayer();
}
@Override
public void play(String filename) {
if (filename.endsWith(".vlc")) {
advancedPlayer.playVlc(filename);
} else if (filename.endsWith(".mp4")) {
advancedPlayer.playMp4(filename);
}
}
}
5. Decorator (Декоратор)
Назначение: Динамическое добавление функциональности объектам без изменения их структуры.
Когда использовать: Добавление поведения во время выполнения, альтернатива наследованию.
interface Coffee {
double getCost();
String getDescription();
}
class SimpleCoffee implements Coffee {
@Override
public double getCost() { return 1.0; }
@Override
public String getDescription() { return "Simple coffee"; }
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
}
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.5;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", milk";
}
}
Применение в Java: InputStream
, OutputStream
, Collections.synchronizedList()
6. Facade (Фасад)
Назначение: Предоставляет упрощенный интерфейс к сложной подсистеме.
Когда использовать: Сложная система с множеством компонентов, нужен простой интерфейс.
class OrderFacade {
private InventoryService inventoryService;
private PaymentService paymentService;
private ShippingService shippingService;
private NotificationService notificationService;
public boolean placeOrder(Order order) {
if (!inventoryService.isAvailable(order.getItems())) {
return false;
}
if (!paymentService.processPayment(order.getPayment())) {
return false;
}
shippingService.arrangeShipping(order);
notificationService.sendConfirmation(order);
return true;
}
}
Поведенческие паттерны (Behavioral Patterns)
7. Observer (Наблюдатель)
Назначение: Определяет зависимость один-ко-многим между объектами, автоматически уведомляя зависимые объекты об изменениях.
Когда использовать: Event-driven архитектура, MVC, pub/sub системы.
interface Observer {
void update(String event);
}
class EventPublisher {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void notifyObservers(String event) {
observers.forEach(observer -> observer.update(event));
}
}
class EmailNotificationService implements Observer {
@Override
public void update(String event) {
System.out.println("Sending email for event: " + event);
}
}
Применение в Java: java.util.Observable
, Event listeners в GUI
8. Strategy (Стратегия)
Назначение: Определяет семейство алгоритмов, инкапсулирует каждый и делает их взаимозаменяемыми.
Когда использовать: Множество способов выполнения задачи, выбор алгоритма во время выполнения.
interface PaymentStrategy {
void pay(double amount);
}
class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " via credit card");
}
}
class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " via PayPal");
}
}
class PaymentContext {
private PaymentStrategy strategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void executePayment(double amount) {
strategy.pay(amount);
}
}
9. Command (Команда)
Назначение: Инкапсулирует запрос как объект, позволяя параметризовать клиентов с различными запросами.
Когда использовать: Undo/Redo операции, очереди команд, макрокоманды.
interface Command {
void execute();
void undo();
}
class TransferCommand implements Command {
private Account from, to;
private double amount;
public TransferCommand(Account from, Account to, double amount) {
this.from = from;
this.to = to;
this.amount = amount;
}
@Override
public void execute() {
from.withdraw(amount);
to.deposit(amount);
}
@Override
public void undo() {
to.withdraw(amount);
from.deposit(amount);
}
}
class BankingSystem {
private Stack<Command> commandHistory = new Stack<>();
public void executeCommand(Command command) {
command.execute();
commandHistory.push(command);
}
public void undoLastCommand() {
if (!commandHistory.isEmpty()) {
commandHistory.pop().undo();
}
}
}
10. Template Method (Шаблонный метод)
Назначение: Определяет скелет алгоритма в базовом классе, позволяя подклассам переопределять отдельные шаги.
Когда использовать: Общий алгоритм с вариативными шагами, избежание дублирования кода.
abstract class DataProcessor {
// Шаблонный метод
public final void processData() {
loadData();
validateData();
processDataImpl();
saveData();
}
protected abstract void loadData();
protected abstract void processDataImpl();
protected abstract void saveData();
protected void validateData() {
// Общая логика валидации
System.out.println("Validating data...");
}
}
class CSVDataProcessor extends DataProcessor {
@Override
protected void loadData() {
System.out.println("Loading CSV data");
}
@Override
protected void processDataImpl() {
System.out.println("Processing CSV data");
}
@Override
protected void saveData() {
System.out.println("Saving CSV data");
}
}
Ключевые принципы для собеседования
SOLID принципы в паттернах:
- Single Responsibility: Каждый класс имеет одну причину для изменения
- Open/Closed: Открыт для расширения, закрыт для модификации (Strategy, Decorator)
- Liskov Substitution: Подклассы должны заменять базовые классы (Template Method)
- Interface Segregation: Клиенты не должны зависеть от неиспользуемых методов
- Dependency Inversion: Зависимость от абстракций, а не от конкретных классов
Практические советы:
- Выбор паттерна: Понимайте проблему, которую решает каждый паттерн
- Производительность: Учитывайте overhead некоторых паттернов (Decorator, Observer)
- Тестируемость: Паттерны должны упрощать тестирование, а не усложнять
- Читаемость: Код должен быть понятен другим разработчикам
- Избегайте переусложнения: Не используйте паттерны там, где они не нужны
Часто задаваемые вопросы:
- Отличие Strategy от Command: Strategy меняет алгоритм, Command инкапсулирует действие
- Adapter vs Facade: Adapter для совместимости интерфейсов, Facade для упрощения
- Singleton vs Static: Singleton может реализовывать интерфейсы, lazy loading
- Observer vs Pub/Sub: Observer прямая связь, Pub/Sub через посредника
- Factory vs Builder: Factory для простых объектов, Builder для сложных с множеством параметров
Порождающие паттерны
Что такое порождающие паттерны?
Порождающие паттерны решают проблемы создания объектов. Они скрывают сложность создания объектов от клиентского кода и делают систему независимой от того, как объекты создаются, компонуются и представляются.
Основные проблемы, которые решают:
- Сложная логика создания объектов
- Необходимость создания семейства связанных объектов
- Контроль над количеством экземпляров
- Создание объектов с множеством параметров
1. Singleton (Одиночка)
Суть паттерна
Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к этому экземпляру.
Когда использовать
- Подключения к базе данных: Один пул соединений на всё приложение
- Логгеры: Единый механизм логирования
- Кэши: Один экземпляр кэша для всего приложения
- Конфигурация: Настройки приложения должны быть едиными
Ключевые элементы
- Приватный конструктор - запрещает создание экземпляров извне
- Статическое поле - хранит единственный экземпляр
- Статический метод - предоставляет доступ к экземпляру
- Ленивая инициализация - создание экземпляра только при первом обращении
public class DatabaseConnectionPool {
private static volatile DatabaseConnectionPool instance;
private final List<Connection> connections;
private DatabaseConnectionPool() {
connections = new ArrayList<>();
// Инициализация пула соединений
}
public static DatabaseConnectionPool getInstance() {
if (instance == null) {
synchronized (DatabaseConnectionPool.class) {
if (instance == null) {
instance = new DatabaseConnectionPool();
}
}
}
return instance;
}
public Connection getConnection() {
// Логика получения соединения
return connections.get(0);
}
}
Важные детали реализации
Double-checked locking - оптимизация для многопоточности:
- Первая проверка без синхронизации (быстрая)
- Вторая проверка внутри synchronized блока (безопасная)
volatile
обеспечивает видимость изменений между потоками
Альтернативные реализации:
// Enum-based Singleton (рекомендуется)
public enum ConfigurationManager {
INSTANCE;
public void loadConfig() { /* ... */ }
}
// Initialization-on-demand holder
public class LazyHolder {
private static class Holder {
private static final LazyHolder INSTANCE = new LazyHolder();
}
public static LazyHolder getInstance() {
return Holder.INSTANCE;
}
}
Проблемы и решения
- Тестирование: Сложно мокировать → используйте DI контейнеры
- Сериализация: Может нарушить единственность → переопределите
readResolve()
- Reflection: Может создать новый экземпляр → добавьте защиту в конструктор
2. Factory Method (Фабричный метод)
Суть паттерна
Factory Method определяет интерфейс для создания объектов, но позволяет подклассам решать, какой класс инстанцировать.
Когда использовать
- Неизвестно заранее, какие объекты нужно создавать
- Сложная логика создания объектов
- Семейство связанных объектов с общим интерфейсом
- Расширяемость: легко добавлять новые типы объектов
Ключевые элементы
- Абстрактный создатель - определяет фабричный метод
- Конкретные создатели - реализуют фабричный метод
- Продукт - общий интерфейс создаваемых объектов
- Конкретные продукты - различные реализации продукта
// Продукт - общий интерфейс
interface PaymentProcessor {
void processPayment(double amount);
String getPaymentMethod();
}
// Конкретные продукты
class CreditCardProcessor implements PaymentProcessor {
public void processPayment(double amount) {
System.out.println("Processing $" + amount + " via Credit Card");
}
public String getPaymentMethod() { return "Credit Card"; }
}
class PayPalProcessor implements PaymentProcessor {
public void processPayment(double amount) {
System.out.println("Processing $" + amount + " via PayPal");
}
public String getPaymentMethod() { return "PayPal"; }
}
// Абстрактный создатель
abstract class PaymentFactory {
// Фабричный метод
protected abstract PaymentProcessor createProcessor();
// Бизнес-логика использует фабричный метод
public void executePayment(double amount) {
PaymentProcessor processor = createProcessor();
processor.processPayment(amount);
}
}
// Конкретные создатели
class CreditCardFactory extends PaymentFactory {
protected PaymentProcessor createProcessor() {
return new CreditCardProcessor();
}
}
class PayPalFactory extends PaymentFactory {
protected PaymentProcessor createProcessor() {
return new PayPalProcessor();
}
}
Использование в Java API
Calendar.getInstance()
- создает календарь в зависимости от локалиNumberFormat.getInstance()
- форматтер чиселCollections.synchronizedList()
- создает потокобезопасную обёртку
Преимущества
- Слабая связанность: клиент не знает конкретные классы
- Расширяемость: легко добавлять новые типы продуктов
- Единое место создания: вся логика создания в одном месте
3. Abstract Factory (Абстрактная фабрика)
Суть паттерна
Abstract Factory предоставляет интерфейс для создания семейств связанных объектов без указания их конкретных классов.
Когда использовать
- Семейство связанных объектов должно использоваться вместе
- Система должна быть независима от способа создания объектов
- Различные конфигурации продуктов (например, UI для разных ОС)
Ключевые элементы
- Абстрактная фабрика - интерфейс для создания семейства продуктов
- Конкретные фабрики - реализуют создание конкретного семейства
- Абстрактные продукты - интерфейсы для каждого типа продукта
- Конкретные продукты - реализации для каждого семейства
// Абстрактные продукты
interface Database {
void connect();
}
interface Cache {
void store(String key, Object value);
}
// Конкретные продукты для MySQL
class MySQLDatabase implements Database {
public void connect() { System.out.println("Connecting to MySQL"); }
}
class RedisCache implements Cache {
public void store(String key, Object value) {
System.out.println("Storing in Redis: " + key);
}
}
// Конкретные продукты для PostgreSQL
class PostgreSQLDatabase implements Database {
public void connect() { System.out.println("Connecting to PostgreSQL"); }
}
class MemcachedCache implements Cache {
public void store(String key, Object value) {
System.out.println("Storing in Memcached: " + key);
}
}
// Абстрактная фабрика
interface InfrastructureFactory {
Database createDatabase();
Cache createCache();
}
// Конкретные фабрики
class MySQLInfrastructureFactory implements InfrastructureFactory {
public Database createDatabase() { return new MySQLDatabase(); }
public Cache createCache() { return new RedisCache(); }
}
class PostgreSQLInfrastructureFactory implements InfrastructureFactory {
public Database createDatabase() { return new PostgreSQLDatabase(); }
public Cache createCache() { return new MemcachedCache(); }
}
Отличие от Factory Method
- Factory Method: создает один тип объектов
- Abstract Factory: создает семейство связанных объектов
4. Builder (Строитель)
Суть паттерна
Builder позволяет создавать сложные объекты пошагово, конструируя их из частей.
Когда использовать
- Много параметров конструктора (больше 4-5)
- Необязательные параметры с значениями по умолчанию
- Сложная логика создания объекта
- Неизменяемые объекты (immutable objects)
Ключевые элементы
- Строитель - определяет шаги конструирования
- Конкретный строитель - реализует шаги и хранит результат
- Директор - управляет процессом конструирования (опционально)
- Продукт - создаваемый сложный объект
public class HttpRequest {
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
private final int timeout;
// Приватный конструктор - объект создается только через Builder
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = new HashMap<>(builder.headers);
this.body = builder.body;
this.timeout = builder.timeout;
}
// Вложенный класс Builder
public static class Builder {
private String url;
private String method = "GET";
private Map<String, String> headers = new HashMap<>();
private String body;
private int timeout = 30000;
public Builder url(String url) {
this.url = url;
return this;
}
public Builder method(String method) {
this.method = method;
return this;
}
public Builder header(String key, String value) {
headers.put(key, value);
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeout(int timeout) {
this.timeout = timeout;
return this;
}
public HttpRequest build() {
if (url == null) {
throw new IllegalStateException("URL is required");
}
return new HttpRequest(this);
}
}
}
// Использование
HttpRequest request = new HttpRequest.Builder()
.url("https://api.example.com/users")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"name\":\"John\"}")
.timeout(5000)
.build();
Преимущества Builder
- Читаемость: код самодокументируется
- Гибкость: можно создавать объекты с разными комбинациями параметров
- Валидация: проверки в методе
build()
- Неизменяемость: объект создается полностью сформированным
Инструменты для Builder
Lombok @Builder - автоматически генерирует Builder:
@Builder
@Data
public class User {
private String name;
private String email;
private int age;
private List<String> roles;
}
// Использование
User user = User.builder()
.name("John")
.email("john@example.com")
.age(30)
.roles(Arrays.asList("USER", "ADMIN"))
.build();
5. Prototype (Прототип)
Суть паттерна
Prototype позволяет создавать новые объекты путем клонирования существующих экземпляров.
Когда использовать
- Создание объекта дорого (сложные вычисления, обращения к БД)
- Объекты мало отличаются друг от друга
- Конфигурация через примеры вместо конструирования с нуля
public class DatabaseConnection implements Cloneable {
private String host;
private int port;
private String database;
private Properties settings;
// Дорогая инициализация
private void initializeConnection() {
// Сложная логика подключения
System.out.println("Expensive connection initialization");
}
@Override
public DatabaseConnection clone() {
try {
DatabaseConnection cloned = (DatabaseConnection) super.clone();
// Глубокое клонирование для изменяемых полей
cloned.settings = (Properties) settings.clone();
return cloned;
} catch (CloneNotSupportedException e) {
throw new RuntimeException("Clone not supported", e);
}
}
}
Важные моменты
- Поверхностное vs глубокое клонирование: учитывайте изменяемые поля
- Интерфейс Cloneable: маркерный интерфейс для разрешения клонирования
- Альтернативы: copy constructors, статические методы копирования
Сравнение порождающих паттернов
Паттерн | Что решает | Когда использовать |
---|---|---|
Singleton | Контроль количества экземпляров | Один экземпляр на приложение |
Factory Method | Создание объектов через наследование | Неизвестен точный тип создаваемого объекта |
Abstract Factory | Создание семейств объектов | Нужно создавать связанные объекты |
Builder | Создание сложных объектов пошагово | Много параметров, сложная конструкция |
Prototype | Создание через клонирование | Дорогое создание, похожие объекты |
Практические советы для собеседования
Частые вопросы
-
"Чем отличается Singleton от статического класса?"
- Singleton может реализовывать интерфейсы
- Ленивая инициализация
- Может быть передан как параметр
-
"Когда использовать Builder вместо конструктора?"
- Больше 4-5 параметров
- Много необязательных параметров
- Нужна валидация перед созданием
-
"Проблемы Singleton в многопоточности?"
- Double-checked locking
- Enum-based реализация
- Initialization-on-demand holder
Применение в реальных проектах
- Spring: ApplicationContext как Singleton
- Hibernate: SessionFactory через Builder
- HTTP клиенты: Request/Response builders
- Конфигурация: Properties как Singleton
- Пулы соединений: Database connection pools
Порождающие паттерны в Spring
Как Spring использует порождающие паттерны
Spring Framework активно применяет порождающие паттерны для управления жизненным циклом объектов, их созданием и конфигурированием. Понимание этих паттернов критически важно для Senior разработчика, так как это основа архитектуры Spring.
Основные преимущества использования Spring:
- Inversion of Control (IoC) - Spring берет на себя создание и управление объектами
- Dependency Injection (DI) - автоматическое внедрение зависимостей
- Конфигурация через аннотации или XML
- Управление жизненным циклом объектов
1. Singleton в Spring
Как Spring реализует Singleton
По умолчанию все бины в Spring являются Singleton-ами. Это означает, что Spring создает только один экземпляр каждого бина на весь ApplicationContext.
Scope бинов в Spring
@Component
@Scope("singleton") // По умолчанию
public class UserService {
// Этот объект будет создан только один раз
}
@Component
@Scope("prototype") // Новый экземпляр каждый раз
public class UserRequest {
// Каждый раз создается новый объект
}
@Component
@Scope("request") // Один экземпляр на HTTP запрос
public class RequestProcessor {
// Новый объект для каждого HTTP запроса
}
Ключевые особенности Singleton в Spring
Thread Safety: Spring Singleton бины должны быть потокобезопасными, так как один экземпляр используется всеми потоками.
@Service
public class CounterService {
private int count = 0; // ОПАСНО! Не потокобезопасно
public synchronized void increment() { // Синхронизация
count++;
}
// Лучше использовать AtomicInteger
private final AtomicInteger atomicCount = new AtomicInteger(0);
public void safeIncrement() {
atomicCount.incrementAndGet();
}
}
Ленивая инициализация - бины создаются при первом обращении:
@Component
@Lazy // Создается только при первом использовании
public class ExpensiveService {
public ExpensiveService() {
System.out.println("Дорогая инициализация");
}
}
ApplicationContext как Singleton
ApplicationContext сам является Singleton-ом в рамках JVM. Он управляет всеми бинами и их жизненным циклом:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Application.class, args);
// Получаем один и тот же экземпляр
UserService service1 = context.getBean(UserService.class);
UserService service2 = context.getBean(UserService.class);
System.out.println(service1 == service2); // true
}
}
2. Factory Method в Spring
@Configuration и @Bean как Factory Method
@Configuration классы в Spring работают как фабрики, а @Bean методы - как фабричные методы, создающие экземпляры объектов.
@Configuration
public class DatabaseConfig {
// Фабричный метод для создания DataSource
@Bean
@Primary
public DataSource primaryDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/primary");
dataSource.setUsername("user");
dataSource.setPassword("password");
dataSource.setMaximumPoolSize(20);
return dataSource;
}
// Альтернативный DataSource для отчетов
@Bean("reportsDataSource")
public DataSource reportsDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/reports");
dataSource.setUsername("reports_user");
dataSource.setPassword("reports_pass");
dataSource.setMaximumPoolSize(5);
return dataSource;
}
}
FactoryBean интерфейс
FactoryBean - специальный интерфейс для создания сложных объектов:
@Component
public class ConnectionPoolFactory implements FactoryBean<ConnectionPool> {
@Override
public ConnectionPool getObject() throws Exception {
// Сложная логика создания пула соединений
ConnectionPool pool = new ConnectionPool();
pool.setMinSize(5);
pool.setMaxSize(50);
pool.initialize();
return pool;
}
@Override
public Class<?> getObjectType() {
return ConnectionPool.class;
}
@Override
public boolean isSingleton() {
return true; // Создаем Singleton
}
}
Conditional Factory Methods
Условное создание бинов в зависимости от профиля или наличия других бинов:
@Configuration
public class PaymentConfig {
@Bean
@ConditionalOnProperty(name = "payment.provider", havingValue = "paypal")
public PaymentService paypalPaymentService() {
return new PayPalPaymentService();
}
@Bean
@ConditionalOnProperty(name = "payment.provider", havingValue = "stripe")
public PaymentService stripePaymentService() {
return new StripePaymentService();
}
@Bean
@ConditionalOnMissingBean(PaymentService.class)
public PaymentService defaultPaymentService() {
return new DefaultPaymentService();
}
}
3. Abstract Factory в Spring
Profiles как Abstract Factory
Spring Profiles позволяют создавать различные наборы (семейства) бинов для разных окружений:
@Configuration
@Profile("development")
public class DevelopmentConfig {
@Bean
public EmailService emailService() {
return new MockEmailService(); // Заглушка для разработки
}
@Bean
public PaymentService paymentService() {
return new MockPaymentService(); // Заглушка для тестов
}
}
@Configuration
@Profile("production")
public class ProductionConfig {
@Bean
public EmailService emailService() {
return new SmtpEmailService(); // Реальная отправка email
}
@Bean
public PaymentService paymentService() {
return new StripePaymentService(); // Реальная оплата
}
}
Qualifier для семейств бинов
@Qualifier помогает группировать связанные бины:
@Service
public class NotificationService {
private final EmailService emailService;
private final SmsService smsService;
public NotificationService(
@Qualifier("production") EmailService emailService,
@Qualifier("production") SmsService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
}
@Configuration
public class NotificationConfig {
@Bean
@Qualifier("production")
public EmailService productionEmailService() {
return new SmtpEmailService();
}
@Bean
@Qualifier("production")
public SmsService productionSmsService() {
return new TwilioSmsService();
}
}
Auto-Configuration как Abstract Factory
Spring Boot Auto-Configuration работает как Abstract Factory, создавая наборы бинов в зависимости от classpath:
@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DatabaseProperties.class)
public class DatabaseAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DatabaseProperties properties) {
return DataSourceBuilder.create()
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword())
.build();
}
@Bean
@ConditionalOnBean(DataSource.class)
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
4. Builder в Spring
@ConfigurationProperties как Builder
@ConfigurationProperties работает как Builder для создания объектов конфигурации:
@ConfigurationProperties(prefix = "app.database")
@Data
@Builder
public class DatabaseProperties {
private String url;
private String username;
private String password;
private int maxConnections;
private Duration connectionTimeout;
private boolean ssl;
}
# application.yml
app:
database:
url: jdbc:mysql://localhost:3306/mydb
username: user
password: password
max-connections: 20
connection-timeout: 30s
ssl: true
RestTemplate Builder
RestTemplateBuilder - встроенный Builder в Spring Boot:
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.rootUri("https://api.example.com")
.connectTimeout(Duration.ofSeconds(5))
.readTimeout(Duration.ofSeconds(30))
.defaultHeader("User-Agent", "MyApp/1.0")
.errorHandler(new CustomErrorHandler())
.build();
}
}
WebClient Builder
WebClient.Builder для реактивного HTTP клиента:
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeader("Content-Type", "application/json")
.defaultHeader("Accept", "application/json")
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
.build();
}
}
Spring Data JPA Query Builder
Spring Data JPA использует Builder для построения запросов:
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
// Метод использует внутренний Builder для построения запроса
List<User> findByNameAndAgeGreaterThan(String name, Integer age);
}
// Использование Specification как Builder
public class UserSpecs {
public static Specification<User> hasName(String name) {
return (root, query, cb) -> cb.equal(root.get("name"), name);
}
public static Specification<User> hasAgeGreaterThan(Integer age) {
return (root, query, cb) -> cb.greaterThan(root.get("age"), age);
}
}
// Использование
List<User> users = userRepository.findAll(
Specification.where(UserSpecs.hasName("John"))
.and(UserSpecs.hasAgeGreaterThan(25))
);
5. Prototype в Spring Security
SecurityContext и Authentication
Spring Security использует Prototype для создания контекстов безопасности:
@Component
@Scope("prototype") // Новый экземпляр для каждого запроса
public class SecurityContextHolder {
private Authentication authentication;
public void setAuthentication(Authentication auth) {
this.authentication = auth;
}
}
Request-scoped бины как Prototype
Request-scoped бины создаются заново для каждого HTTP запроса:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String requestId;
private LocalDateTime startTime;
@PostConstruct
public void init() {
this.requestId = UUID.randomUUID().toString();
this.startTime = LocalDateTime.now();
}
}
Интеграция паттернов в Spring Boot
Application Events как Observer + Factory
ApplicationEvent система Spring комбинирует несколько паттернов:
// Событие (создается через Factory)
public class UserRegisteredEvent extends ApplicationEvent {
private final User user;
public UserRegisteredEvent(Object source, User user) {
super(source);
this.user = user;
}
}
// Создатель событий (Factory)
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void registerUser(User user) {
// Логика регистрации
userRepository.save(user);
// Создание и публикация события
UserRegisteredEvent event = new UserRegisteredEvent(this, user);
eventPublisher.publishEvent(event);
}
}
// Обработчик событий (Observer)
@Component
public class EmailNotificationHandler {
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
User user = event.getUser();
emailService.sendWelcomeEmail(user.getEmail());
}
}
Caching как Singleton + Proxy
Spring Cache использует Singleton для кэшей и Proxy для перехвата вызовов:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
);
return manager;
}
}
@Service
public class UserService {
@Cacheable("users") // Proxy перехватывает вызов
public User findById(Long id) {
// Дорогая операция - вызывается только если нет в кэше
return userRepository.findById(id).orElse(null);
}
}
Практические советы для собеседования
Ключевые вопросы и ответы
1. "Чем отличается Singleton в Spring от классического Singleton?"
- Spring Singleton: один экземпляр на ApplicationContext
- Классический Singleton: один экземпляр на JVM
- Spring управляет жизненным циклом
- Возможность настройки через конфигурацию
2. "Как Spring решает проблему циклических зависимостей?"
- Использует Proxy для создания "пустых" объектов
- Трехуровневый кэш бинов
- Field injection vs Constructor injection
3. "Когда использовать @Component vs @Bean?"
- @Component: для ваших классов, автоматическое сканирование
- @Bean: для внешних библиотек, сложная логика создания
Инструменты и аннотации
Создание бинов:
@Component
,@Service
,@Repository
,@Controller
@Configuration
+@Bean
@ComponentScan
для автоматического поиска
Управление жизненным циклом:
@PostConstruct
,@PreDestroy
@Scope
(singleton, prototype, request, session)@Lazy
для ленивой инициализации
Условное создание:
@ConditionalOnProperty
@ConditionalOnClass
@ConditionalOnMissingBean
@Profile
Лучшие практики
1. Prefer Constructor Injection:
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// Конструктор инъекция - обязательные зависимости
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}
2. Use @ConfigurationProperties для сложной конфигурации:
@ConfigurationProperties(prefix = "app.security")
@Validated
public class SecurityProperties {
@NotNull
private String secretKey;
@Min(300)
private int tokenExpirationSeconds;
}
3. Avoid циклических зависимостей:
// Плохо - циклическая зависимость
@Service
public class UserService {
@Autowired
private OrderService orderService; // OrderService тоже зависит от UserService
}
// Хорошо - использование событий
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void updateUser(User user) {
userRepository.save(user);
eventPublisher.publishEvent(new UserUpdatedEvent(user));
}
}
Производительность и мониторинг
Spring Boot Actuator для мониторинга бинов:
management:
endpoints:
web:
exposure:
include: beans, configprops, env
Endpoint /actuator/beans
покажет все созданные бины и их зависимости - полезно для понимания того, как Spring применяет порождающие паттерны в вашем приложении.
Структурные паттерны
Что такое структурные паттерны?
Структурные паттерны решают проблемы композиции классов и объектов. Они показывают, как из простых объектов и классов строить более крупные структуры, сохраняя при этом гибкость и эффективность.
Основные проблемы, которые решают:
- Как совместить несовместимые интерфейсы
- Как добавить функциональность без изменения кода
- Как упростить работу со сложными системами
- Как эффективно управлять большим количеством объектов
Ключевое отличие от других паттернов: структурные паттерны фокусируются на отношениях между объектами, а не на их создании или поведении.
1. Adapter (Адаптер)
Суть паттерна
Adapter позволяет объектам с несовместимыми интерфейсами работать вместе. Это как переходник между различными стандартами - он "переводит" вызовы одного интерфейса в вызовы другого.
Когда использовать
- Legacy код: Нужно интегрировать старую систему с новой
- Внешние библиотеки: API третьих сторон не подходит под ваш интерфейс
- Разные форматы данных: JSON API нужно адаптировать под XML
- Миграция систем: Постепенный переход с одной технологии на другую
Ключевые элементы
- Target - интерфейс, который ожидает клиент
- Adaptee - существующий класс с несовместимым интерфейсом
- Adapter - класс, который связывает Target и Adaptee
- Client - код, который использует Target интерфейс
// Target - то, что ожидает наш код
interface PaymentProcessor {
PaymentResult processPayment(double amount, String currency);
}
// Adaptee - внешняя библиотека с другим интерфейсом
class ThirdPartyPaymentGateway {
public boolean makePayment(int amountInCents, String currencyCode, Map<String, String> metadata) {
System.out.println("Processing " + amountInCents + " cents in " + currencyCode);
return true; // Упрощенная логика
}
}
// Adapter - переводит вызовы
class PaymentGatewayAdapter implements PaymentProcessor {
private final ThirdPartyPaymentGateway gateway;
public PaymentGatewayAdapter(ThirdPartyPaymentGateway gateway) {
this.gateway = gateway;
}
@Override
public PaymentResult processPayment(double amount, String currency) {
// Конвертируем доллары в центы
int amountInCents = (int) (amount * 100);
// Подготавливаем метаданные
Map<String, String> metadata = new HashMap<>();
metadata.put("source", "web-app");
// Вызываем адаптируемый метод
boolean success = gateway.makePayment(amountInCents, currency, metadata);
return new PaymentResult(success, success ? "SUCCESS" : "FAILED");
}
}
// Использование
PaymentProcessor processor = new PaymentGatewayAdapter(new ThirdPartyPaymentGateway());
PaymentResult result = processor.processPayment(99.99, "USD");
Применение в Java
- Collections.list() - адаптирует Enumeration к List
- Arrays.asList() - адаптирует массив к List
- InputStreamReader - адаптирует InputStream к Reader
Практические примеры в backend
// Адаптер для логирования
class Slf4jToLog4jAdapter implements Logger {
private final org.apache.log4j.Logger log4jLogger;
public void info(String message) {
log4jLogger.info(message);
}
}
// Адаптер для кэширования
class RedisCacheAdapter implements CacheManager {
private final JedisPool jedisPool;
public void put(String key, Object value) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.set(key, serialize(value));
}
}
}
2. Decorator (Декоратор)
Суть паттерна
Decorator позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные "обертки". Это альтернатива наследованию для расширения поведения.
Когда использовать
- Добавление функций во время выполнения: Логирование, кэширование, валидация
- Избежание "взрыва классов": Вместо создания множества подклассов
- Цепочка обработчиков: Middleware в web-приложениях
- Прозрачное расширение: Клиент не знает о декораторах
Ключевые элементы
- Component - общий интерфейс для объектов и декораторов
- ConcreteComponent - основной объект, к которому добавляется функциональность
- Decorator - базовый класс для всех декораторов
- ConcreteDecorator - конкретные декораторы с дополнительной функциональностью
// Component - общий интерфейс
interface DataSource {
void writeData(String data);
String readData();
}
// ConcreteComponent - базовая функциональность
class FileDataSource implements DataSource {
private String filename;
private String data = "";
public FileDataSource(String filename) {
this.filename = filename;
}
@Override
public void writeData(String data) {
this.data = data;
System.out.println("Writing to file: " + filename);
}
@Override
public String readData() {
System.out.println("Reading from file: " + filename);
return data;
}
}
// Базовый декоратор
abstract class DataSourceDecorator implements DataSource {
protected DataSource wrappee;
public DataSourceDecorator(DataSource source) {
this.wrappee = source;
}
@Override
public void writeData(String data) {
wrappee.writeData(data);
}
@Override
public String readData() {
return wrappee.readData();
}
}
// Конкретные декораторы
class EncryptionDecorator extends DataSourceDecorator {
public EncryptionDecorator(DataSource source) {
super(source);
}
@Override
public void writeData(String data) {
super.writeData(encrypt(data));
}
@Override
public String readData() {
return decrypt(super.readData());
}
private String encrypt(String data) {
return "encrypted(" + data + ")";
}
private String decrypt(String data) {
return data.replace("encrypted(", "").replace(")", "");
}
}
class CompressionDecorator extends DataSourceDecorator {
public CompressionDecorator(DataSource source) {
super(source);
}
@Override
public void writeData(String data) {
super.writeData(compress(data));
}
@Override
public String readData() {
return decompress(super.readData());
}
private String compress(String data) {
return "compressed(" + data + ")";
}
private String decompress(String data) {
return data.replace("compressed(", "").replace(")", "");
}
}
// Использование - можно комбинировать декораторы
DataSource source = new FileDataSource("data.txt");
source = new CompressionDecorator(source);
source = new EncryptionDecorator(source);
source.writeData("Hello World!"); // Будет и сжато, и зашифровано
Применение в Java
- InputStream/OutputStream: BufferedInputStream, GZIPInputStream
- Collections: Collections.synchronizedList(), Collections.unmodifiableList()
- Servlet Filters: Цепочка фильтров в web-приложениях
Spring примеры
// Декоратор для методов сервиса
@Component
public class CachingServiceDecorator implements UserService {
private final UserService userService;
private final CacheManager cacheManager;
@Override
public User findById(Long id) {
return cacheManager.get(id, () -> userService.findById(id));
}
}
// AOP как декоратор
@Aspect
@Component
public class LoggingAspect {
@Around("@annotation(Loggable)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // Вызов оригинального метода
long executionTime = System.currentTimeMillis() - start;
logger.info("Method {} executed in {} ms", joinPoint.getSignature().getName(), executionTime);
return result;
}
}
3. Facade (Фасад)
Суть паттерна
Facade предоставляет простой интерфейс к сложной подсистеме. Это как пульт управления, который скрывает сложность множества компонентов за простыми кнопками.
Когда использовать
- Сложная подсистема: Много взаимосвязанных классов
- Упрощение API: Клиентам нужен простой интерфейс
- Слой абстракции: Скрыть детали реализации
- Интеграция с legacy системами: Обернуть старый код в современный интерфейс
Ключевые элементы
- Facade - упрощенный интерфейс к подсистеме
- Subsystem classes - множество классов сложной подсистемы
- Client - использует только Facade, не знает о внутренних классах
// Сложная подсистема - множество сервисов
class InventoryService {
public boolean checkAvailability(String productId, int quantity) {
System.out.println("Checking inventory for " + productId);
return true;
}
public void reserveItems(String productId, int quantity) {
System.out.println("Reserving " + quantity + " of " + productId);
}
}
class PaymentService {
public boolean processPayment(String cardNumber, double amount) {
System.out.println("Processing payment: $" + amount);
return true;
}
public void refund(String transactionId) {
System.out.println("Refunding transaction: " + transactionId);
}
}
class ShippingService {
public String scheduleDelivery(String address, List<String> items) {
System.out.println("Scheduling delivery to " + address);
return "TRACK123";
}
}
class NotificationService {
public void sendOrderConfirmation(String email, String orderId) {
System.out.println("Sending confirmation to " + email);
}
public void sendShippingNotification(String email, String trackingId) {
System.out.println("Sending shipping notification with tracking: " + trackingId);
}
}
// Facade - простой интерфейс ко всей подсистеме
class OrderFacade {
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final ShippingService shippingService;
private final NotificationService notificationService;
public OrderFacade() {
this.inventoryService = new InventoryService();
this.paymentService = new PaymentService();
this.shippingService = new ShippingService();
this.notificationService = new NotificationService();
}
// Простой метод, который координирует всю сложность
public OrderResult placeOrder(OrderRequest request) {
try {
// 1. Проверяем наличие товара
if (!inventoryService.checkAvailability(request.getProductId(), request.getQuantity())) {
return OrderResult.failed("Product not available");
}
// 2. Резервируем товар
inventoryService.reserveItems(request.getProductId(), request.getQuantity());
// 3. Обрабатываем платеж
if (!paymentService.processPayment(request.getCardNumber(), request.getAmount())) {
return OrderResult.failed("Payment failed");
}
// 4. Планируем доставку
String trackingId = shippingService.scheduleDelivery(
request.getShippingAddress(),
List.of(request.getProductId())
);
// 5. Отправляем уведомления
String orderId = "ORDER-" + System.currentTimeMillis();
notificationService.sendOrderConfirmation(request.getEmail(), orderId);
notificationService.sendShippingNotification(request.getEmail(), trackingId);
return OrderResult.success(orderId, trackingId);
} catch (Exception e) {
return OrderResult.failed("Order processing failed: " + e.getMessage());
}
}
}
// Клиент использует только простой интерфейс
OrderFacade orderFacade = new OrderFacade();
OrderRequest request = new OrderRequest("PRODUCT-123", 2, "john@email.com", "123 Main St", "4111111111111111", 99.99);
OrderResult result = orderFacade.placeOrder(request);
Применение в реальных проектах
// Spring Boot Actuator как Facade
@RestController
public class HealthFacade {
private final DatabaseHealthIndicator dbHealth;
private final RedisHealthIndicator redisHealth;
private final QueueHealthIndicator queueHealth;
@GetMapping("/health")
public HealthStatus getOverallHealth() {
// Фасад объединяет проверки всех подсистем
return HealthStatus.builder()
.database(dbHealth.check())
.cache(redisHealth.check())
.queue(queueHealth.check())
.build();
}
}
// Service Layer как Facade для Repository слоя
@Service
@Transactional
public class UserManagementFacade {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final AuditService auditService;
private final EmailService emailService;
public void createUserWithRoles(CreateUserRequest request) {
// Фасад координирует работу нескольких компонентов
User user = userRepository.save(new User(request.getName(), request.getEmail()));
request.getRoles().forEach(roleName -> {
Role role = roleRepository.findByName(roleName);
user.addRole(role);
});
auditService.logUserCreation(user);
emailService.sendWelcomeEmail(user.getEmail());
}
}
4. Proxy (Заместитель)
Суть паттерна
Proxy предоставляет объект-заместитель, который контролирует доступ к другому объекту. Proxy может добавлять дополнительную логику: ленивую загрузку, кэширование, контроль доступа, логирование.
Когда использовать
- Ленивая инициализация: Создание дорогих объектов по требованию
- Контроль доступа: Проверка прав перед выполнением операций
- Кэширование: Сохранение результатов дорогих операций
- Логирование: Отслеживание обращений к объекту
- Удаленные объекты: Работа с объектами через сеть
Типы Proxy
- Virtual Proxy - ленивая загрузка
- Protection Proxy - контроль доступа
- Remote Proxy - работа с удаленными объектами
- Caching Proxy - кэширование результатов
// Subject - общий интерфейс для RealSubject и Proxy
interface DatabaseService {
List<User> getAllUsers();
User getUserById(Long id);
}
// RealSubject - реальный объект, который выполняет работу
class DatabaseServiceImpl implements DatabaseService {
@Override
public List<User> getAllUsers() {
// Имитируем дорогую операцию БД
try {
Thread.sleep(2000); // 2 секунды
System.out.println("Loading all users from database...");
return Arrays.asList(
new User(1L, "John"),
new User(2L, "Jane")
);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public User getUserById(Long id) {
try {
Thread.sleep(1000); // 1 секунда
System.out.println("Loading user " + id + " from database...");
return new User(id, "User" + id);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
// Proxy - добавляет кэширование и ленивую загрузку
class CachingDatabaseProxy implements DatabaseService {
private DatabaseService realService;
private Map<String, Object> cache = new ConcurrentHashMap<>();
private static final int CACHE_TTL_SECONDS = 300; // 5 минут
// Ленивая инициализация реального сервиса
private DatabaseService getRealService() {
if (realService == null) {
System.out.println("Initializing real database service...");
realService = new DatabaseServiceImpl();
}
return realService;
}
@Override
public List<User> getAllUsers() {
String cacheKey = "all_users";
// Проверяем кэш
CachedResult<List<User>> cached = (CachedResult<List<User>>) cache.get(cacheKey);
if (cached != null && !cached.isExpired()) {
System.out.println("Returning cached users");
return cached.getData();
}
// Загружаем из БД
List<User> users = getRealService().getAllUsers();
// Сохраняем в кэш
cache.put(cacheKey, new CachedResult<>(users, CACHE_TTL_SECONDS));
return users;
}
@Override
public User getUserById(Long id) {
String cacheKey = "user_" + id;
CachedResult<User> cached = (CachedResult<User>) cache.get(cacheKey);
if (cached != null && !cached.isExpired()) {
System.out.println("Returning cached user " + id);
return cached.getData();
}
User user = getRealService().getUserById(id);
cache.put(cacheKey, new CachedResult<>(user, CACHE_TTL_SECONDS));
return user;
}
}
// Вспомогательный класс для кэширования
class CachedResult<T> {
private final T data;
private final long timestamp;
private final int ttlSeconds;
public CachedResult(T data, int ttlSeconds) {
this.data = data;
this.ttlSeconds = ttlSeconds;
this.timestamp = System.currentTimeMillis();
}
public boolean isExpired() {
return System.currentTimeMillis() - timestamp > ttlSeconds * 1000;
}
public T getData() { return data; }
}
Применение в Java и Spring
// Spring AOP как Dynamic Proxy
@Service
public class UserServiceImpl implements UserService {
@Cacheable("users")
@PreAuthorize("hasRole('ADMIN')")
public User findById(Long id) {
return userRepository.findById(id);
}
}
// JPA Lazy Loading как Virtual Proxy
@Entity
public class Order {
@OneToMany(fetch = FetchType.LAZY) // Proxy для ленивой загрузки
private List<OrderItem> items;
}
// HTTP Client как Remote Proxy
@FeignClient(name = "user-service")
public interface UserServiceProxy {
@GetMapping("/users/{id}")
User getUserById(@PathVariable Long id);
}
5. Composite (Компоновщик)
Суть паттерна
Composite позволяет сгруппировать объекты в древовидную структуру и работать с ней так, как будто это единичный объект. Клиенты одинаково обращаются как к отдельным объектам, так и к их композициям.
Когда использовать
- Иерархические структуры: Файловые системы, организационные структуры
- Рекурсивные операции: Операции должны применяться ко всему дереву
- Единообразная обработка: Листья и ветки обрабатываются одинаково
// Component - общий интерфейс для листьев и композитов
interface FileSystemComponent {
String getName();
long getSize();
void display(String indent);
}
// Leaf - конечный элемент (файл)
class File implements FileSystemComponent {
private String name;
private long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public String getName() { return name; }
@Override
public long getSize() { return size; }
@Override
public void display(String indent) {
System.out.println(indent + "📄 " + name + " (" + size + " bytes)");
}
}
// Composite - составной элемент (папка)
class Directory implements FileSystemComponent {
private String name;
private List<FileSystemComponent> children = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public void add(FileSystemComponent component) {
children.add(component);
}
public void remove(FileSystemComponent component) {
children.remove(component);
}
@Override
public String getName() { return name; }
@Override
public long getSize() {
// Рекурсивно вычисляем размер всех дочерних элементов
return children.stream()
.mapToLong(FileSystemComponent::getSize)
.sum();
}
@Override
public void display(String indent) {
System.out.println(indent + "📁 " + name + "/ (" + getSize() + " bytes total)");
// Рекурсивно отображаем все дочерние элементы
children.forEach(child -> child.display(indent + " "));
}
}
// Использование - одинаково работаем с файлами и папками
Directory root = new Directory("root");
Directory documents = new Directory("documents");
Directory photos = new Directory("photos");
documents.add(new File("report.pdf", 1024));
documents.add(new File("presentation.pptx", 2048));
photos.add(new File("vacation1.jpg", 512));
photos.add(new File("vacation2.jpg", 768));
root.add(documents);
root.add(photos);
root.add(new File("readme.txt", 256));
root.display(""); // Выводит всю иерархию
System.out.println("Total size: " + root.getSize() + " bytes");
Применение в реальных проектах
// Меню приложения
interface MenuComponent {
void execute();
String getTitle();
}
class MenuItem implements MenuComponent {
private String title;
private Runnable action;
public void execute() { action.run(); }
}
class Menu implements MenuComponent {
private String title;
private List<MenuComponent> items = new ArrayList<>();
public void execute() {
items.forEach(MenuComponent::execute);
}
}
// Валидация форм
interface Validator {
ValidationResult validate(Object value);
}
class CompositeValidator implements Validator {
private List<Validator> validators = new ArrayList<>();
public ValidationResult validate(Object value) {
return validators.stream()
.map(v -> v.validate(value))
.reduce(ValidationResult.success(), ValidationResult::combine);
}
}
6. Bridge (Мост)
Суть паттерна
Bridge разделяет абстракцию и её реализацию, позволяя им изменяться независимо. Это полезно, когда у вас есть несколько способов реализации и несколько вариантов использования.
Когда использовать
- Множественные реализации: Разные способы выполнения одной задачи
- Избежание постоянного связывания: Абстракция и реализация должны выбираться во время выполнения
- Расширяемость: Легко добавлять новые абстракции и реализации
// Implementor - интерфейс для конкретных реализаций
interface MessageSender {
void sendMessage(String message, String recipient);
}
// ConcreteImplementor - конкретные реализации
class EmailSender implements MessageSender {
@Override
public void sendMessage(String message, String recipient) {
System.out.println("Email to " + recipient + ": " + message);
}
}
class SmsSender implements MessageSender {
@Override
public void sendMessage(String message, String recipient) {
System.out.println("SMS to " + recipient + ": " + message);
}
}
class SlackSender implements MessageSender {
@Override
public void sendMessage(String message, String recipient) {
System.out.println("Slack to " + recipient + ": " + message);
}
}
// Abstraction - базовый класс для различных типов уведомлений
abstract class Notification {
protected MessageSender messageSender;
public Notification(MessageSender messageSender) {
this.messageSender = messageSender;
}
public abstract void notify(String message);
}
// Refined Abstraction - конкретные типы уведомлений
class SimpleNotification extends Notification {
private String recipient;
public SimpleNotification(String recipient, MessageSender messageSender) {
super(messageSender);
this.recipient = recipient;
}
@Override
public void notify(String message) {
messageSender.sendMessage(message, recipient);
}
}
class UrgentNotification extends Notification {
private String recipient;
public UrgentNotification(String recipient, MessageSender messageSender) {
super(messageSender);
this.recipient = recipient;
}
@Override
public void notify(String message) {
messageSender.sendMessage("URGENT: " + message, recipient);
messageSender.sendMessage("This is a follow-up for urgent message", recipient);
}
}
// Использование - можем комбинировать любые абстракции с любыми реализациями
Notification emailNotification = new SimpleNotification("john@example.com", new EmailSender());
Notification urgentSms = new UrgentNotification("+1234567890", new SmsSender());
Notification slackAlert = new UrgentNotification("@john", new SlackSender());
emailNotification.notify("Your order has been shipped");
urgentSms.notify("Server is down");
slackAlert.notify("Deploy completed");
Сравнение структурных паттернов
Паттерн | Назначение | Ключевая особенность |
---|---|---|
Adapter | Совместимость интерфейсов | Конвертирует один интерфейс в другой |
Decorator | Добавление функций | Расширяет поведение без наследования |
Facade | Упрощение интерфейса | Скрывает сложность подсистемы |
Proxy | Контроль доступа | Управляет доступом к объекту |
Composite | Древовидные структуры | Единообразная работа с частью и целым |
Bridge | Разделение абстракции | Независимое изменение абстракции и реализации |
Практические советы для собеседования
Частые вопросы
1. "Чем отличается Adapter от Facade?"
- Adapter: Делает совместимыми два интерфейса
- Facade: Упрощает сложный интерфейс
2. "Когда использовать Decorator вместо наследования?"
- Когда нужно добавлять поведение во время выполнения
- Когда комбинации поведений слишком много для подклассов
- Когда хотите избежать "взрыва классов"
3. "Чем отличается Proxy от Decorator?"
- Proxy: Контролирует доступ к объекту (тот же интерфейс)
- Decorator: Расширяет функциональность объекта
Структурные паттерны в Spring
Как Spring использует структурные паттерны
Spring Framework широко применяет структурные паттерны для создания гибкой, расширяемой архитектуры. Понимание этих паттернов критически важно для Senior разработчика, так как они лежат в основе многих ключевых механизмов Spring.
Основные преимущества Spring подхода:
- Прозрачность: Паттерны работают "за кулисами", не усложняя код
- Конфигурируемость: Поведение настраивается через аннотации и конфигурацию
- Интеграция: Паттерны работают совместно, усиливая друг друга
- AOP интеграция: Aspect-Oriented Programming как основа многих паттернов
1. Proxy в Spring
Spring AOP как основа Proxy паттерна
Spring AOP (Aspect-Oriented Programming) - это фундаментальный механизм Spring, который реализует Proxy паттерн для добавления cross-cutting concerns (сквозной функциональности) к бизнес-логике.
Как работает Spring AOP Proxy
Spring создает прокси-объекты двумя способами:
- JDK Dynamic Proxy - для интерфейсов
- CGLIB Proxy - для классов
Ключевая особенность: Клиентский код не знает, что работает с прокси, а не с оригинальным объектом.
@Service
@Transactional // Spring создаст прокси для управления транзакциями
public class UserService {
@Autowired
private UserRepository userRepository;
@Cacheable("users") // Еще один прокси для кэширования
@PreAuthorize("hasRole('ADMIN')") // Прокси для безопасности
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Retryable(maxAttempts = 3) // Прокси для retry логики
public void updateUser(User user) {
userRepository.save(user);
}
}
Детальное объяснение работы
Когда Spring видит аннотации вроде @Transactional
, он:
- Создает прокси-класс, который имплементирует тот же интерфейс
- Перехватывает вызовы методов в прокси
- Выполняет дополнительную логику (открытие транзакции)
- Вызывает оригинальный метод
- Выполняет завершающую логику (коммит/роллбек транзакции)
Практический пример создания кастомного аспекта
@Aspect
@Component
public class PerformanceMonitoringAspect {
private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitoringAspect.class);
// Pointcut - определяет, где применяется аспект
@Around("@annotation(MonitorPerformance)")
public Object monitorExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
try {
// Вызов оригинального метода
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime;
logger.info("Method {} executed in {} ms", methodName, executionTime);
return result;
} catch (Exception e) {
logger.error("Method {} failed after {} ms", methodName, System.currentTimeMillis() - startTime);
throw e;
}
}
}
// Кастомная аннотация
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MonitorPerformance {
}
// Использование
@Service
public class OrderService {
@MonitorPerformance // Spring создаст прокси для мониторинга
public Order processOrder(OrderRequest request) {
// Бизнес-логика
return new Order();
}
}
Spring Boot Actuator как Proxy
Spring Boot Actuator использует прокси для мониторинга приложения:
@RestController
public class HealthController {
@Autowired
private HealthIndicator dbHealthIndicator;
@GetMapping("/actuator/health")
public ResponseEntity<?> health() {
// Actuator создает прокси для каждого HealthIndicator
// и агрегирует результаты
return ResponseEntity.ok(healthService.getOverallHealth());
}
}
2. Decorator в Spring
Spring Security как Decorator
Spring Security широко использует Decorator для добавления слоев безопасности к существующим компонентам без изменения их кода.
Filter Chain как цепочка декораторов
SecurityFilterChain - это классический пример Decorator паттерна:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// Каждый метод добавляет новый декоратор
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(csrf -> csrf.disable())
.build();
}
}
Объяснение работы Security Filters
Каждый фильтр - это декоратор, который:
- Получает запрос от предыдущего фильтра
- Выполняет свою специфическую логику (аутентификация, авторизация, CSRF защита)
- Передает запрос дальше по цепочке или прерывает выполнение
Method Security как Decorator
@Service
public class BankingService {
// Декоратор для проверки прав доступа
@PreAuthorize("hasRole('ADMIN') or #accountId == authentication.principal.accountId")
public Account getAccount(Long accountId) {
return accountRepository.findById(accountId);
}
// Декоратор для post-обработки результата
@PostAuthorize("returnObject.owner == authentication.principal.username")
public Account findAccountByNumber(String accountNumber) {
return accountRepository.findByNumber(accountNumber);
}
// Декоратор для фильтрации коллекций
@PostFilter("filterObject.isPublic() or filterObject.owner == authentication.principal.username")
public List<Document> getAllDocuments() {
return documentRepository.findAll();
}
}
Spring Cache как Decorator
Spring Cache добавляет кэширование к методам прозрачно:
@Service
@CacheConfig(cacheNames = "users")
public class UserService {
@Cacheable(key = "#id") // Декоратор добавляет кэширование
public User findById(Long id) {
System.out.println("Loading user from database: " + id);
return userRepository.findById(id).orElse(null);
}
@CachePut(key = "#user.id") // Декоратор обновляет кэш
public User updateUser(User user) {
return userRepository.save(user);
}
@CacheEvict(key = "#id") // Декоратор удаляет из кэша
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
@CacheEvict(allEntries = true) // Очищает весь кэш
public void clearCache() {
// Метод может быть пустым
}
}
HTTP Client Interceptors как Decorator
RestTemplate и WebClient используют interceptors как декораторы:
@Configuration
public class HttpClientConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
// Добавляем декораторы (interceptors)
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
// Декоратор для логирования
interceptors.add(new LoggingInterceptor());
// Декоратор для аутентификации
interceptors.add(new AuthenticationInterceptor());
// Декоратор для retry логики
interceptors.add(new RetryInterceptor());
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
// Пример декоратора для логирования
public class LoggingInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(
HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// Логика до выполнения запроса
logger.info("Request: {} {}", request.getMethod(), request.getURI());
// Вызов следующего декоратора/оригинального запроса
ClientHttpResponse response = execution.execute(request, body);
// Логика после выполнения запроса
logger.info("Response: {}", response.getStatusCode());
return response;
}
}
3. Adapter в Spring
Spring Data JPA как Adapter
Spring Data JPA служит адаптером между объектно-ориентированным Java кодом и реляционными базами данных:
// Ваш доменный интерфейс
public interface UserService {
List<User> findActiveUsers();
User findByEmail(String email);
}
// Spring Data создает адаптер автоматически
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Spring переводит имя метода в SQL запрос
List<User> findByActiveTrue();
// Кастомный запрос через аннотацию
@Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true")
Optional<User> findActiveUserByEmail(@Param("email") String email);
// Native SQL запрос
@Query(value = "SELECT * FROM users WHERE created_date > :date", nativeQuery = true)
List<User> findUsersCreatedAfter(@Param("date") LocalDate date);
}
// Реализация сервиса использует адаптер
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository; // Это адаптер к БД
@Override
public List<User> findActiveUsers() {
return userRepository.findByActiveTrue();
}
@Override
public User findByEmail(String email) {
return userRepository.findActiveUserByEmail(email).orElse(null);
}
}
Message Converters как Adapter
Spring MVC использует адаптеры для конвертации данных между различными форматами:
@RestController
public class UserController {
// Spring автоматически использует адаптеры для конвертации
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
// MappingJackson2HttpMessageConverter адаптирует JSON в объект
User user = userService.createUser(request);
// Тот же конвертер адаптирует объект обратно в JSON
return ResponseEntity.ok(user);
}
@GetMapping("/users/export")
public ResponseEntity<byte[]> exportUsers() {
List<User> users = userService.getAllUsers();
// Кастомный адаптер для конвертации в CSV
byte[] csvData = csvConverter.convertToCSV(users);
return ResponseEntity.ok()
.header("Content-Type", "text/csv")
.header("Content-Disposition", "attachment; filename=users.csv")
.body(csvData);
}
}
// Кастомный адаптер для CSV
@Component
public class CsvMessageConverter implements HttpMessageConverter<Object> {
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return MediaType.parseMediaType("text/csv").isCompatibleWith(mediaType);
}
@Override
public void write(Object object, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException {
// Адаптируем объект в CSV формат
String csv = convertObjectToCsv(object);
outputMessage.getBody().write(csv.getBytes());
}
}
Property Sources как Adapter
Spring Boot использует адаптеры для работы с различными источниками конфигурации:
@Configuration
@PropertySource("classpath:custom.properties")
public class DatabaseConfig {
// Environment адаптирует различные источники конфигурации
@Autowired
private Environment env;
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
// Адаптер ищет свойства в различных источниках:
// 1. System properties
// 2. Environment variables
// 3. application.yml/properties
// 4. @PropertySource файлы
dataSource.setJdbcUrl(env.getProperty("database.url"));
dataSource.setUsername(env.getProperty("database.username"));
dataSource.setPassword(env.getProperty("database.password"));
return dataSource;
}
}
// Альтернативный подход с @ConfigurationProperties
@ConfigurationProperties(prefix = "database")
@Data
public class DatabaseProperties {
private String url;
private String username;
private String password;
private int maxPoolSize = 10;
}
Integration Adapters
Spring Integration предоставляет множество адаптеров для интеграции с внешними системами:
@Configuration
@EnableIntegration
public class IntegrationConfig {
// Адаптер для работы с файлами
@Bean
public MessageSource<File> fileAdapter() {
FileReadingMessageSource source = new FileReadingMessageSource();
source.setDirectory(new File("/input"));
source.setFilter(new SimplePatternFileListFilter("*.csv"));
return source;
}
// Адаптер для отправки email
@Bean
public MessageHandler emailAdapter() {
MailSendingMessageHandler handler = new MailSendingMessageHandler(mailSender);
handler.setRequiresReply(false);
return handler;
}
// Адаптер для работы с JMS
@Bean
public MessageHandler jmsAdapter() {
JmsSendingMessageHandler handler = new JmsSendingMessageHandler(jmsTemplate);
handler.setDestinationName("orders.queue");
return handler;
}
}
4. Facade в Spring
Spring Boot Auto-Configuration как Facade
Spring Boot Auto-Configuration - это мощный фасад, который скрывает сложность конфигурирования многих компонентов:
// Пользователь видит только простой код
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// За кулисами Spring Boot Auto-Configuration работает как фасад
@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
// Фасад скрывает сложность создания DataSource
return DataSourceBuilder.create()
.driverClassName(properties.getDriverClassName())
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword())
.build();
}
@Bean
@ConditionalOnBean(DataSource.class)
@ConditionalOnMissingBean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
@ConditionalOnProperty(name = "spring.jpa.enabled", havingValue = "true", matchIfMissing = true)
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
// Сложная конфигурация JPA скрыта за фасадом
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setDataSource(dataSource);
factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
return factory;
}
}
Service Layer как Facade
Service слой в Spring приложениях часто работает как фасад к сложной бизнес-логике:
@Service
@Transactional
public class OrderProcessingFacade {
// Фасад координирует работу множества компонентов
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final ShippingService shippingService;
private final NotificationService notificationService;
private final AuditService auditService;
// Простой метод скрывает сложную бизнес-логику
public OrderResult processOrder(OrderRequest request) {
try {
// 1. Валидация заказа
validateOrder(request);
// 2. Проверка наличия товара
if (!inventoryService.reserveItems(request.getItems())) {
return OrderResult.failed("Items not available");
}
// 3. Обработка платежа
PaymentResult payment = paymentService.processPayment(request.getPayment());
if (!payment.isSuccessful()) {
inventoryService.releaseItems(request.getItems());
return OrderResult.failed("Payment failed");
}
// 4. Создание заказа
Order order = orderRepository.save(new Order(request));
// 5. Планирование доставки
ShippingInfo shipping = shippingService.scheduleDelivery(order);
// 6. Отправка уведомлений
notificationService.sendOrderConfirmation(order);
// 7. Аудит
auditService.logOrderCreated(order);
return OrderResult.success(order.getId(), shipping.getTrackingNumber());
} catch (Exception e) {
// Фасад обрабатывает все исключения
auditService.logOrderError(request, e);
return OrderResult.failed("Order processing failed");
}
}
// Вспомогательные методы скрыты от клиента
private void validateOrder(OrderRequest request) {
if (request.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must contain items");
}
// Дополнительная валидация...
}
}
Spring MVC как Facade
DispatcherServlet работает как фасад ко всей MVC архитектуре:
// Простой контроллер
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(userMapper.toDto(user));
}
}
// За кулисами DispatcherServlet координирует:
// 1. HandlerMapping - поиск подходящего контроллера
// 2. HandlerAdapter - адаптация вызова метода
// 3. ViewResolver - разрешение представлений
// 4. MessageConverter - конвертация данных
// 5. ExceptionHandler - обработка исключений
5. Composite в Spring
Spring Expression Language (SpEL) как Composite
SpEL использует Composite для построения сложных выражений из простых компонентов:
@Service
public class SecurityService {
// Простые выражения можно комбинировать в сложные
@PreAuthorize("hasRole('ADMIN') and #user.department == authentication.principal.department")
public void updateUser(User user) {
userRepository.save(user);
}
@PreAuthorize("hasRole('MANAGER') or (#user.createdBy == authentication.principal.id and hasRole('USER'))")
public void deleteUser(User user) {
userRepository.delete(user);
}
// Сложные выражения с методами
@PreAuthorize("@securityService.canAccessDocument(#documentId, authentication.principal.id)")
public Document getDocument(Long documentId) {
return documentRepository.findById(documentId);
}
public boolean canAccessDocument(Long documentId, Long userId) {
// Сложная логика проверки доступа
Document doc = documentRepository.findById(documentId);
User user = userRepository.findById(userId);
return doc.isPublic() ||
doc.getOwner().equals(user) ||
user.hasRole("ADMIN") ||
doc.getSharedUsers().contains(user);
}
}
Validation как Composite
Bean Validation в Spring использует Composite для объединения валидаторов:
@Entity
public class User {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
private String email;
@NotNull(message = "Age is required")
@Min(value = 18, message = "Age should be at least 18")
@Max(value = 120, message = "Age should be less than 120")
private Integer age;
// Композитная валидация
@Valid
@NotNull
private Address address;
// Кастомная композитная валидация
@ValidUserProfile
private String profileData;
}
// Кастомный композитный валидатор
@Constraint(validatedBy = UserProfileValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidUserProfile {
String message() default "Invalid user profile";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class UserProfileValidator implements ConstraintValidator<ValidUserProfile, String> {
// Композитный валидатор может включать множество проверок
private List<ProfileRule> rules = Arrays.asList(
new LengthRule(),
new ContentRule(),
new FormatRule()
);
@Override
public boolean isValid(String profile, ConstraintValidatorContext context) {
return rules.stream().allMatch(rule -> rule.validate(profile));
}
}
Spring Boot Configuration Properties как Composite
@ConfigurationProperties может создавать композитные структуры конфигурации:
@ConfigurationProperties(prefix = "app")
@Data
public class ApplicationProperties {
// Композитные свойства
private Database database = new Database();
private Security security = new Security();
private Monitoring monitoring = new Monitoring();
@Data
public static class Database {
private String url;
private String username;
private String password;
private Pool pool = new Pool();
@Data
public static class Pool {
private int minSize = 5;
private int maxSize = 20;
private Duration maxWait = Duration.ofSeconds(30);
}
}
@Data
public static class Security {
private Jwt jwt = new Jwt();
private OAuth2 oauth2 = new OAuth2();
@Data
public static class Jwt {
private String secret;
private Duration expiration = Duration.ofHours(24);
}
@Data
public static class OAuth2 {
private String clientId;
private String clientSecret;
private List<String> scopes = new ArrayList<>();
}
}
@Data
public static class Monitoring {
private boolean enabled = true;
private Metrics metrics = new Metrics();
private Tracing tracing = new Tracing();
@Data
public static class Metrics {
private boolean enabled = true;
private String endpoint = "/metrics";
}
@Data
public static class Tracing {
private boolean enabled = false;
private double samplingRate = 0.1;
}
}
}
// Использование композитной конфигурации
@Configuration
@EnableConfigurationProperties(ApplicationProperties.class)
public class AppConfig {
@Bean
public DataSource dataSource(ApplicationProperties props) {
Database dbProps = props.getDatabase();
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(dbProps.getUrl());
dataSource.setUsername(dbProps.getUsername());
dataSource.setPassword(dbProps.getPassword());
dataSource.setMinimumIdle(dbProps.getPool().getMinSize());
dataSource.setMaximumPoolSize(dbProps.getPool().getMaxSize());
return dataSource;
}
}
Интеграция структурных паттернов в Spring
Spring Cloud как комбинация паттернов
Spring Cloud объединяет множество структурных паттернов для создания микросервисной архитектуры:
// Service Discovery как Proxy + Adapter
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
@Bean
@LoadBalanced // Proxy для load balancing
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
// Circuit Breaker как Proxy + Decorator
@Service
public class ExternalApiService {
@Autowired
@LoadBalanced
private RestTemplate restTemplate;
@CircuitBreaker(name = "external-api", fallbackMethod = "fallbackResponse")
@TimeLimiter(name = "external-api")
@Retry(name = "external-api")
public CompletableFuture<String> callExternalApi() {
// Resilience4j создает несколько прокси:
// 1. Circuit Breaker Proxy
// 2. Time Limiter Proxy
// 3. Retry Proxy
return CompletableFuture.supplyAsync(() ->
restTemplate.getForObject("http://external-service/api/data", String.class)
);
}
public CompletableFuture<String> fallbackResponse(Exception ex) {
return CompletableFuture.completedFuture("Fallback response");
}
}
// API Gateway как Facade + Proxy
@RestController
public class ApiGatewayController {
@Autowired
private UserServiceClient userService;
@Autowired
private OrderServiceClient orderService;
// Фасад объединяет несколько микросервисов
@GetMapping("/api/user-profile/{userId}")
public UserProfile getUserProfile(@PathVariable Long userId) {
User user = userService.getUser(userId);
List<Order> orders = orderService.getUserOrders(userId);
return UserProfile.builder()
.user(user)
.recentOrders(orders.stream().limit(5).collect(toList()))
.build();
}
}
Spring Data как Multi-Pattern Framework
Spring Data комбинирует несколько структурных паттернов:
// Repository как Proxy + Adapter
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
// Spring Data создает прокси, который адаптирует методы к SQL
List<User> findByActiveTrue();
@Query("SELECT u FROM User u WHERE u.department.name = :departmentName")
List<User> findByDepartment(@Param("departmentName") String departmentName);
}
// Specification как Composite
public class UserSpecifications {
public static Specification<User> hasName(String name) {
return (root, query, cb) -> cb.equal(root.get("name"), name);
}
public static Specification<User> isActive() {
return (root, query, cb) -> cb.isTrue(root.get("active"));
}
public static Specification<User> inDepartment(String department) {
return (root, query, cb) -> cb.equal(root.join("department").get("name"), department);
}
}
// Композиция спецификаций
@Service
public class UserSearchService {
@Autowired
private UserRepository userRepository;
public List<User> searchUsers(UserSearchCriteria criteria) {
Specification<User> spec = Specification.where(null);
if (criteria.getName() != null) {
spec = spec.and(UserSpecifications.hasName(criteria.getName()));
}
if (criteria.isActiveOnly()) {
spec = spec.and(UserSpecifications.isActive());
}
if (criteria.getDepartment() != null) {
spec = spec.and(UserSpecifications.inDepartment(criteria.getDepartment()));
}
return userRepository.findAll(spec);
}
}
Bridge паттерн в Spring
Spring Profiles как Bridge
Spring Profiles реализуют Bridge паттерн, разделяя абстракцию (бизнес-логика) от реализации (конфигурация окружения):
// Абстракция - интерфейс сервиса
public interface EmailService {
void sendEmail(String to, String subject, String body);
}
// Реализация для разработки
@Service
@Profile("development")
public class MockEmailService implements EmailService {
@Override
public void sendEmail(String to, String subject, String body) {
System.out.println("MOCK EMAIL - To: " + to + ", Subject: " + subject);
}
}
// Реализация для продакшена
@Service
@Profile("production")
public class SmtpEmailService implements EmailService {
@Value("${smtp.host}")
private String smtpHost;
@Value("${smtp.port}")
private int smtpPort;
@Override
public void sendEmail(String to, String subject, String body) {
// Реальная отправка через SMTP
System.out.println("Sending real email via " + smtpHost + ":" + smtpPort);
}
}
// Бизнес-логика не зависит от конкретной реализации
@Service
public class UserRegistrationService {
private final EmailService emailService; // Абстракция
public UserRegistrationService(EmailService emailService) {
this.emailService = emailService;
}
public void registerUser(User user) {
userRepository.save(user);
// Используем абстракцию, не зная о конкретной реализации
emailService.sendEmail(
user.getEmail(),
"Welcome!",
"Thank you for registering"
);
}
}
Database Abstraction как Bridge
Spring Data JPA использует Bridge для разделения бизнес-логики от конкретной СУБД:
// Абстракция - бизнес-логика
@Service
public class ReportService {
private final ReportRepository reportRepository;
public List<SalesReport> generateSalesReport(LocalDate from, LocalDate to) {
// Бизнес-логика не зависит от типа БД
return reportRepository.findSalesBetweenDates(from, to);
}
}
// Мост - Spring Data JPA
@Repository
public interface ReportRepository extends JpaRepository<SalesReport, Long> {
@Query("SELECT new com.example.SalesReport(DATE(s.createdAt), SUM(s.amount)) " +
"FROM Sale s WHERE s.createdAt BETWEEN :from AND :to GROUP BY DATE(s.createdAt)")
List<SalesReport> findSalesBetweenDates(@Param("from") LocalDate from, @Param("to") LocalDate to);
}
# Реализация может меняться через конфигурацию
# application-mysql.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/myapp
# application-postgresql.yml
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/myapp
Практические советы для собеседования
Ключевые вопросы и ответы
1. "Как Spring AOP реализует Proxy паттерн?"
- JDK Dynamic Proxy для интерфейсов
- CGLIB для классов
- Interceptors для добавления cross-cutting concerns
- @EnableAspectJAutoProxy для активации
2. "Чем отличается Spring Security Filter от обычного Decorator?"
- Chain of Responsibility + Decorator
- Каждый фильтр может прервать цепочку
- Порядок выполнения критически важен
- SecurityContext передается по цепочке
3. "Как Spring Boot Auto-Configuration работает как Facade?"
- Скрывает сложность конфигурирования
- @ConditionalOn* аннотации для условного создания бинов
- spring.factories для автоматической загрузки
- Starter modules как готовые фасады
Архитектурные паттерны в Spring
Layered Architecture:
@Controller // Presentation Layer
@Service // Business Layer
@Repository // Data Access Layer
@Configuration // Infrastructure Layer
Hexagonal Architecture с Spring:
// Порты (интерфейсы)
public interface UserRepository {
User save(User user);
}
// Адаптеры (реализации)
@Repository
public class JpaUserRepository implements UserRepository {
// Реализация через JPA
}
@Repository
@Profile("test")
public class InMemoryUserRepository implements UserRepository {
// Реализация в памяти для тестов
}
Performance и Monitoring
Spring Boot Actuator endpoints:
management:
endpoints:
web:
exposure:
include: health,info,metrics,beans,configprops
endpoint:
health:
show-details: always
Полезные endpoints для понимания паттернов:
/actuator/beans
- показывает все созданные прокси/actuator/configprops
- композитные конфигурации/actuator/mappings
- фасады контроллеров/actuator/metrics
- декораторы для мониторинга
Лучшие практики
1. Избегайте self-invocation в AOP:
@Service
public class UserService {
@Transactional
public void updateUser(User user) {
userRepository.save(user);
// Это НЕ СРАБОТАЕТ - прокси не перехватит self-call
this.sendNotification(user);
}
@Async
public void sendNotification(User user) {
emailService.send(user.getEmail(), "Updated");
}
}
// Правильное решение
@Service
public class UserService {
@Autowired
private UserService self; // Внедряем прокси
@Transactional
public void updateUser(User user) {
userRepository.save(user);
self.sendNotification(user); // Используем прокси
}
}
2. Правильная конфигурация кэширования:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats() // Для мониторинга
);
return manager;
}
}
3. Эффективное использование Profiles:
@Configuration
public class DatabaseConfig {
@Bean
@Profile("!test")
public DataSource productionDataSource() {
return new HikariDataSource();
}
@Bean
@Profile("test")
public DataSource testDataSource() {
return new H2DataSource();
}
}
Структурные паттерны в Spring Framework образуют основу гибкой, расширяемой архитектуры. Понимание их работы критически важно для Senior разработчика, так как они влияют на производительность, тестируемость и поддерживаемость приложения.
Поведенческие паттерны
Что такое поведенческие паттерны?
Поведенческие паттерны — это паттерны проектирования, которые определяют алгоритмы взаимодействия между объектами и распределение обязанностей между ними. Они помогают организовать коммуникацию между классами и объектами, делая систему более гибкой и расширяемой.
1. Observer (Наблюдатель)
Описание
Определяет зависимость "один ко многим" между объектами. При изменении состояния одного объекта все зависящие от него объекты уведомляются и обновляются автоматически.
Когда использовать
- Когда изменение одного объекта требует изменения других
- Когда количество наблюдателей может изменяться во время выполнения
- Когда объекты должны быть слабо связаны
Пример (Java)
// Интерфейс наблюдателя
interface Observer {
void update(String message);
}
// Конкретный наблюдатель
class User implements Observer {
private String name;
public User(String name) { this.name = name; }
@Override
public void update(String message) {
System.out.println(name + " получил: " + message);
}
}
// Издатель (Subject)
class NewsAgency {
private List<Observer> observers = new ArrayList<>();
private String news;
public void subscribe(Observer observer) {
observers.add(observer);
}
public void unsubscribe(Observer observer) {
observers.remove(observer);
}
public void setNews(String news) {
this.news = news;
notifyObservers();
}
private void notifyObservers() {
observers.forEach(observer -> observer.update(news));
}
}
Применение в Java
- java.util.Observer (deprecated в Java 9)
- Event Listeners в GUI
- Spring Events (@EventListener)
- Reactive Streams (RxJava, Project Reactor)
2. Strategy (Стратегия)
Описание
Определяет семейство алгоритмов, инкапсулирует каждый и делает их взаимозаменяемыми. Стратегия позволяет изменять алгоритм независимо от клиентов, которые его используют.
Когда использовать
- Когда есть множество способов выполнения задачи
- Когда нужно избежать условных операторов при выборе алгоритма
- Когда алгоритмы должны быть взаимозаменяемыми
Пример (Java)
// Интерфейс стратегии
interface PaymentStrategy {
void pay(int amount);
}
// Конкретные стратегии
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println("Оплата " + amount + " картой " + cardNumber);
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println("Оплата " + amount + " через PayPal " + email);
}
}
// Контекст
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
Применение в Java
- Comparator в Collections
- ThreadPoolExecutor с различными политиками
- Spring Security (AuthenticationProvider)
3. Command (Команда)
Описание
Инкапсулирует запрос как объект, позволяя параметризовать клиентов с различными запросами, ставить запросы в очередь и поддерживать отмену операций.
Когда использовать
- Когда нужно параметризовать объекты действиями
- Когда нужно ставить запросы в очередь или логировать их
- Когда нужна поддержка отмены операций
Пример (Java)
// Интерфейс команды
interface Command {
void execute();
void undo();
}
// Получатель команды
class Light {
private boolean isOn = false;
public void turnOn() {
isOn = true;
System.out.println("Свет включен");
}
public void turnOff() {
isOn = false;
System.out.println("Свет выключен");
}
}
// Конкретная команда
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
@Override
public void undo() {
light.turnOff();
}
}
// Инициатор
class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
public void pressUndo() {
command.undo();
}
}
Применение в Java
- Runnable и Callable интерфейсы
- ActionListener в Swing
- TransactionTemplate в Spring
- Queue для асинхронной обработки
4. Template Method (Шаблонный метод)
Описание
Определяет скелет алгоритма в базовом классе, позволяя подклассам переопределять отдельные шаги алгоритма, не изменяя его структуру.
Когда использовать
- Когда есть общий алгоритм с вариациями в отдельных шагах
- Когда нужно контролировать расширение подклассов
- Когда нужно избежать дублирования кода
Пример (Java)
// Абстрактный класс с шаблонным методом
abstract class DataProcessor {
// Шаблонный метод
public final void process() {
readData();
processData();
writeData();
}
// Конкретный метод
private void readData() {
System.out.println("Чтение данных...");
}
// Абстрактный метод - должен быть реализован в подклассах
protected abstract void processData();
// Хук-метод - может быть переопределен
protected void writeData() {
System.out.println("Запись данных...");
}
}
// Конкретная реализация
class CSVProcessor extends DataProcessor {
@Override
protected void processData() {
System.out.println("Обработка CSV данных");
}
}
class XMLProcessor extends DataProcessor {
@Override
protected void processData() {
System.out.println("Обработка XML данных");
}
@Override
protected void writeData() {
System.out.println("Запись XML данных в специальном формате");
}
}
Применение в Java
- AbstractList, AbstractSet в Collections
- HttpServlet (service(), doGet(), doPost())
- Spring шаблоны (JdbcTemplate, RestTemplate)
5. State (Состояние)
Описание
Позволяет объекту изменять свое поведение при изменении внутреннего состояния. Создается впечатление, что объект изменил свой класс.
Когда использовать
- Когда поведение объекта зависит от его состояния
- Когда есть множество условных операторов, зависящих от состояния
- Когда состояния имеют сложную логику переходов
Пример (Java)
// Интерфейс состояния
interface State {
void handle(Context context);
}
// Конкретные состояния
class StartState implements State {
@Override
public void handle(Context context) {
System.out.println("Игрок в состоянии START");
context.setState(new PlayingState());
}
}
class PlayingState implements State {
@Override
public void handle(Context context) {
System.out.println("Игрок играет");
context.setState(new EndState());
}
}
class EndState implements State {
@Override
public void handle(Context context) {
System.out.println("Игра окончена");
context.setState(new StartState());
}
}
// Контекст
class Context {
private State state;
public Context() {
state = new StartState();
}
public void setState(State state) {
this.state = state;
}
public void request() {
state.handle(this);
}
}
Применение в Java
- TCP Connection states
- Thread states в Java
- Spring State Machine
- Workflow engines
6. Chain of Responsibility (Цепочка обязанностей)
Описание
Позволяет передавать запросы последовательно по цепочке обработчиков. Каждый обработчик решает, может ли он обработать запрос и стоит ли передавать его дальше.
Когда использовать
- Когда система должна обрабатывать разнообразные запросы различными способами
- Когда набор обработчиков и их порядок должны определяться динамически
- Когда нужно разделить отправителя запроса и получателя
Пример (Java)
// Абстрактный обработчик
abstract class Handler {
protected Handler nextHandler;
public void setNext(Handler handler) {
this.nextHandler = handler;
}
public abstract void handleRequest(Request request);
}
// Запрос
class Request {
private String type;
private String content;
public Request(String type, String content) {
this.type = type;
this.content = content;
}
public String getType() { return type; }
public String getContent() { return content; }
}
// Конкретные обработчики
class AuthHandler extends Handler {
@Override
public void handleRequest(Request request) {
if (request.getType().equals("AUTH")) {
System.out.println("Обработка аутентификации: " + request.getContent());
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
class ValidationHandler extends Handler {
@Override
public void handleRequest(Request request) {
if (request.getType().equals("VALIDATION")) {
System.out.println("Валидация: " + request.getContent());
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
Применение в Java
- Servlet Filters в web-приложениях
- Exception handling в некоторых фреймворках
- Logging frameworks (Logback, Log4j)
- Spring Security filter chain
7. Mediator (Посредник)
Описание
Определяет объект, который инкапсулирует взаимодействие множества объектов. Посредник обеспечивает слабую связанность, избавляя объекты от необходимости ссылаться друг на друга.
Когда использовать
- Когда множество объектов взаимодействуют сложным образом
- Когда повторное использование объекта затруднено из-за связей с другими объектами
- Когда поведение, распределенное между классами, нужно настроить без создания подклассов
Пример (Java)
// Интерфейс посредника
interface ChatMediator {
void sendMessage(String message, User user);
void addUser(User user);
}
// Конкретный посредник
class ChatRoom implements ChatMediator {
private List<User> users = new ArrayList<>();
@Override
public void addUser(User user) {
users.add(user);
}
@Override
public void sendMessage(String message, User user) {
users.stream()
.filter(u -> u != user)
.forEach(u -> u.receive(message));
}
}
// Абстрактный коллега
abstract class User {
protected ChatMediator mediator;
protected String name;
public User(ChatMediator mediator, String name) {
this.mediator = mediator;
this.name = name;
}
public abstract void send(String message);
public abstract void receive(String message);
}
// Конкретный коллега
class ChatUser extends User {
public ChatUser(ChatMediator mediator, String name) {
super(mediator, name);
}
@Override
public void send(String message) {
System.out.println(name + " отправляет: " + message);
mediator.sendMessage(message, this);
}
@Override
public void receive(String message) {
System.out.println(name + " получает: " + message);
}
}
Применение в Java
- Spring ApplicationContext как посредник между beans
- Message Brokers (RabbitMQ, Apache Kafka)
- Event Bus patterns
- MVC контроллеры
8. Visitor (Посетитель)
Описание
Позволяет определить новую операцию, не изменяя классы объектов, над которыми она оперирует. Разделяет алгоритм от структуры объектов.
Когда использовать
- Когда нужно выполнить операцию над объектами сложной структуры
- Когда часто добавляются новые операции, но структура объектов стабильна
- Когда операции не связаны с основной логикой классов
Пример (Java)
// Интерфейс посетителя
interface Visitor {
void visit(Book book);
void visit(Movie movie);
}
// Интерфейс элемента
interface Element {
void accept(Visitor visitor);
}
// Конкретные элементы
class Book implements Element {
private String title;
private double price;
public Book(String title, double price) {
this.title = title;
this.price = price;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String getTitle() { return title; }
public double getPrice() { return price; }
}
class Movie implements Element {
private String title;
private double price;
public Movie(String title, double price) {
this.title = title;
this.price = price;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String getTitle() { return title; }
public double getPrice() { return price; }
}
// Конкретный посетитель
class PriceCalculator implements Visitor {
private double totalPrice = 0;
@Override
public void visit(Book book) {
totalPrice += book.getPrice() * 0.9; // скидка на книги
}
@Override
public void visit(Movie movie) {
totalPrice += movie.getPrice() * 0.8; // скидка на фильмы
}
public double getTotalPrice() { return totalPrice; }
}
Применение в Java
- Compiler design (AST traversal)
- XML/JSON processing
- File system operations
- Static analysis tools
9. Iterator (Итератор)
Описание
Предоставляет способ последовательного доступа к элементам составного объекта, не раскрывая его внутреннего представления.
Когда использовать
- Когда нужно обходить составной объект, не раскрывая его структуру
- Когда нужно поддерживать несколько способов обхода
- Когда нужен единообразный интерфейс для обхода различных структур
Пример (Java)
// Интерфейс итератора уже есть в Java Collections
import java.util.Iterator;
// Пользовательская коллекция
class BookCollection implements Iterable<Book> {
private List<Book> books = new ArrayList<>();
public void addBook(Book book) {
books.add(book);
}
@Override
public Iterator<Book> iterator() {
return new BookIterator();
}
// Внутренний класс итератора
private class BookIterator implements Iterator<Book> {
private int currentIndex = 0;
@Override
public boolean hasNext() {
return currentIndex < books.size();
}
@Override
public Book next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return books.get(currentIndex++);
}
}
}
Применение в Java
- Java Collections Framework (List, Set, Map)
- Stream API
- For-each loops
- ResultSet в JDBC
10. Memento (Хранитель)
Описание
Позволяет сохранить и восстановить предыдущее состояние объекта, не нарушая принципы инкапсуляции.
Когда использовать
- Когда нужно сохранить состояние объекта для последующего восстановления
- Когда прямое получение состояния нарушает инкапсуляцию
- Когда нужна функция "отмены" (Undo)
Пример (Java)
// Memento - хранитель состояния
class TextMemento {
private final String text;
private final int cursorPosition;
public TextMemento(String text, int cursorPosition) {
this.text = text;
this.cursorPosition = cursorPosition;
}
public String getText() { return text; }
public int getCursorPosition() { return cursorPosition; }
}
// Originator - создатель состояния
class TextEditor {
private String text;
private int cursorPosition;
public void setText(String text) {
this.text = text;
}
public void setCursorPosition(int position) {
this.cursorPosition = position;
}
public TextMemento createMemento() {
return new TextMemento(text, cursorPosition);
}
public void restoreFromMemento(TextMemento memento) {
this.text = memento.getText();
this.cursorPosition = memento.getCursorPosition();
}
public void showState() {
System.out.println("Текст: " + text + ", Позиция: " + cursorPosition);
}
}
// Caretaker - опекун
class EditorHistory {
private Stack<TextMemento> history = new Stack<>();
public void save(TextEditor editor) {
history.push(editor.createMemento());
}
public void undo(TextEditor editor) {
if (!history.isEmpty()) {
editor.restoreFromMemento(history.pop());
}
}
}
Применение в Java
- Undo/Redo functionality
- Database transactions
- Game save states
- Version control systems
Ключевые различия между паттернами
Паттерн | Основная цель | Когда использовать |
---|---|---|
Observer | Уведомление о изменениях | Один объект изменяется → много объектов реагируют |
Strategy | Взаимозаменяемые алгоритмы | Множество способов выполнения одной задачи |
Command | Инкапсуляция запросов | Нужна очередь, логирование или отмена операций |
Template Method | Скелет алгоритма | Общий алгоритм с вариациями в шагах |
State | Изменение поведения | Поведение зависит от состояния объекта |
Chain of Responsibility | Цепочка обработчиков | Запрос может быть обработан разными способами |
Mediator | Слабая связанность | Сложное взаимодействие между объектами |
Visitor | Новые операции | Часто добавляются операции, структура стабильна |
Iterator | Последовательный доступ | Нужен обход коллекции без раскрытия структуры |
Memento | Сохранение состояния | Нужна функция отмены или снапшоты состояния |
Советы для собеседования
- Знайте применение в Java: Изучите как паттерны используются в стандартной библиотеке и популярных фреймворках
- Понимайте проблемы: Сможете объяснить, какую конкретную проблему решает каждый паттерн
- Приводите примеры: Готовьте реальные примеры из своего опыта
- Знайте недостатки: Понимайте, когда паттерн может усложнить код
- Различайте похожие паттерны: Особенно State vs Strategy, Observer vs Mediator
Помните: Паттерны — это инструменты, а не цель. Используйте их для решения конкретных проблем, а не для демонстрации знаний.
Поведенческие паттерны в Spring Framework
Введение в поведенческие паттерны Spring
Spring Framework активно использует поведенческие паттерны для обеспечения слабой связанности, расширяемости и гибкости архитектуры. Понимание этих паттернов критически важно для Senior Java Backend разработчика, так как они лежат в основе многих Spring механизмов.
1. Observer Pattern в Spring Events
Что это такое?
Spring Events — это реализация паттерна Observer, которая позволяет компонентам приложения публиковать события и подписываться на них без прямых зависимостей.
Ключевые компоненты:
- ApplicationEvent — базовый класс для всех событий
- ApplicationEventPublisher — интерфейс для публикации событий
- @EventListener — аннотация для обработчиков событий
- ApplicationContext — выступает как посредник между издателями и подписчиками
Как работает:
// Пользовательское событие
public class UserRegisteredEvent extends ApplicationEvent {
private final String email;
public UserRegisteredEvent(Object source, String email) {
super(source);
this.email = email;
}
public String getEmail() { return email; }
}
// Издатель события
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher publisher;
public void registerUser(String email) {
// Бизнес-логика регистрации
System.out.println("Пользователь зарегистрирован: " + email);
// Публикация события
publisher.publishEvent(new UserRegisteredEvent(this, email));
}
}
// Подписчики на событие
@Component
public class EmailNotificationService {
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
System.out.println("Отправка welcome email: " + event.getEmail());
}
}
@Component
public class AnalyticsService {
@EventListener
@Async // Асинхронная обработка
public void handleUserRegistered(UserRegisteredEvent event) {
System.out.println("Отправка данных в аналитику: " + event.getEmail());
}
}
Преимущества:
- Слабая связанность — сервисы не знают друг о друге
- Расширяемость — легко добавлять новые обработчики
- Асинхронность — поддержка @Async для неблокирующей обработки
- Условная обработка — можно добавлять условия через SpEL
Применение в реальных проектах:
- Отправка уведомлений после бизнес-операций
- Логирование и аудит
- Кэширование и инвалидация
- Интеграция с внешними системами
2. Template Method в Spring Templates
Что это такое?
Spring Templates — это реализация паттерна Template Method, которая предоставляет готовый скелет для выполнения типичных операций с различными технологиями.
Основные Template классы:
- JdbcTemplate — для работы с базами данных
- RestTemplate — для HTTP-вызовов
- JmsTemplate — для работы с JMS
- RedisTemplate — для работы с Redis
JdbcTemplate — детальный разбор:
@Repository
public class UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
// Template Method скрывает:
// 1. Получение Connection
// 2. Создание PreparedStatement
// 3. Обработку исключений
// 4. Закрытие ресурсов
public List<User> findAllUsers() {
String sql = "SELECT id, name, email FROM users";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return user;
});
}
public void saveUser(User user) {
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
jdbcTemplate.update(sql, user.getName(), user.getEmail());
}
}
RestTemplate — пример использования:
@Service
public class ExternalApiService {
@Autowired
private RestTemplate restTemplate;
public UserDto getUserFromApi(Long userId) {
String url = "https://api.example.com/users/" + userId;
// Template Method обрабатывает:
// - Создание HTTP-запроса
// - Сериализацию/десериализацию
// - Обработку ошибок
// - Закрытие соединений
return restTemplate.getForObject(url, UserDto.class);
}
}
Преимущества Template Method в Spring:
- Убирает boilerplate код — нет необходимости в ручном управлении ресурсами
- Единообразие — один и тот же API для разных технологий
- Обработка исключений — автоматическая трансляция технологических исключений в Spring исключения
- Конфигурируемость — можно настроить поведение через параметры
3. Strategy Pattern в Spring Security
Что это такое?
Spring Security использует паттерн Strategy для предоставления различных способов аутентификации и авторизации.
Ключевые Strategy интерфейсы:
- AuthenticationProvider — стратегии аутентификации
- UserDetailsService — стратегии загрузки пользователей
- PasswordEncoder — стратегии кодирования паролей
- AccessDecisionManager — стратегии принятия решений о доступе
Пример с AuthenticationProvider:
// Стратегия для аутентификации через базу данных
@Component
public class DatabaseAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails user = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(password, user.getPassword())) {
return new UsernamePasswordAuthenticationToken(
username, password, user.getAuthorities());
}
throw new BadCredentialsException("Invalid credentials");
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
// Стратегия для аутентификации через JWT
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
@Autowired
private JwtTokenValidator jwtTokenValidator;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String token = (String) authentication.getCredentials();
if (jwtTokenValidator.isValid(token)) {
String username = jwtTokenValidator.getUsername(token);
List<GrantedAuthority> authorities = jwtTokenValidator.getAuthorities(token);
return new JwtAuthenticationToken(username, token, authorities);
}
throw new BadCredentialsException("Invalid JWT token");
}
@Override
public boolean supports(Class<?> authentication) {
return JwtAuthenticationToken.class.isAssignableFrom(authentication);
}
}
Конфигурация стратегий:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(
List<AuthenticationProvider> providers) {
return new ProviderManager(providers);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // Стратегия кодирования
}
}
Преимущества Strategy в Spring Security:
- Гибкость — можно комбинировать различные способы аутентификации
- Расширяемость — легко добавлять новые стратегии
- Переиспользование — стратегии можно использовать в разных контекстах
- Тестируемость — каждую стратегию можно тестировать отдельно
4. Chain of Responsibility в Spring Security Filters
Что это такое?
Spring Security Filter Chain — это реализация паттерна Chain of Responsibility для обработки HTTP-запросов через цепочку фильтров безопасности.
Основные фильтры в цепочке:
- SecurityContextPersistenceFilter — управление SecurityContext
- UsernamePasswordAuthenticationFilter — аутентификация по логину/паролю
- BasicAuthenticationFilter — HTTP Basic аутентификация
- BearerTokenAuthenticationFilter — OAuth2/JWT аутентификация
- AuthorizationFilter — авторизация доступа к ресурсам
Пример пользовательского фильтра:
@Component
public class CustomAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && isValidApiKey(apiKey)) {
// Создаем аутентификацию
ApiKeyAuthenticationToken authentication =
new ApiKeyAuthenticationToken(apiKey);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// Передаем управление следующему фильтру в цепочке
filterChain.doFilter(request, response);
}
private boolean isValidApiKey(String apiKey) {
// Логика проверки API ключа
return "valid-api-key".equals(apiKey);
}
}
Конфигурация цепочки фильтров:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(new CustomAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
}
Преимущества Chain of Responsibility:
- Модульность — каждый фильтр отвечает за свою задачу
- Гибкость — можно изменять порядок фильтров
- Расширяемость — легко добавлять новые фильтры
- Переиспользование — фильтры можно использовать в разных цепочках
5. Command Pattern в Spring MVC
Что это такое?
Spring MVC использует паттерн Command для обработки HTTP-запросов через контроллеры. Каждый метод контроллера представляет собой команду.
Ключевые компоненты:
- @Controller/@RestController — обработчики команд
- @RequestMapping — маппинг команд на URL
- HandlerMapping — определение команды для запроса
- HandlerAdapter — выполнение команды
Пример контроллера как Command:
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
// Команда для создания пользователя
@PostMapping
public ResponseEntity<User> createUser(@RequestBody CreateUserCommand command) {
User user = userService.createUser(command.getName(), command.getEmail());
return ResponseEntity.ok(user);
}
// Команда для обновления пользователя
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id,
@RequestBody UpdateUserCommand command) {
User user = userService.updateUser(id, command.getName(), command.getEmail());
return ResponseEntity.ok(user);
}
// Команда для удаления пользователя
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
// Command объекты для инкапсуляции параметров
public class CreateUserCommand {
private String name;
private String email;
// геттеры и сеттеры
}
public class UpdateUserCommand {
private String name;
private String email;
// геттеры и сеттеры
}
Дополнительные элементы Command Pattern:
// Interceptor для логирования команд
@Component
public class CommandLoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
System.out.println("Выполняется команда: " +
method.getMethod().getName());
}
return true;
}
}
// Глобальный обработчик ошибок команд
@ControllerAdvice
public class CommandExceptionHandler {
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
ValidationException ex) {
ErrorResponse error = new ErrorResponse("VALIDATION_ERROR", ex.getMessage());
return ResponseEntity.badRequest().body(error);
}
}
Преимущества Command Pattern в Spring MVC:
- Инкапсуляция — каждая операция представлена отдельной командой
- Логирование — можно легко логировать выполнение команд
- Валидация — команды можно валидировать с помощью Bean Validation
- Тестируемость — каждую команду можно тестировать отдельно
6. Mediator Pattern в Spring ApplicationContext
Что это такое?
Spring ApplicationContext выступает как посредник (Mediator) между различными компонентами приложения, обеспечивая их взаимодействие без прямых зависимостей.
Функции ApplicationContext как Mediator:
- Dependency Injection — внедрение зависимостей между компонентами
- Event Publishing — публикация и маршрутизация событий
- Resource Management — управление ресурсами приложения
- Lifecycle Management — управление жизненным циклом компонентов
Пример взаимодействия через ApplicationContext:
// Компоненты не знают друг о друге напрямую
@Service
public class OrderService {
@Autowired
private PaymentService paymentService; // Внедрено через ApplicationContext
@Autowired
private ApplicationEventPublisher eventPublisher; // Посредник для событий
public void processOrder(Order order) {
// Обработка заказа
paymentService.processPayment(order.getPayment());
// Публикация события через посредника
eventPublisher.publishEvent(new OrderProcessedEvent(order));
}
}
@Service
public class PaymentService {
public void processPayment(Payment payment) {
// Обработка платежа
System.out.println("Платеж обработан: " + payment.getAmount());
}
}
// Компоненты реагируют на события через посредника
@Component
public class EmailService {
@EventListener
public void handleOrderProcessed(OrderProcessedEvent event) {
System.out.println("Отправка email для заказа: " + event.getOrder().getId());
}
}
@Component
public class InventoryService {
@EventListener
public void handleOrderProcessed(OrderProcessedEvent event) {
System.out.println("Обновление инвентаря для заказа: " + event.getOrder().getId());
}
}
Конфигурация медиатора:
@Configuration
@EnableAsync
@EnableScheduling
public class ApplicationConfig {
// ApplicationContext автоматически выступает как медиатор
// для внедрения зависимостей
@Bean
public AsyncTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
Преимущества Mediator Pattern:
- Слабая связанность — компоненты не зависят друг от друга напрямую
- Централизованное управление — все взаимодействия проходят через ApplicationContext
- Гибкость — легко изменять взаимодействия между компонентами
- Тестируемость — можно легко создавать моки для тестирования
7. Visitor Pattern в Spring Expression Language (SpEL)
Что это такое?
Spring Expression Language использует паттерн Visitor для обхода и вычисления выражений в AST (Abstract Syntax Tree).
Применение SpEL в Spring:
- @Value — для внедрения значений из конфигурации
- @Conditional — для условной конфигурации
- Security expressions — для правил безопасности
- Cache expressions — для ключей кэша
Примеры использования SpEL:
@Component
public class ConfigurationComponent {
// Простое значение из properties
@Value("${app.name}")
private String appName;
// Выражение с условием
@Value("#{${app.debug:false} ? 'DEBUG' : 'PROD'}")
private String mode;
// Вызов метода через SpEL
@Value("#{T(java.lang.Math).random() * 100}")
private double randomValue;
// Работа с другими бинами
@Value("#{userService.getCurrentUserName()}")
private String currentUser;
}
// Использование в условной конфигурации
@Configuration
@ConditionalOnExpression("#{environment.getProperty('feature.enabled') == 'true'}")
public class FeatureConfiguration {
@Bean
public FeatureService featureService() {
return new FeatureServiceImpl();
}
}
// Использование в кэшировании
@Service
public class UserService {
@Cacheable(value = "users", key = "#userId", condition = "#userId > 0")
public User getUserById(Long userId) {
return userRepository.findById(userId);
}
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
userRepository.save(user);
}
}
Применение в Spring Security:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityMethodConfig {
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public void updateUser(Long userId, User user) {
// Метод доступен только админам или владельцу аккаунта
}
@PostAuthorize("returnObject.owner == authentication.principal.username")
public Document getDocument(Long documentId) {
// Возвращает документ только если текущий пользователь - владелец
return documentRepository.findById(documentId);
}
}
Преимущества Visitor Pattern в SpEL:
- Гибкость — можно создавать сложные выражения
- Переиспользование — выражения можно использовать в разных контекстах
- Безопасность — SpEL предоставляет контролируемую среду выполнения
- Интеграция — тесная интеграция с Spring контекстом
8. State Pattern в Spring State Machine
Что это такое?
Spring State Machine — это отдельный проект Spring для реализации конечных автоматов с использованием паттерна State.
Основные компоненты:
- State — состояния системы
- Event — события, которые могут вызвать переходы
- Transition — переходы между состояниями
- Action — действия, выполняемые при переходах
Пример конечного автомата для заказа:
// Определение состояний
public enum OrderStates {
CREATED,
PAID,
SHIPPED,
DELIVERED,
CANCELLED
}
// Определение событий
public enum OrderEvents {
PAY,
SHIP,
DELIVER,
CANCEL
}
// Конфигурация автомата
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states)
throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.states(EnumSet.allOf(OrderStates.class))
.end(OrderStates.DELIVERED)
.end(OrderStates.CANCELLED);
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions)
throws Exception {
transitions
.withExternal()
.source(OrderStates.CREATED).target(OrderStates.PAID)
.event(OrderEvents.PAY)
.action(paymentAction())
.and()
.withExternal()
.source(OrderStates.PAID).target(OrderStates.SHIPPED)
.event(OrderEvents.SHIP)
.action(shippingAction())
.and()
.withExternal()
.source(OrderStates.SHIPPED).target(OrderStates.DELIVERED)
.event(OrderEvents.DELIVER)
.action(deliveryAction())
.and()
.withExternal()
.source(OrderStates.CREATED).target(OrderStates.CANCELLED)
.event(OrderEvents.CANCEL)
.guard(orderCancellationGuard());
}
@Bean
public Action<OrderStates, OrderEvents> paymentAction() {
return context -> {
System.out.println("Обработка платежа...");
// Логика обработки платежа
};
}
@Bean
public Action<OrderStates, OrderEvents> shippingAction() {
return context -> {
System.out.println("Отправка заказа...");
// Логика отправки
};
}
@Bean
public Guard<OrderStates, OrderEvents> orderCancellationGuard() {
return context -> {
// Условие для отмены заказа
return true; // Можно отменить
};
}
}
Использование State Machine:
@Service
public class OrderService {
@Autowired
private StateMachine<OrderStates, OrderEvents> stateMachine;
public void processOrder(Order order) {
// Запуск автомата
stateMachine.start();
// Отправка событий
stateMachine.sendEvent(OrderEvents.PAY);
stateMachine.sendEvent(OrderEvents.SHIP);
stateMachine.sendEvent(OrderEvents.DELIVER);
// Получение текущего состояния
OrderStates currentState = stateMachine.getState().getId();
System.out.println("Текущее состояние: " + currentState);
}
}
Преимущества State Pattern в Spring State Machine:
- Явное управление состояниями — четкое определение всех возможных состояний
- Контролируемые переходы — невозможно попасть в недопустимое состояние
- Действия и охранники — можно выполнять действия при переходах и добавлять условия
- Персистентность — состояние можно сохранять и восстанавливать
Сводная таблица применения паттернов в Spring
Паттерн | Реализация в Spring | Основное назначение | Пример использования |
---|---|---|---|
Observer | Spring Events | Слабая связанность компонентов | Отправка уведомлений после бизнес-операций |
Template Method | JdbcTemplate, RestTemplate | Убирание boilerplate кода | Работа с БД, HTTP-вызовы |
Strategy | Spring Security | Взаимозаменяемые алгоритмы | Различные способы аутентификации |
Chain of Responsibility | Security Filter Chain | Поэтапная обработка запросов | Фильтры безопасности |
Command | Spring MVC Controllers | Инкапсуляция операций | Обработка HTTP-запросов |
Mediator | ApplicationContext | Центральное управление | Dependency Injection, Event Publishing |
Visitor | SpEL | Обход и вычисление выражений | @Value, @Conditional, Security expressions |
State | Spring State Machine | Управление состояниями | Workflow, бизнес-процессы |
Ключевые вопросы для собеседования
1. Spring Events vs JMS/RabbitMQ
Вопрос: "Когда использовать Spring Events, а когда внешние message brokers?"
Ответ:
- Spring Events — для внутренних событий в рамках одного приложения
- Message Brokers — для межсервисного взаимодействия, гарантии доставки, персистентности
2. Проблемы с Spring Events
Вопрос: "Какие проблемы могут возникнуть с Spring Events?"
Ответ:
- Синхронность по умолчанию — блокирует основной поток
- Отсутствие гарантий доставки — при падении приложения события теряются
- Отсутствие retry — нужно реализовывать самостоятельно
- Сложность отладки — неявные зависимости между компонентами
3. Различия между Template классами
Вопрос: "В чем разница между JdbcTemplate и JPA?"
Ответ:
- JdbcTemplate — низкоуровневый, полный контроль над SQL
- JPA — высокоуровневый, ORM, автоматическая генерация SQL
- Производительность — JdbcTemplate быстрее для простых операций
- Сложность — JPA лучше для сложных доменных моделей
4. Безопасность SpEL
Вопрос: "Какие проблемы безопасности есть у SpEL?"
Ответ:
- Code injection — если позволить пользователю вводить SpEL выражения
- Доступ к системным классам — можно ограничить через SecurityManager
- Производительность — компиляция выражений может быть медленной
5. Альтернативы Spring State Machine
Вопрос: "Когда НЕ стоит использовать Spring State Machine?"
Ответ:
- Простые состояния — достаточно enum и if-else
- Распределенные системы — лучше использовать Saga Pattern
- Высокие нагрузки — может быть overhead
- Легаси системы — сложно интегрировать
Практические советы
1. Производительность
- @Async для Spring Events — не забывайте про асинхронность
- Кэширование SpEL выражений — компилируйте один раз
- Пул потоков для Template классов — настройте правильно
2. Тестирование
- @MockBean для изоляции компонентов
- @TestEventListener для тестирования событий
- @TestConfiguration для тестовых конфигураций
3. Мониторинг
- Actuator для мониторинга Spring компонентов
- Micrometer для метрик
- Логирование событий и переходов состояний
Помните: Spring паттерны — это не просто теория, а практические инструменты для создания maintainable и scalable приложений.