🧑‍💻 Код
August 24

Как я на практике понял паттерн «Интерфейс»

Как я на практике понял паттерн «Интерфейс», не читая о нём тонны теории

Когда писал своего первого торгового робота, меня захлестнуло море однотипного кода. Каждая новая стратегия выглядела как злой близнец предыдущей: та же структура, но с немного изменённой душой.

Вот классический портрет моей функции 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})

И тут меня осенило. То, что я только что сделал, и есть тот самый, загадочный паттерн «Интерфейс» (или его функциональный аналог — протокол), о котором я столько читал и смотрел видео, но никак не мог понять на практике.

Не придумывая абстрактное правило и не подгоняя под него код. Решение выявилось из своего собственного, „плохого“ кода, через боль повторения и неудобство.

Почему «снизу вверх» работает лучше

С ужасом представил, как бы страдал, подходя к задаче «правильно» с точки зрения учебников: сначала сел бы, продумал идеальный интерфейс, объявил его, а потом начал бы под него строить все стратегии. Я бы потратил кучу времени на абстракции, которые, возможно, мне и не пригодились бы. Был бы скован страхом сделать «не по канону». У меня уже была статья про то как страх «плохого кода» мешает развиваться.

Мой же путь — это тактическое программирование и последующий рефакторинг:

  1. Сделать как попало. Быстро решить задачу первым работающим способом.
  2. Почувствовать боль. Столкнуться с последствиями (копипаст, сложность изменений).
  3. Выявить паттерн. Проанализировать, что общее во всех «плохих» решениях.
  4. Абстрагировать. Выделить общую логику в универсальный механизм (интерфейс).

Мой мозг так и работает: чтобы по-настоящему понять суть паттерна или принципа, мне нужно докопаться до него самому, пройдя через неоптимальные решения. Это не просто «говнокодить», это — исследовательский итеративный процесс. Грязный код — это сырьё, из которого выкристаллизовывается чистая архитектура.

Но самое интересное началось потом. Когда у меня появился этот универсальный universal-signal-generator, я не остановился и осознал, что создал не просто функцию, а точку расширения системы.

Внезапно добавление новой стратегии стало делом пяти минут. Любой новый алгоритм, от скользящих средних до машинного обучения, можно было "завернуть" в функцию, возвращающую данные в ожидаемом формате, и просто "скормить" моему обработчику. Система стала открытой для расширения, но закрытой для изменений — а это уже краеугольный камень хорошей архитектуры.

Ирония в том, что в функциональных языках, таких как Clojure, этот паттерн выглядит еще элегантнее. Тебе не нужны явные interface и implements из ООП. Интерфейс здесь — это "протокол" в терминах Clojure или просто ожидаемая структура данных (например, тот самый map с ключами :signal и :price). Это "утиная типизация" в лучшем ее проявлении: если функция возвращает map с нужными ключами — система примет ее с распростертыми объятиями. Это делает архитектуру невероятно гибкой и декомпонованной.

Так что если вы, как и я, не до конца понимаете, зачем нужны все эти интерфейсы, абстракции и паттерны, — попробуйте мой способ. Сначала сделайте «как получится», позвольте себе это. А затем прислушайтесь к своему коду, и он сам подскажет, где нужен порядок. Не бойтесь сперва нарисовать картину красками, чтобы потом увидеть, какие именно линии нужно будет обвести черным контуром.

Возможно, этот путь — от тактического хаоса к стратегическому порядку — окажется для вас таким же прозрением, как и для меня. Вы не просто заучите паттерны, а проживете их необходимость, и это знание останется с вами навсегда.