🧑‍💻 Код
August 23

Библиотека для трейдинга: архитектура

Архитектура Clojure-библиотеки: Почему я выбрал плоскую структуру неймспейсов

Архитектура taljure

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

Вот как выглядит src/taljure/core.clj:

(ns taljure.core
  (:require
   [taljure.indicators.simple :as simple]))

(def moving-avg simple/moving-avg)
(def rsi simple/rsi)
(def sma simple/sma)
(def ema simple/ema)
(def atr simple/atr)
(def stochastic simple/stochastic)
(def vwap simple/vwap)

Почему именно так? Давайте разбираться.

1. Контроль версий и стабильность API (Самое главное)

Внутренняя структура может меняться: Сегодня sma лежит в taljure.indicators.simple. Завтра я могу понять, что правильнее перенести его в taljure.indicators.core или даже оптимизировать и переписать с нуля в другом неймспейсе.

Если пользователь импортирует напрямую:

; У пользователя в коде:
(:require [taljure.indicators.simple :as simple])
(simple/sma data) ; ← Прямая привязка к внутренней структуре

Что происходит при изменении? Я ломаю обратную совместимость. Пользовательское приложение перестает компилироваться.

С моим подходом:

; У пользователя:
(:require [taljure.core :as t])
(t/sma data) ; ← Интерфейс стабилен

; А у меня внутри я могу менять реализацию:
(ns taljure.core
  (:require
   [taljure.indicators.v2.optimized :as optim])) ; ← Новый модуль!

(def sma optim/super-fast-sma) ; ← API не изменилось!

Выигрыш: Я могу радикально менять внутреннюю организацию кода, не ломая существующие приложения. taljure.core выступает как буфер совместимости.

2. Единая точка входа (Facade Pattern)

Я сознательно реализую шаблон «Фасад». Модуль taljure.core выступает единой точкой входа в библиотеку.

Преимущества:

  • Сокрытие сложности: Пользователю не нужно знать о внутренней структуре (indicators.simple, indicators.advanced и т.д.).
  • Гибкость развития: Я могу перекладывать реализации между внутренними модулями, не ломая API пользователей. Сегодня sma лежит в simple, завтра перенесу в core — и никто не заметит.
  • Простота документирования: Вся публичная API описана в одном месте.

3. Идиоматичный Clojure-подход

В Clojure принято предоставлять ключевые функции в корневом неймспейсе библиотеки. Посмотрите на популярные библиотеки:

; Пример с clojure.java-time
(:require [java-time :as jt])
(jt/local-date) ; а не jt.api/local-date

; Пример с ring
(:require [ring.adapter.jetty :as jetty])
; jetty/run-jetty, а не ring.adapters.jetty/run-jetty

Мой подход соответствует идиомам экосистемы Clojure.

4. Подготовка к расширению

Сейчас у меня только один модуль с индикаторами — simple. Но в будущем обязательно появятся:

  • taljure.indicators.advanced
  • taljure.indicators.volatility
  • taljure.signals
  • taljure.backtest

Плоская структура позволяет легко добавлять новые функции без усложнения API:

; Будущее расширение
(ns taljure.core
  (:require
   [taljure.indicators.simple :as simple]
   [taljure.indicators.advanced :as advanced]
   [taljure.signals :as signals]))

; Простые индикаторы
(def sma simple/sma)
(def ema simple/ema)

; Сложные индикаторы
(def macd advanced/macd)
(def bollinger-bands advanced/bollinger-bands)

; Сигналы
(def crossover signals/crossover)

Пользователь продолжит использовать taljure.core и не заметит изменений.

5. Контроль над публичным API

Явное объявление функций через def — это форма документирования намерений. Я показываю, какие функции являются стабильным публичным API, а какие — внутренними.

Если функция не экспортирована в core, значит она:

  • Нестабильна
  • Предназначена для внутреннего использования
  • Экспериментальна

Это защищает пользователей от случайного использования нестабильного API.

6. Производительность (миф и реальность)

Возможное возражение: "Но ведь это лишние вызовы def! Это медленнее!"

На практике:

  1. Время компиляции: Разница ничтожна — микросекунды.
  2. Время выполнения: Нулевая разница — JIT-компилятор оптимизирует такие вещи.
  3. Читаемость vs. микрооптимизация: Выигрыш в читаемости и поддерживаемости несоизмеримо важнее.

7. Альтернативы, которые я отверг

Я рассматривал другие варианты:

Автогенерация через :refer:

(ns taljure.core
  (:refer taljure.indicators.simple :all))

Недостаток: нет контроля над API, импортируется всё.

Макросы-генераторы:
Слишком сложно для такой простой задачи.

Многочисленные импорты пользователя:
Заставлять пользователя импортировать 10 неймспейсов — плохой UX.

Мое решение оказалось самым сбалансированным.

Заключение: Простота как результат сложной работы

Кажущаяся простота плоского API — это результат продуманного дизайна. Я сознательно пошел на:

  • Небольшое дублирование в виде def
  • Дополнительный уровень абстракции (фасад)
  • Явное объявление публичного API

Взамен я получаю:

  • ✅ Чистый и понятный API для пользователей
  • ✅ Гибкость для будущего развития библиотеки
  • ✅ Соответствие Clojure-идиомам
  • ✅ Контроль над стабильностью API

В следующей статье мы реализуем первый индикатор (SMA) и напишем тесты, которые подтвердят, что архитектура работает на практике.

А что вы думаете о таком подходе? Сталкивались ли с подобными архитектурными дилеммами в своих проектах?