Библиотека для трейдинга: архитектура
Архитектура Clojure-библиотеки: Почему я выбрал плоскую структуру неймспейсов
В предыдущих статьях я показывал, как настроить окружение и структуру проекта для библиотеки алготрейдинга 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
. Но в будущем обязательно появятся:
Плоская структура позволяет легко добавлять новые функции без усложнения 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
! Это медленнее!"
- Время компиляции: Разница ничтожна — микросекунды.
- Время выполнения: Нулевая разница — JIT-компилятор оптимизирует такие вещи.
- Читаемость vs. микрооптимизация: Выигрыш в читаемости и поддерживаемости несоизмеримо важнее.
7. Альтернативы, которые я отверг
Я рассматривал другие варианты:
(ns taljure.core (:refer taljure.indicators.simple :all))
Недостаток: нет контроля над API, импортируется всё.
Макросы-генераторы:
Слишком сложно для такой простой задачи.
Многочисленные импорты пользователя:
Заставлять пользователя импортировать 10 неймспейсов — плохой UX.
Мое решение оказалось самым сбалансированным.
Заключение: Простота как результат сложной работы
Кажущаяся простота плоского API — это результат продуманного дизайна. Я сознательно пошел на:
- Небольшое дублирование в виде
def
- Дополнительный уровень абстракции (фасад)
- Явное объявление публичного API
- ✅ Чистый и понятный API для пользователей
- ✅ Гибкость для будущего развития библиотеки
- ✅ Соответствие Clojure-идиомам
- ✅ Контроль над стабильностью API
В следующей статье мы реализуем первый индикатор (SMA) и напишем тесты, которые подтвердят, что архитектура работает на практике.
А что вы думаете о таком подходе? Сталкивались ли с подобными архитектурными дилеммами в своих проектах?