Моя повседневная работа с основном связана с языком Elm. Благодаря сочетанию функционального языка и архитектуры Elm, многие архитектурные решения в нём получаются практически незаметными (подробнее об этом я рассказываю в этом посте у меня в блоге). Вы получаете четкое разделение задач, а язык по умолчанию подталкивает вас к хорошему проектированию.
Но моя работа не ограничивается Elm. Я часто создаю функции, которые охватывают как фронтенд, так и бэкенд — пишу новые конечные точки, а иногда даже проектирую новые таблицы баз данных. Когда я выхожу за пределы мира Elm, я вспоминаю, что архитектура — это то, к чему я должен снова относиться сознательно.
Этот пост не о том, какой код идет в какой слой, и не о конкретных паттернах проектирования. Прежде всего, я хочу поговорить об обманчиво простом выборе: следует ли просто реализовать то, что вам нужно, или же лучше начать с создания абстракции?
Я буду утверждать, что начинать с абстракции выгодно, даже если это кажется лишней работой.
Давайте возьмём конкретный пример.
Предположим, мне нужна SearchCacheRepository — сущность, которая будет хранить и извлекать кэшированные результаты поиска.
Я мог бы просто написать класс, который делает то, что мне нужно, сразу перейти к созданию соответствующих таблиц (и индексов) — и на этом закончить.
Но вместо этого я ловлю себя на том, что тянусь к созданию интерфейса:
@ImplementedBy(SearchCacheInMemoryImpl::class)
interface SearchCacheRepository {
val cacheTimeout: Duration
suspend fun getCachedSearch(
userID: Int,
searchId: String,
): Either<CacheError, CachedSearch>
}
А затем я напишу быструю реализацию в оперативной памяти:
@Singleton
class SearchCacheInMemoryImpl : SearchCacheRepository {
override val cacheTimeout = 10.seconds
// Реализация с использованием hashMap
}
Позже, когда мне понадобится сохранить данные, я смогу добавить реализацию на основе SQL:
@Singleton
class SearchCacheSqlImpl
@Inject
constructor(
private val dbProvider: DatasourceProvider,
override val cacheTimeout: Duration
) : SearchCacheRepository {
// Реализация с использованием Postgres или чего-либо подобного
}
Зачем заморачиваться абстракцией?
Заманчиво считать это ненужными затратами — почему бы просто не написать нужный код, а потом, если действительно понадобится заменить реализацию, провести рефакторинг? Но на практике я обнаружил, что начало с абстракции дает несколько важных преимуществ:
Ясность намерения: определив сначала интерфейс, я вынужден подумать о том, какая функциональность мне действительно нужна. Я даже могу начать вызывать методы со стороны потребителя до того, как я их реализовал, что является отличным способом проверить, подходит ли API на практике.
Ускорение итераций: реализация в оперативной памяти бесполезна в продакшене, но она невероятно удобна для локальной разработки и тестирования. Я могу запустить остальную часть системы, провести как ручные, так и автоматические тесты, и только когда буду готов, заняться «реальной» реализацией.
Параллельная разработка: если задача разделена между несколькими разработчиками, я могу передать реализацию SQL кому-то другому, не нарушая соглашения между бэкэндом и фронтендом или между контроллером/маршрутом и репозиторием. Все могут работать параллельно, будучи уверенными, что все будет работать как надо.
Простая замена: когда придет время перейти от in-memory версии к реальной, нужно будет просто подключить новую реализацию. Не нужно трогать остальную часть кодовой базы.
Бонусный балл: на самом деле чаще, чем вы думаете, принцип YAGNI (You Ain’t Gonna Need It, «тебе это не понадобится») вступает в силу там, где вы этого не ожидаете. Например, вы начинаете с файлового хранилища и обнаруживаете, что оно вполне удовлетворяет вашим потребностям на долгие годы, прежде чем вам действительно понадобится та корпоративно-кластерно-солнечно-эластично-космическая база данных, которую Azure пытается вам продать.
Когда абстракция не нужна?
Конечно, не каждый фрагмент кода требует интерфейса или дополнительного уровня. Иногда правильнее выбрать непосредственную реализацию. Например:
одноразовые скрипты или миграция;
по-настоящему тривиальная логика, которая вряд ли изменится;
внутренний код с единственным потребителем;
быстрые прототипы или тестовые реализации;
признать, что скорость доставки важнее гибкости.
Это не исчерпывающий список, но суть в том, что нужно действовать осознанно. Используйте абстракции тогда, когда они решают реальную задачу, а не просто по привычке.
С последними двумя пунктами, впрочем, будьте особенно осторожны: никогда не знаешь, когда ваш прототип внезапно окажется в продакшене, и вам (или какому-то другому бедолаге) придётся его поддерживать.
Заключение
По моему опыту, небольшие изначальные затраты на определение абстракции часто окупаются многократно. Дело не только в подготовке к будущим изменениям или в удобстве тестирования (хотя и это приятные побочные эффекты) — речь о том, чтобы упростить процесс размышлений, итераций и совместной работы. Даже если вы единственный разработчик в проекте, ваше будущее «я» скажет вам спасибо.
А если вы пришли из Elm, где архитектура почти незаметна, стоит помнить: немного явной структуры может сильно помочь — особенно в языках, где компилятор не держит вас за руку крепко.
Oeaoo
Тут дело в дуальности, как со светом. С одной стороны, программа - это набор сущностей (частиц). А с другой - функций (волн). Как посмотришь так и будет. Но важно иметь в коде и одно и другое, как инь и ян.
bromzh
Ну хватит уже столетние модели КМ теребить, нет там "дуальности".