W jaki sposób wzorzec używania programów obsługi poleceń do radzenia sobie z trwałością pasuje do czysto funkcjonalnego języka, w którym chcemy, aby kod związany z IO był jak najcieńszy?
Podczas implementowania projektowania opartego na domenie w języku obiektowym często stosuje się wzorzec polecenia / procedury obsługi do wykonywania zmian stanu. W tym projekcie programy obsługi poleceń znajdują się nad obiektami domeny i są odpowiedzialne za nudną logikę związaną z trwałością, taką jak używanie repozytoriów i publikowanie zdarzeń domeny. Procedury obsługi są publiczną twarzą Twojego modelu domeny; kod aplikacji, taki jak interfejs użytkownika, wywołuje programy obsługi, gdy musi zmienić stan obiektów domeny.
Szkic w 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
Obiekt domena jest odpowiedzialna za wdrażanie reguł biznesowych (takich jak „użytkownik powinien mieć uprawnienie do odrzucenia dokumentu” lub „nie można odrzucić dokument, który już został odrzucony”) oraz do generowania zdarzeń domen musimy publikować ( document.NewEvents
będzie być IEnumerable<Event>
i prawdopodobnie zawiera DocumentDiscarded
zdarzenie).
Jest to ładny projekt - można go łatwo rozszerzyć (można dodawać nowe przypadki użycia bez zmiany modelu domeny, dodając nowe programy obsługi poleceń) i jest agnostyczny, jeśli chodzi o sposób utrwalania obiektów (można łatwo wymienić repozytorium NHibernate dla Mongo repozytorium lub zamień wydawcę RabbitMQ na wydawcę EventStore), co ułatwia testowanie przy użyciu podróbek i prób. Przestrzega także separacji modelu / widoku - moduł obsługi poleceń nie ma pojęcia, czy jest używany przez zadanie wsadowe, interfejs GUI, czy interfejs API REST.
W czysto funkcjonalnym języku, takim jak Haskell, możesz modelować moduł obsługi poleceń mniej więcej tak:
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"
Oto część, którą staram się zrozumieć. Zazwyczaj istnieje jakiś rodzaj kodu „prezentacji”, który wywołuje moduł obsługi poleceń, na przykład GUI lub interfejs API REST. Więc teraz mamy w naszym programie dwie warstwy, które muszą wykonywać operacje wejścia / wyjścia - moduł obsługi poleceń i widok - co w Haskell jest dużym zakazem.
O ile mi wiadomo, istnieją tutaj dwie przeciwstawne siły: jedna to separacja modelu od widoku, a druga to potrzeba zachowania modelu. Musi istnieć kod IO, aby gdzieś utrwalić model , ale separacja modelu / widoku mówi, że nie możemy umieścić go w warstwie prezentacji z całym innym kodem IO.
Oczywiście w „normalnym” języku IO może (i tak się dzieje) wszędzie. Dobry projekt nakazuje, aby różne typy IO były oddzielone, ale kompilator tego nie wymusza.
Więc: jak pogodzić separację modelu / widoku z chęcią przesunięcia kodu IO na samą krawędź programu, kiedy model musi zostać utrwalony? Jak utrzymywać dwa różne typy IO osobno , ale wciąż z dala od całego czystego kodu?
Aktualizacja : Nagroda wygasa za mniej niż 24 godziny. Nie wydaje mi się, aby którakolwiek z obecnych odpowiedzi w ogóle odnosiła się do mojego pytania. @ Ptharien's Flame komentarz na temat acid-state
wydaje się obiecujący, ale nie jest to odpowiedź i brakuje w niej szczegółów. Nie chciałbym, żeby te punkty zmarnowały się!
acid-state
wygląda całkiem świetnie, dzięki za ten link. Jeśli chodzi o projekt interfejsu API, nadal wydaje się, że jest to związane IO
; moje pytanie dotyczy tego, jak struktura trwałości pasuje do większej architektury. Czy znasz jakieś aplikacje typu open source, które używają acid-state
obok warstwy prezentacji, i udało Ci się je rozdzielić?
Query
i Update
monady są dość dalekie IO
. Spróbuję podać prosty przykład w odpowiedzi.
acid-state
wydaje się być zbliżony do tego, co opisujesz .