Какие паттерны проектирования не нравятся
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Критический взгляд на паттерны проектирования
За 10+ лет разработки я научился критически относиться к паттернам проектирования. Они — не панацея, а инструменты, которые нужно применять с осторожностью. Расскажу о паттернах, которые, на мой взгляд, часто используются неправильно или приносят больше вреда, чем пользы.
1. Singleton — самый опасный паттерн
Singleton часто критикуют, и в этом есть очень веские причины.
// Классический Singleton
public class DatabaseConnection {
private static DatabaseConnection instance;
private DatabaseConnection() {}
public static synchronized DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
}
// Где это создает проблемы:
// 1. Скрытые зависимости
public class UserService {
// Зависимость от DatabaseConnection замаскирована
public void createUser(String name) {
DatabaseConnection db = DatabaseConnection.getInstance();
// ...
}
}
// 2. Невозможно тестировать (сложно мокировать)
// 3. Потокобезопасность — часто реализована неправильно
// 4. Нарушение принципа Single Responsibility
Почему не нравится:
- Скрытые зависимости — код получает зависимость не через конструктор
- Сложность тестирования — нельзя легко подменить на mock
- Глобальное состояние — нарушает принципы FP и чистоты функций
- Потокобезопасность — обычно реализуется неправильно
Что использовать вместо Singleton:
// Инъекция зависимостей (правильный подход)
public class UserService {
private final DatabaseConnection db;
// Зависимость явно указана в конструкторе
public UserService(DatabaseConnection db) {
this.db = db;
}
public void createUser(String name) {
db.executeQuery("INSERT INTO users...");
}
}
// В Spring это просто
@Service
public class UserService {
@Autowired
private DatabaseConnection db; // Spring управляет жизненным циклом
}
2. Абстрактная фабрика — за-инженеринг
Этот паттерн часто используется, когда вообще не нужен.
// Абстрактная фабрика - перебор
interface UIFactory {
Button createButton();
TextField createTextField();
CheckBox createCheckBox();
}
class WindowsUIFactory implements UIFactory {
public Button createButton() { return new WindowsButton(); }
public TextField createTextField() { return new WindowsTextField(); }
public CheckBox createCheckBox() { return new WindowsCheckBox(); }
}
// Проблемы:
// 1. Много бойлерплейта
// 2. Сложно добавлять новые методы (нужно менять все фабрики)
// 3. Часто используется где-то, где достаточно простой фабрики
Почему не нравится:
- Оверинженеринг — используется где простой фабрики достаточно
- Жесткость — добавление новых методов требует изменения всех реализаций
- Много кода — большое количество бойлерплейта
Альтернатива (с Java 8+):
// Функциональный подход - гораздо проще
public class UIFactory {
private Supplier<Button> buttonCreator;
private Supplier<TextField> textFieldCreator;
public UIFactory(
Supplier<Button> buttonCreator,
Supplier<TextField> textFieldCreator
) {
this.buttonCreator = buttonCreator;
this.textFieldCreator = textFieldCreator;
}
public Button createButton() { return buttonCreator.get(); }
public TextField createTextField() { return textFieldCreator.get(); }
}
// Использование
UIFactory factory = new UIFactory(
() -> new WindowsButton(),
() -> new WindowsTextField()
);
3. Fluent Builder — красиво, но сложно
// Fluent Builder
public class QueryBuilder {
private String table;
private List<String> columns = new ArrayList<>();
private List<String> conditions = new ArrayList<>();
public QueryBuilder from(String table) {
this.table = table;
return this;
}
public QueryBuilder select(String... cols) {
columns.addAll(Arrays.asList(cols));
return this;
}
public QueryBuilder where(String condition) {
conditions.add(condition);
return this;
}
public String build() {
// Сложная логика построения запроса
return "SELECT " + String.join(", ", columns) + " FROM " + table;
}
}
// Использование
String sql = new QueryBuilder()
.from("users")
.select("id", "name", "email")
.where("age > 18")
.where("country = 'USA'")
.build();
// Проблемы:
// 1. Можно забыть вызвать build() — нет гарантий
// 2. Невозможна валидация на этапе построения
// 3. Может быть очень много методов в цепи
Почему не нравится:
- Нет типобезопасности — можно создать невалидный объект
- Сложность отладки — цепь вызовов может быть длинной
- Мутирует состояние — промежуточные объекты меняются
Альтернатива (конструктор с параметрами):
// Просто используй конструктор
public class Query {
private final String table;
private final List<String> columns;
private final List<String> conditions;
public Query(String table, List<String> columns, List<String> conditions) {
this.table = table;
this.columns = columns;
this.conditions = conditions;
}
}
// Или record в Java 16+
public record Query(
String table,
List<String> columns,
List<String> conditions
) {}
4. Service Locator — скрытые зависимости
// Service Locator - антипаттерн
public class ServiceLocator {
private static Map<Class<?>, Object> services = new HashMap<>();
public static void register(Class<?> service, Object instance) {
services.put(service, instance);
}
public static <T> T getService(Class<T> service) {
return (T) services.get(service);
}
}
// Использование (плохо)
public class UserService {
public void createUser(String name) {
DatabaseConnection db = ServiceLocator.getService(DatabaseConnection.class);
// Неявная зависимость!
}
}
// Проблемы:
// 1. Скрытые зависимости (как Singleton)
// 2. Сложно тестировать
// 3. Код неясен — не видно, какие зависимости нужны
Почему не нравится:
- Скрытые зависимости — невозможно понять из сигнатуры метода, что нужно
- Runtime ошибки — зависимость не найдена только во время выполнения
- Сложность тестирования — нужно регистрировать все в Service Locator
5. GoF паттерны которые устарели
Observer (до Java 8)
// Старый способ (Observer паттерн из GoF)
interface Observer {
void update(Observable o, Object arg);
}
class ConcreteObserver implements Observer {
public void update(Observable o, Object arg) {
System.out.println("Событие: " + arg);
}
}
// Современный способ (функциональный)
observable.subscribe(
event -> System.out.println("Событие: " + event)
);
Strategy (до Java 8)
// Старый способ (Strategy паттерн)
interface SortStrategy {
void sort(List<?> items);
}
class QuickSortStrategy implements SortStrategy {
public void sort(List<?> items) { /* ... */ }
}
class Sorter {
private SortStrategy strategy;
Sorter(SortStrategy strategy) { this.strategy = strategy; }
void sort(List<?> items) { strategy.sort(items); }
}
// Современный способ (функциональный)
List<Integer> items = Arrays.asList(3, 1, 2);
items.sort((a, b) -> Integer.compare(a, b)); // Strategy как lambda
6. Template Method — жесткая иерархия
// Template Method - часто приводит к жесткой иерархии
abstract class ReportGenerator {
public final void generate() {
collectData();
formatData();
sendReport();
}
protected abstract void collectData();
protected abstract void formatData();
protected abstract void sendReport();
}
class PDFReportGenerator extends ReportGenerator {
@Override
protected void collectData() { }
@Override
protected void formatData() { }
@Override
protected void sendReport() { }
}
// Проблемы:
// 1. Жесткая иерархия (наследование)
// 2. Сложно комбинировать различные шаги
// 3. Нарушает composition over inheritance
// Функциональный подход (лучше)
public class ReportGenerator {
private Consumer<List<?>> dataCollector;
private Function<List<?>, String> formatter;
private Consumer<String> sender;
public void generate() {
List<?> data = new ArrayList<>();
dataCollector.accept(data);
String formatted = formatter.apply(data);
sender.accept(formatted);
}
}
7. Decorator — может стать слишком сложным
// Decorator может быть невероятно сложным
InputStream input = new GZIPInputStream(
new BufferedInputStream(
new FileInputStream("file.txt")
)
);
// Проблемы:
// 1. Много слоев обёртывания
// 2. Сложно отследить что где происходит
// 3. Порядок Decorator'ов критичен
// Альтернатива (if возможна) - конфигурация
public class InputStreamConfig {
private boolean gzipped = false;
private boolean buffered = false;
public InputStream build(String filename) throws IOException {
InputStream stream = new FileInputStream(filename);
if (buffered) stream = new BufferedInputStream(stream);
if (gzipped) stream = new GZIPInputStream(stream);
return stream;
}
}
Рекомендации
- Не используй паттерны "потому что они есть" — используй потому что они решают реальную проблему
- Предпочитай композицию наследованию — это часто заменяет множество паттернов
- Используй функциональное программирование — Java 8+ позволяет избежать много паттернов
- Тестируемость > проектирование — если код сложно тестировать, это плохой паттерн
- KISS (Keep It Simple, Stupid) — самое важное правило
Что использовать вместо этих паттернов
// Dependency Injection вместо Singleton
// Composition вместо Inheritance
// Functional interfaces вместо Strategy/Observer
// Constructor параметры вместо Service Locator
// Spring/Guice вместо ручного управления зависимостями
Заключение
Паттерны проектирования — это инструменты, а не закон. Лучшие разработчики знают когда использовать паттерны, когда нарушать их, и когда просто писать простой, понятный код. YAGNI (You Aren't Gonna Need It) и KISS часто важнее любого паттерна.