Як наполегливість вписується в суто функціональну мову?


18

Як модель використання командних обробників для боротьби зі стійкістю вписується в суто функціональну мову, де ми хочемо зробити код, пов'язаний з IO, максимально тонким?


Під час реалізації дизайну, керованого доменом, на об'єктно-орієнтованій мові, звичайно використовувати шаблон Command / Handler для виконання змін стану. У цьому дизайні обробники команд сидять над об’єктами вашого домену та відповідають за нудну логіку, пов’язану з постійністю, як-от використання сховищ та публікація подій домену. Обробники - це публічне обличчя вашої моделі домену; код програми, як інтерфейс, викликає обробники, коли йому потрібно змінити стан об'єктів домену.

Ескіз на C #:

public class DiscardDraftDocumentCommandHandler : CommandHandler<DiscardDraftDocument>
{
    IDraftDocumentRepository _repo;
    IEventPublisher _publisher;

    public DiscardDraftCommandHandler(IDraftDocumentRepository repo, IEventPublisher publisher)
    {
        _repo = repo;
        _publisher = publisher;
    }

    public override void Handle(DiscardDraftDocument command)
    {
        var document = _repo.Get(command.DocumentId);
        document.Discard(command.UserId);
        _publisher.Publish(document.NewEvents);
    }
}

documentОб'єкт домену несе відповідальність за виконання бізнес - правил (наприклад , «користувач повинен мати дозволу на відкидати документ» або «ви не можете відмовитися від документа , який вже був відкидається») і для генерації події домену ми повинні опублікувати ( document.NewEventsбуде бути IEnumerable<Event>і, ймовірно, міститиме DocumentDiscardedподія).

Це приємний дизайн - його легко розширити (ви можете додавати нові випадки використання, не змінюючи модель домену, додаючи нові обробники команд) та є агностичним щодо того, як об’єкти зберігаються (ви можете легко замінити сховище NHibernate на монго сховище або замінити видавця RabbitMQ на видавця EventStore), що спрощує тестування за допомогою підробок і макетів. Він також підпорядковується розділенню моделі / перегляду - обробник команд не має поняття, використовується він пакетним завданням, графічним інтерфейсом або API REST.


На чисто функціональній мові, як Haskell, ви можете моделювати обробник команд приблизно так:

newtype CommandHandler = CommandHandler {handleCommand :: Command -> IO Result)
data Result a = Success a | Failure Reason
type Reason = String

discardDraftDocumentCommandHandler = CommandHandler handle
    where handle (DiscardDraftDocument documentID userID) = do
              document <- loadDocument documentID
              let result = discard document userID :: Result [Event]
              case result of
                   Success events -> publishEvents events >> return result
                   -- in an event-sourced model, there's no extra step to save the document
                   Failure _ -> return result
          handle _ = return $ Failure "I expected a DiscardDraftDocument command"

Ось частина, яку я намагаюся зрозуміти. Як правило, буде якийсь код "презентації", який викликає обробник команд, як GUI або REST API. Отже, зараз у нашій програмі є два шари, яким потрібно виконати IO - обробник команд та view - що в Haskell є великим "no-no".

Наскільки я можу розібратися, тут є дві протилежні сили: одна - це модель / розділення перегляду, а друга - необхідність зберігати модель. Потрібно мати код IO, щоб десь зберігати модель , але розділення моделі / перегляду говорить про те, що ми не можемо помістити її у презентаційний шар разом з усіма іншими кодами IO.

Звичайно, в "нормальній" мові IO може (і справді) траплятися де завгодно. Хороший дизайн передбачає, що різні типи вводу-виводу повинні зберігатись окремо, але компілятор не застосовує їх.

Отже: як ми можемо узгодити поділ моделі / перегляду з бажанням підштовхнути код IO до самого краю програми, коли модель потребує збереження? Як ми можемо тримати два різних типу вводу-виводу окремо , але все ще далеко від чистого коду?


Оновлення : виплата закінчується менш ніж за 24 години. Я не відчуваю, що жодна з нинішніх відповідей взагалі не стосується мого питання. @ Коментар полум’я Птарієна про acid-stateздається багатообіцяючим, але це не відповідь і його бракує докладно. Мені б не хотілося, щоб ці моменти пішли на сміття!


1
Можливо, було б корисно подивитися на дизайн різних бібліотек, що зберігаються в Haskell; зокрема, acid-stateздається, близький до того, що ви описуєте .
Полум'я Птарієна

