Лабораторная работа № 11 — «Шаблоны проектирования и SOLID на существующем коде»
Перед началом сгенерируйте в ИИ намеренно плохой код:
public BigDecimal calculateFee( Transaction t, String strategy ) { if ("STANDARD".equals(strategy)) { ... } else if ("PREMIUM".equals(strategy)) { ... } else if ("STUDENT".equals(strategy)) { ... } ... }
и:
if (format.equals("CSV")) { ... } else if (format.equals("JSON")) { ... } else if (format.equals("TEXT")) { ... }
Затем последовательно решаются четыре задачи.
Strategy.
public interface FeeStrategy { BigDecimal calculate(Transaction transaction); }
Реализации:
StandardFeeStrategy StudentFeeStrategy PremiumFeeStrategy
Клиент:
public class FeeService { private final FeeStrategy strategy; public FeeService(FeeStrategy strategy) { this.strategy = strategy; } }
Главный вопрос: почему это лучше цепочки if/switch, если алгоритмы действительно взаимозаменяемы?
Observer.
Студент создаёт собственную простую модель:
public interface TransactionListener { void onTransactionCreated(Transaction transaction); }
Наблюдатели:
AuditListener LargeExpenseAlertListener StatisticsListener
После добавления операции сервис публикует событие, не зная деталей реакции.
Не стоит строить лабораторную на старом java.util.Observable: студенту полезнее реализовать контракт самостоятельно, а затем на защите связать идею с event-механизмами framework'ов.
Factory Method.
Здесь важно не называть любой switch, возвращающий объект, «Factory Method». Лучше показать собственно идею overridable creation method:
public abstract class ReportGenerator { public final String generate( List<Transaction> transactions ) { ReportFormatter formatter = createFormatter(); return formatter.format(transactions); } protected abstract ReportFormatter createFormatter(); }
public class CsvReportGenerator extends ReportGenerator { @Override protected ReportFormatter createFormatter() { return new CsvReportFormatter(); } }
Студент сравнивает это с простой статической фабрикой и объясняет различие.
Singleton.
Singleton не следует превращать в обязательную архитектуру приложения. Практическая задача состоит из двух шагов: студент реализует небольшой корректный singleton, например через enum, а затем анализирует, что произойдёт, если сделать singleton'ом TransactionService с глобальным mutable state.
После Spring-лабораторной появляется хороший контрольный вопрос:
Одинаковы ли GoF Singleton и Spring bean с singleton scope?
Ответ: нет. У них родственная идея «один экземпляр в некоторой области», но Spring управляет lifecycle и scope внутри контейнера, а GoF Singleton кодирует контроль экземпляра самим классом. Spring действительно использует singleton как default bean scope, но это scope контейнера, а не требование добавлять private static instance в каждый сервис.
SOLID-анализ.
Студенту не требуется заучивать SOLID отдельно от кода. Он заполняет таблицу:
| Изменение | Какой принцип затронут |
|---|---|
Убрали печать из Transaction
|
SRP |
Сервис зависит от TransactionRepository, а не JPA-класса
|
DIP |
| Новый fee algorithm добавляется новым Strategy | OCP |
| Маленькие специализированные interfaces | ISP |
| Реализации repository взаимозаменяемы | LSP/DIP |
Для каждого паттерна студент отвечает на три вопроса:
какая проблема была до паттерна? какие участники появились? какую цену/сложность добавил паттерн?
Это защищает курс от классической ошибки «паттерны всегда улучшают код». Паттерн должен появляться из проблемы.
Вопросы для углубления:
Что такое паттерн проектирования и чем он
отличается от «архитектурного стиля»?Какие есть
группы паттернов
(порождающие/структурные/поведенческие)?В
каких ситуациях уместны Singleton и Factory
Method?В чем идея Observer и Strategy (задачи,
которые они решают)?Как связаны принципы
SOLID и применение паттернов?
