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