1
acid-stateвиглядає досить чудово, дякую за це посилання. Що стосується дизайну API, він все ще, здається, зобов'язаний IO; моє запитання - про те, як система стійкості вписується в більшу архітектуру. Чи знаєте ви про будь-які програми з відкритим кодом, які використовують acid-stateпоруч із презентаційним шаром, і їм вдається зберегти їх окремо?
Бенджамін Ходжсон

QueryІ Updateмонади досить далеко від IO, на самому ділі. Спробую дати простий приклад у відповідь.
Полум'я Птарієна

Ризикуючи бути поза темою, будь-яким читачам, які таким чином використовують шаблон Command / Handler, я дуже рекомендую перевірити Akka.NET. Тут акторська модель відчуває себе добре. Існує чудовий курс для Pluralsight. (Клянусь, я просто фанат, а не промо-бот.)
RJB

Відповіді:


6

Загальний спосіб відокремлення компонентів у Haskell - це через стеки трансформаторів. Я пояснюю це більш докладно нижче.

Уявіть, що ми будуємо систему з кількома масштабними компонентами:

  • компонент, який спілкується з диском або базою даних (підмодель)
  • компонент, який робить перетворення в нашому домені (модель)
  • компонент, який взаємодіє з користувачем (перегляд)
  • компонент, який описує зв'язок між представленням, моделлю та підмоделем (контролером)
  • компонент, який запускає всю систему (драйвер)

Ми вирішуємо, що нам потрібно тримати ці компоненти в поєднанні, щоб підтримувати хороший стиль коду.

Тому ми кодуємо кожен наш компонент поліморфно, використовуючи різні класи MTL для керівництва нами:

  • кожна функція в підмоделі має тип MonadState DataState m => Foo -> Bar -> ... -> m Baz
    • DataState - це чисте зображення короткого огляду стану нашої бази даних або сховища
  • кожна функція в моделі є чистою
  • кожна функція у представленні має тип MonadState UIState m => Foo -> Bar -> ... -> m Baz
    • UIState - це чисте подання огляду стану нашого користувальницького інтерфейсу
  • кожна функція в контролері має тип MonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
    • Зауважте, що контролер має доступ як до стану перегляду, так і до стану підмоделі
  • у драйвера є лише одне визначення, main :: IO ()яке виконує майже тривіальну роботу щодо об'єднання інших компонентів в одну систему
    • вигляд і підмодель потрібно буде підняти в той же тип стану, що і контролер, zoomабо аналогічний комбінатор
    • модель чиста, і тому її можна використовувати без обмежень
    • Зрештою, все живе (тип, сумісний із) StateT (DataState, UIState) IO, який потім запускається з фактичним вмістом бази даних чи сховищем для створення IO.

1
Це відмінна порада, і саме те, що я шукав. Спасибі!
Бенджамін Ходжсон

2
Я перетравлюю цю відповідь. Скажіть, будь ласка, роль "підмоделі" в цій архітектурі? Як це "розмовляти з диском або базою даних" без виконання IO? Мене особливо бентежить те, що ви маєте на увазі під " DataStateчистим зображенням огляду стану нашої бази даних чи сховища". Імовірно, ви не маєте намір завантажувати всю базу даних у пам'ять!
Бенджамін Ходжсон

1
Я б дуже хотів побачити ваші думки щодо C # реалізації цієї логіки. Не гадаю, що я можу підкупити вас обнародуванням? ;-)
RJB

1
@RJB На жаль, вам доведеться підкупити команду розробників C #, щоб дозволити вищі мови в мові, тому що без них ця архітектура стає дещо рівною.
Полум’я Птарієна

4

Отже: як ми можемо узгодити поділ моделі / перегляду з бажанням підштовхнути код IO до самого краю програми, коли модель потребує збереження?

Чи слід зберігати модель? У багатьох програмах збереження моделі потрібно, оскільки стан непередбачуваний; будь-яка операція може будь-яким чином мутувати модель, тому єдиний спосіб дізнатися про стан моделі - це отримати прямий доступ до неї.

Якщо у вашому сценарії послідовність подій (команди, які були затверджені та прийняті), завжди може генерувати стан, то це потрібно, щоб вони були збережені, а не обов'язково. Стан завжди можна створити, відтворюючи події.

Сказавши це, часто стан зберігається, але просто як знімок / кеш, щоб уникнути відтворення команд, а не як важливих даних програми.

Отже, зараз у нашій програмі є два шари, яким потрібно виконати IO - обробник команд та view - що в Haskell є великим "no-no".

