Недавно я решала задачу — нужно было валидировать файлы при загрузке и вызывать для них метод convert. Сложность в том, что у меня было 4-5 форматов, и со временем их могло стать еще больше.
Какие были варианты решения:
☠️ if-case
Просто перечислить все форматы и вызвать нужный метод:
if (format == FileFormat.arb) {
validateArb(file);
} else if (format == FileFormat.csv) {
validateCsv(file);
}
// ...
// И это только validate, а еще есть convert...В целом это будет работать, но немного отдает болью и будущими проблемами в виде сложно поддерживаемого кода.
Добавить новый формат — +1 условие.
Протестировать — сложновато.
Расширять — ну такое.
✅ Использовать паттерн проектирования
Я решила, что Стратегия — то, что нужно.
Что говорит теория?
Стратегия — поведенческий паттерн, который:
▫️ выделяет семейство алгоритмов
▫️ выносит каждый из них в отдельный класс
▫️ делает их взаимозаменяемыми
Проще говоря: если у тебя есть несколько вариантов поведения, которые могут меняться, стратегия позволяет:
▪️ вынести каждый вариант в отдельный класс
▪️ использовать их через единый интерфейс
▪️заменять поведение в рантайме без изменения основного кода
В жизни часто можно столкнуться со стратегией: выбор способа оплаты (карта, наличные), выбор транспорта (метро, такси, автобус).
Вернемся к задаче
Я определила общий интерфейс FileConversionStrategy, который описывает, как работать с файлом:
abstract class FileConversionStrategy {
bool supports(FileFormat format);
FutureOr<ImportFileModel> convert(
ImportFileModel file,
List<String> editedResult,
);
void validate(ImportFileModel file);
}Затем написала реализации под каждый формат. Например, ArbConversionStrategy:
class ArbConversionStrategy implements FileConversionStrategy {
@override
bool supports(FileFormat format) => format == FileFormat.arb;
@override
FutureOr<ImportFileModel> convert(
ImportFileModel file,
List<String> editedResult,
) {
// логика конвертации
}
@override
void validate(ImportFileModel file) {
// логика валидации
}
}Каждая стратегия знает, как обрабатывать только свой формат. И вуаля — проблема решена! Теперь я могу добавлять сколько угодно новых форматов — достаточно просто написать для него реализацию FileConversionStrategy.
❕Но это еще не все. У нас есть реализации, но как выбрать необходимую в нужный момент?
Для этого я написала менеджер стратегий. Он перебирает доступные реализации и возвращает ту, что подходит:
class FileConversionStrategyManager {
final List<FileConversionStrategy> strategies;
FileConversionStrategyManager(this.strategies);
FileConversionStrategy getStrategy(FileFormat format) {
return strategies.firstWhere(
(strategy) => strategy.supports(format),
orElse: () => throw UnsupportedError('No strategy for format $format'),
);
}
}А теперь разберем, как все это работает вместе:
1. Контекст — запрашивает подходящую стратегию и работает с ней через единый интерфейс
2. Интерфейс — объединяет все алгоритмы в общую структуру (validate, convert)
3. Конкретные стратегии — реализуют обработку разных форматов (CSV, ARB, JSON и других)
4. Во время выполнения мы подбираем нужную стратегию
5. Можно легко заменить стратегию на новую
Но применять паттерны ради паттернов — плохая идея. Их сила не в модности, а в том, что они решают конкретные проблемы.
Поэтому используйте стратегию:
✔️ Когда у вас разные сценарии поведения, и хочется вынести их из основного кода
✔️ Когда поведение часто меняется или дополняется
✔️ Когда хочется разделить ответственность и не мешать все в одном классе
Это уже третий паттерн. Здесь подробнее про:
🔗Observer
🔗Mediator
