sirewix / jsonserver.hs

Simple rest api json server in haskell
BSD 3-Clause "New" or "Revised" License
0 stars 0 forks source link

Продумать архитектуру проекта #1

Open stanislav-az opened 4 years ago

stanislav-az commented 4 years ago

Сейчас не очень понятно устройство приложения, так как все модули в одной папке. Нужно выделить ядро, чтобы была ясна основная цель приложения, а также инфраструктурные слои, которые занимаются доступом к базе данных, и т.д. В целом, вертикальное и горизонтальное разделение смешано, или его нет. Можно почитать статью по теме, если не очень понятно что требуется http://aspiringcraftsman.com/2008/01/03/art-of-separation-of-concerns/

sirewix commented 4 years ago

Придумывать названия очень сложно. Разделять целые модули чтобы избежать циклических зависимостей и orphaned instances, придумывая в какой модуль и в какую папку это закинуть -- тоже сложно. Совершенно не уверен в исправленной мною структуре и нейминге модулей, тем не менее выделил два основных слоя: App.Prototype, который содержит ядро (интерфейс) и App.Implementation с реализацией. Все json api методы переместил в API.*. Теперь вместо IO везде mtl-style монада (ReaderT + ExceptT)

stanislav-az commented 4 years ago

В целом все хорошо, над названиями можно поработать, ага :) Еще бы Entities я бы не сваливал в один модуль, плюс обычно сущности доменки это часть ядра, причем самая центральная.

kelizarov commented 4 years ago

Скорее надо подумать как можно разделить работу АПИ от работы с базой данных. В текущем варианте получается что эндпойнты прямо совершают действия на базами данными и тут же возвращают джейсончик ее клиенту. Проекту явно не хватает слой бизнес логики, в которой бы было описаны чистые сущности (юзеру, посты, авторы итд). Имея чистые сущности можно было написать мапперы для АПИшки и для БД представлений, таким образом код можно будет легче понять :)

sirewix commented 4 years ago

Еще бы Entities я бы не сваливал в один модуль, плюс обычно сущности доменки это часть ядра, причем самая центральная.

В моем случае, название модуля, вероятно, не совсем верное, потому что единственная цель содержащихся в нем типов -- парсинг query параметров. Я вижу тут два варианта:

  1. Раскидать по тематике (авторы, теги, новости, ...) в API.*, или в отдельный модуль QueryParams.* (или как то так), с зеркальной к API.* структурой. Проблема в том, что некоторые параметры используются не только в одном модуле (Page, Token), поэтому изначально и свалил все в единый модуль со всеми типами (Entities)
  2. На каждый тип по модулю в QueryParams.* (или как то так). В целом, наверно, это лучший вариант, но не уверен

Относить эти типы к ядру (по крайней мере к самой его основе) я не совсем считаю обоснованным, потому что там хранится как бы "движок програмы", а что с его помощью написано это уже другой архитектурный слой (к которому относятся типы из Entities, все эндпоинты и Entry)

Скорее надо подумать как можно разделить работу АПИ от работы с базой данных. В текущем варианте получается что эндпойнты прямо совершают действия на базами данными и тут же возвращают джейсончик ее клиенту. Проекту явно не хватает слой бизнес логики, в которой бы было описаны чистые сущности (юзеру, посты, авторы итд). Имея чистые сущности можно было написать мапперы для АПИшки и для БД представлений, таким образом код можно будет легче понять :)

Основная проблема у меня была вроде в том, что при возврате вложенной структуры придется ее "выпрямлять" (возможно я выбрал плохую либу для sql (postres-simple)), что не очень удобно, а в случае с массивом произвольной длины бывает и невозможно (например, категория в посте это массив произвольной длины (от рут категории, до текущей), при этом каждый элемент массива это пара из category id и имени, я это десериализовать удобно не смог). Поэтому я от этого отказался в пользу сериализации в json на стороне бд.

Вообще, немножко плевался с sql'я, когда писал, потому что чтобы хорошо абстрагироваться, придется уйти в кодогенерацию sql'я, что чревато нетестируемыми багами. Ну и с вложенностями очень неудобно работать

Есть конечно вариант джоинить все и уже на стороне сервера выяснять что к чему относится, в массивы собирать, но это как то не очень звучит

kelizarov commented 4 years ago

Советую попробовать разделить типы на слои приложения. Обычно это может выглядить так: API -> Domain -> Storage. И для каждого слоя используются свои типы для взаимодействия. Например для слоя API могут типы которые работают с квери парамертами. Storage занимался бы обычным представлением в базе данным, а Domain это уже не посредственно типы для основной логики приложения. Ну и так же между этими слоями нужно будет мапать между собой. :)

sirewix commented 4 years ago

Добавил Model.* параллельно API.*. Получается что API это слой API -> Domain, а Model это Domain -> Storage. Правда, в API все равно часто реюзал сущности из Model (заворачивая в newtype) просто потому что они идентичные. В целом каждая сущность имеет три три представления: Essential для создания, Partial для редактирования и Full полная структура с вложенностями. Для первых двух определяется только ToRow, для Full только FromJSON и ToJSON. Как одним sql запросом получать вложенную структуру без аггрегирования в json я так и не разобрался, поэтому оставил это как было.