Після того, як команда буде прийнята, подія передається двом напрямкам (сховище події та система звітності), але на одному і тому ж рівні програми.

Дивіться також
Вивільнення подій з
нетерплячим результатом читання


2
Я знайомий з джерелом подій (я використовую це в своєму прикладі вище!), І щоб уникнути розщеплення волосся, я все одно скажу, що пошук подій є підходом до проблеми постійності. У будь-якому випадку, пошук подій не усуває необхідності завантажувати доменні об’єкти в обробник команд . Обробник команд не знає, чи потрапили об'єкти з потоку подій, ORM чи збереженої процедури - він просто отримує його з сховища.
Бенджамін Ходжсон

1
Здається, ваше розуміння поєднує погляд і обробник команд разом, щоб створити кілька вводу-виводу. Я розумію, що обробник породжує подію і більше не цікавиться. Перегляд у цьому випадку функціонує як окремий модуль (навіть якщо технічно в одній програмі) і не пов'язаний з обробником команд.
FMJaguar

1
Я думаю, що ми можемо говорити з перехресними цілями. Коли я кажу "перегляд", я кажу про весь рівень презентації, який може бути API REST або система контролера перегляду моделей. (Я погоджуюсь, що погляд повинен бути відокремлений від моделі в шаблоні MVC.) Я в основному маю на увазі "все, що викликає в обробник команд".
Бенджамін Ходжсон

2

Ви намагаєтесь вкласти простір у своє інтенсивне додаток для всіх операцій, не пов'язаних з IO; на жаль, типові додатки CRUD, як ви говорите, мало чого, крім IO.

Я думаю, ви добре розумієте відповідне розділення, але там, де ви намагаєтесь розмістити код постійного вводу-виводу на деяку кількість шарів подалі від коду презентації, загальний факт цього питання знаходиться у вашому контролері десь вам слід дзвонити до свого рівень стійкості, який може бути занадто близьким до вашої презентації - але це лише збіг у такому типі додатків.

Презентація та наполегливість складають в основному весь тип програми, я думаю, ви тут описуєте.

Якщо ви думаєте в голові про подібний додаток, в якому було багато складної бізнес-логіки та обробки даних, я думаю, ви зможете уявити, як це чудово відокремитись від презентаційного IO та наполегливого IO, такого, що про це не потрібно нічого знати. Проблема, яка у вас зараз є, є лише перцептивною проблемою, викликаною спробою розв'язати проблему у типі додатків, у яких ця проблема не повинна починатися.


1
Ви говорите, що нормально, щоб системи CRUD поєднували наполегливість та уявлення. Мені це здається розумним; однак я не згадував CRUD. Я спеціально запитую про DDD, де у вас є бізнес-об'єкти, які мають складні взаємодії, стійкий шар (обробники команд) та презентаційний шар. Як ви зберігаєте два шари введення-виведення, зберігаючи тонку IO-обгортку?
Бенджамін Ходжсон

1
NB: Домен, який я описав у запитанні, може бути дуже складним. Можливо, відмова від проекту документа підлягає певній участі в перевірці дозволів, або може знадобитися розробка декількох версій одного чернетка, або потрібно надсилати сповіщення, або дія потребує схвалення іншого користувача, або чернетки проходять через ряд Етапи життєвого циклу до фіналізації ...
Бенджамін Ходжсон

2
@BenjaminHodgson Я б настійно радив не змішувати DDD або інші притаманні методології проектування OO в цю ситуацію в вашій голові, це лише заплутається. Хоча так, ви можете створювати такі об'єкти, як біти та bobbles в чистому ПЗ, підходи до проектування, засновані на них, не обов'язково повинні бути вашим першим досягненням. У описаному вами сценарії я б передбачив, як я вже згадував вище, контролер, який спілкується між двома введеннями та чистим кодом: презентація IO входить і вимагається від контролера, контролер передає речі в чисті розділи та в розділи постійності.
Джиммі Хоффа

1
@BenjaminHodgson ви можете уявити міхур, де живе весь ваш чистий код, з усіма шарами та вигадливістю, які ви можете захотіти в будь-якому дизайні, який ви цінуєте. Точкою входу для цієї бульбашки буде крихітний фрагмент, який я називаю "контролером" (можливо, неправильно), який здійснює зв'язок між презентацією, наполегливістю та чистими фрагментами. Таким чином, ваша наполегливість не знає нічого презентаційного чи чистого, і навпаки - і це зберігає ваші IO речі в цьому тонкому шарі над бульбашкою вашої чистої системи.
Джиммі Хоффа

