Как я на практике понял паттерн «Интерфейс»
Как я на практике понял паттерн «Интерфейс», не читая о нём тонны теории
Когда писал своего первого торгового робота, меня захлестнуло море однотипного кода. Каждая новая стратегия выглядела как злой близнец предыдущей: та же структура, но с немного изменённой душой.
Вот классический портрет моей функции generate-signal
в те дни:
(defn generate-signal-stupid-rsi [] ;; получение свечи ;; получение цены закрытия ;; логика индикаторов (RSI) ;; вызов функции создания сигнала signal) (defn generate-signal-clever-ma [] ;; получение свечи ;; получение цены закрытия ;; логика индикаторов (Moving Average) ;; вызов функции создания сигнала signal)
Я занимался самым примитивным копипастом, а затем методично переименовывал функции и переписывал блок с логикой. Это было скучно, некрасиво и чревато ошибками. Любое изменение в общем механизме (например, в способе получения свечи) требовало правок в каждой такой функции. Мой мозг кричал: «Должно быть лучше!».
Озарение: от хаоса к порядку через боль
В какой-то момент чаша терпения переполнилась и захотелось сделать одну-единственную, универсальную функцию generate-signal
, которая могла бы работать с любой стратегией.
Решение оказалось на удивление простым. Взглянул на все свои стратегии и задался вопросом: «Что у них общего?». Несмотря на разную логику, все они в конечном счёте выдавали некий результат — сигнал на покупку или продажу. Формализовал этот результат и договорился с самим собой, что каждая стратегия будет возвращать данные в строго определённом формате, например, хэш-карту с ключами :signal
, :price
и :confidence
.
И тогда родилась универсальная функция-обработчик:
(defn universal-signal-generator [strategy-fn] (let [result (strategy-fn)] ; Получаем данные от стратегии ; Теперь мы знаем ТОЧНЫЙ формат result! (if (:buy? result) (place-order (:price result)) (do-nothing))))
А сами стратегии превратились в чистые, независимые функции, которые только занимаются вычислениями и возвращают данные в ожидаемом формате.
(defn rsi-strategy [] ;; вся сложная логика здесь {:signal :buy, :price 100.50, :confidence 0.85}) ; <- Вот этот формат и есть контракт (defn ma-strategy [] ;; вся сложная логика здесь {:signal :sell, :price 99.20, :confidence 0.72})
И тут меня осенило. То, что я только что сделал, и есть тот самый, загадочный паттерн «Интерфейс» (или его функциональный аналог — протокол), о котором я столько читал и смотрел видео, но никак не мог понять на практике.
Не придумывая абстрактное правило и не подгоняя под него код. Решение выявилось из своего собственного, „плохого“ кода, через боль повторения и неудобство.
Почему «снизу вверх» работает лучше
С ужасом представил, как бы страдал, подходя к задаче «правильно» с точки зрения учебников: сначала сел бы, продумал идеальный интерфейс, объявил его, а потом начал бы под него строить все стратегии. Я бы потратил кучу времени на абстракции, которые, возможно, мне и не пригодились бы. Был бы скован страхом сделать «не по канону». У меня уже была статья про то как страх «плохого кода» мешает развиваться.
Мой же путь — это тактическое программирование и последующий рефакторинг:
- Сделать как попало. Быстро решить задачу первым работающим способом.
- Почувствовать боль. Столкнуться с последствиями (копипаст, сложность изменений).
- Выявить паттерн. Проанализировать, что общее во всех «плохих» решениях.
- Абстрагировать. Выделить общую логику в универсальный механизм (интерфейс).
Мой мозг так и работает: чтобы по-настоящему понять суть паттерна или принципа, мне нужно докопаться до него самому, пройдя через неоптимальные решения. Это не просто «говнокодить», это — исследовательский итеративный процесс. Грязный код — это сырьё, из которого выкристаллизовывается чистая архитектура.
Но самое интересное началось потом. Когда у меня появился этот универсальный universal-signal-generator
, я не остановился и осознал, что создал не просто функцию, а точку расширения системы.
Внезапно добавление новой стратегии стало делом пяти минут. Любой новый алгоритм, от скользящих средних до машинного обучения, можно было "завернуть" в функцию, возвращающую данные в ожидаемом формате, и просто "скормить" моему обработчику. Система стала открытой для расширения, но закрытой для изменений — а это уже краеугольный камень хорошей архитектуры.
Ирония в том, что в функциональных языках, таких как Clojure, этот паттерн выглядит еще элегантнее. Тебе не нужны явные interface
и implements
из ООП. Интерфейс здесь — это "протокол" в терминах Clojure или просто ожидаемая структура данных (например, тот самый map с ключами :signal
и :price
). Это "утиная типизация" в лучшем ее проявлении: если функция возвращает map с нужными ключами — система примет ее с распростертыми объятиями. Это делает архитектуру невероятно гибкой и декомпонованной.
Так что если вы, как и я, не до конца понимаете, зачем нужны все эти интерфейсы, абстракции и паттерны, — попробуйте мой способ. Сначала сделайте «как получится», позвольте себе это. А затем прислушайтесь к своему коду, и он сам подскажет, где нужен порядок. Не бойтесь сперва нарисовать картину красками, чтобы потом увидеть, какие именно линии нужно будет обвести черным контуром.
Возможно, этот путь — от тактического хаоса к стратегическому порядку — окажется для вас таким же прозрением, как и для меня. Вы не просто заучите паттерны, а проживете их необходимость, и это знание останется с вами навсегда.