Одной из основных архитектурных целей при проектировании больших приложений является уменьшение связанности и зависимостей. Под зависимостями я подразумеваю зависимости исходного кода, когда одна функция или тип данных использует другую функцию или другой тип. Руководство по архитектуре высокого уровня, по-видимому, выглядит так: >архитектура портов и адаптеров с небольшими вариациями, также называемая луковой архитектурой, Гексагональная архитектура или Чистая архитектура: типы и функции, моделирующие домен em> приложения находятся в центре, затем следуют варианты использования, которые предоставляют полезные услуги на основе домена, а в самом дальнем кольце находятся технические аспекты, такие как постоянство, сеть и пользовательский интерфейс.
Правило зависимостей гласит, что зависимости должны указывать только внутрь. Например.; постоянство может зависеть от функций и типов из вариантов использования, а варианты использования могут зависеть от функций и типов из предметной области. Но домен не может зависеть от внешних колец. Как мне реализовать такую архитектуру в Haskell? Чтобы конкретизировать: как я могу реализовать модуль варианта использования, который не зависит (= импортирует) функции и типы от модуля постоянства, даже если ему необходимо извлекать и хранить данные?
Скажем, я хочу реализовать вариант использования размещение заказа с помощью функции U.placeOrder :: D.Customer -> [D.LineItem] -> IO U.OrderPlacementResult
, которая создает заказ из позиций и пытается сохранить заказ. Здесь U
указывает на модуль вариантов использования, а D
— на модуль домена. Функция возвращает действие ввода-вывода, потому что ей каким-то образом нужно сохранить заказ. Однако само постоянство находится в самом внешнем архитектурном кольце — реализовано в каком-то модуле P
; поэтому указанная выше функция не должна зависеть ни от чего, экспортируемого из P
.
Я могу представить два общих решения:
- Функции высшего порядка: функция
U.placeOrder
принимает дополнительный аргумент функции, скажем,U.OrderDto -> U.PersistenceResult
. Эта функция реализована в модуле persistence (P
), но она зависит от типов модуляU
, тогда как для модуляU
не нужно объявлять зависимость отP
. - Классы типов: Модуль
U
определяет класс типовPersistence
, который объявляет указанную выше функцию. МодульP
зависит от этого класса типов и предоставляет для него экземпляр.
Вариант 1 довольно явный, но не очень общий. Потенциально это приводит к функциям со многими аргументами. Вариант 2 менее подробный (см., например, здесь). Однако вариант 2 приводит ко множеству беспринципных классов типов, что считается плохой практикой в большинстве современных учебников и руководств по Haskell.
Итак, у меня осталось два вопроса:
- Я пропустил другие альтернативы?
- Какой подход обычно рекомендуется, если таковой имеется?