2
@ BenjaminHodgson цей підхід "розумних об'єктів", про який ви говорите, по суті є поганим підходом для FP, проблема з розумними об'єктами в FP полягає в тому, що вони парують занадто багато і узагальнюють занадто мало. У кінцевому підсумку ви пов'язані з ними дані та функціональні можливості, в яких FP вважає за краще, щоб ваші дані мали слабке з'єднання з функціональністю, щоб ви могли реалізовувати свої функції для узагальнення, а потім вони працюватимуть у кількох типах даних. Прочитайте мою відповідь тут: programmers.stackexchange.com/questions/203077/203082#203082
Джиммі Хоффа

1

Якнайближче я можу зрозуміти ваше запитання (яке я, можливо, не вважаю, але я подумав, що я б кинув свої 2 центи), оскільки вам не обов’язково мати доступ до самих об'єктів, вам потрібно мати власну базу даних об’єктів, яка самостійно, закінчується з часом).

В ідеалі самі об'єкти можуть бути покращені для зберігання свого стану, тому коли їх "пройдуть", різні процесори команд будуть знати, з чим вони працюють.

Якщо це неможливо (icky icky), єдиний спосіб - це мати загальний ключ, схожий на БД, який можна використовувати для зберігання інформації в магазині, який налаштовано, щоб бути доступним для різних команд - і, сподіваємось, "відкрити" інтерфейс та / або код, тому будь-які інші автори команд також приймуть ваш інтерфейс для збереження та обробки метаінформації.

В області файлових серверів samba має різні способи зберігання таких речей, як списки доступу та альтернативні потоки даних, залежно від того, що надає хост-операційна система. В ідеалі, розміщення самби у файловій системі надає розширені атрибути файлам. Приклад "xfs" на "linux" - більше команд копіює розширені атрибути разом з файлом (за замовчуванням більшість утиліт на Linux "виросли", не вважаючи розширеними атрибутами).

Альтернативним рішенням, яке працює для декількох процесів samba від різних користувачів, що працюють над загальними файлами (об'єктами), є те, що якщо файлова система не підтримує приєднання ресурсу безпосередньо до файлу, як із розширеними атрибутами, використовує модуль, який реалізує рівень віртуальної файлової системи для емуляції розширених атрибутів для процесів samba. Про це знає лише самба, але вона має перевагу працювати, коли формат об'єкта не підтримує його, але все ще працює з різноманітними користувачами samba (пор. Командні процесори), які виконують певну роботу над файлом на основі його попереднього стану. Він зберігатиме метаінформацію у загальній базі даних для файлової системи, що допомагає контролювати розмір бази даних (і не '

Можливо, вам це не стане в нагоді, якщо вам потрібна додаткова інформація, конкретна для реалізації, з якою ви працюєте, але концептуально ця теорія може бути застосована до обох наборів проблем. Тож якщо ви шукали алгоритми та методи робити те, що ви хочете, це може допомогти. Якщо вам потрібні більш конкретні знання в якихось конкретних рамках, то, можливо, не так корисно ... ;-)

BTW - причина, про яку я згадую про "самоздатність", полягає в тому, що її не зрозуміло, якщо ви знаєте, які об’єкти там і як довго вони зберігаються. Якщо у вас немає прямого способу дізнатися, коли об’єкт видалено, вам доведеться обрізати власну метаDB, щоб не заповнити стару або давню метаінформацію, для якої користувачі давно видалили об'єкти.

Якщо ви знаєте, коли термін придатності чи видалення об’єктів закінчується, то ви випереджаєте гру та можете одночасно вимкнути її з метаДБ, але не було зрозуміло, чи була у вас така опція.

Ура!


1
Мені це здається відповіддю на зовсім інше питання. Я шукав поради щодо архітектури в чисто функціональному програмуванні в контексті доменного дизайну. Чи можете ви уточнити свої питання, будь ласка?
Бенджамін Ходжсон

Ви запитуєте про збереження даних у суто функціональній парадигмі програмування. Цитуючи Вікіпедію: "Чисто функціональний - це термін у обчислювальних технологіях, що використовується для опису алгоритмів, структур даних або мов програмування, які виключають руйнівні модифікації (оновлення) сутностей у середовищі виконання програми". ==== За визначенням, збереження даних не має значення і не має користі для чогось, що не змінює даних. Строго кажучи, немає відповіді на ваше запитання. Я робив спробу більш слабкої інтерпретації того, що ви написали.
Астара
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.