Como o padrão de usar manipuladores de comando para lidar com persistência se encaixa em uma linguagem puramente funcional, na qual queremos tornar o código relacionado à IO o mais fino possível?
Ao implementar o Design Orientado a Domínio em uma linguagem orientada a objetos, é comum usar o padrão Comando / Manipulador para executar alterações de estado. Nesse design, os manipuladores de comando ficam sobre os objetos do domínio e são responsáveis pela lógica chata relacionada à persistência, como usar repositórios e publicar eventos do domínio. Os manipuladores são a face pública do seu modelo de domínio; código de aplicativo como a interface do usuário chama os manipuladores quando ele precisa alterar o estado dos objetos de domínio.
Um esboço em 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);
}
}
O document
objeto de domínio é responsável por implementar as regras de negócios (como "o usuário deve ter permissão para descartar o documento" ou "você não pode descartar um documento que já foi descartado") e por gerar os eventos de domínio que precisamos publicar ( document.NewEvents
seria ser um IEnumerable<Event>
e provavelmente conteria um DocumentDiscarded
evento).
Esse é um bom design - é fácil de estender (você pode adicionar novos casos de uso sem alterar o modelo de domínio, adicionando novos manipuladores de comando) e é independente da maneira como os objetos são persistidos (você pode facilmente trocar um repositório NHibernate por um Mongo repositório ou troque um editor RabbitMQ por um editor EventStore), o que facilita o teste usando falsificações e zombarias. Ele também obedece à separação de modelo / exibição - o manipulador de comandos não tem idéia se está sendo usado por um trabalho em lotes, uma GUI ou uma API REST.
Em uma linguagem puramente funcional como Haskell, você pode modelar o manipulador de comandos aproximadamente assim:
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"
Aqui está a parte que estou lutando para entender. Normalmente, haverá algum tipo de código de 'apresentação' que chama o manipulador de comandos, como uma GUI ou uma API REST. Portanto, agora temos duas camadas em nosso programa que precisam fazer IO - o manipulador de comandos e a exibição - o que é um grande não-não no Haskell.
Até onde eu entendi, existem duas forças opostas aqui: uma é a separação modelo / vista e a outra é a necessidade de persistir no modelo. É necessário haver código de IO para manter o modelo em algum lugar , mas a separação de modelo / exibição diz que não podemos colocá-lo na camada de apresentação com todos os outros códigos de IO.
Obviamente, em um idioma "normal", o IO pode (e acontece) em qualquer lugar. Um bom design determina que os diferentes tipos de E / S sejam mantidos separados, mas o compilador não o impõe.
Então: como reconciliar o modelo / visualizar a separação com o desejo de levar o código de E / S até a borda do programa, quando o modelo precisa ser persistido? Como mantemos os dois tipos diferentes de E / S separados , mas ainda longe de todo o código puro?
Atualização : A recompensa expira em menos de 24 horas. Não acho que nenhuma das respostas atuais tenha respondido à minha pergunta. O comentário de @ Ptharien's Flame sobre acid-state
parece promissor, mas não é uma resposta e está faltando detalhes. Eu odiaria que esses pontos fossem desperdiçados!
acid-state
parece ótimo, obrigado por esse link. Em termos de design da API, ainda parece estar vinculado IO
; minha pergunta é sobre como uma estrutura de persistência se encaixa em uma arquitetura maior. Você conhece algum aplicativo de código aberto usado acid-state
junto com uma camada de apresentação e consegue manter os dois separados?
Query
e Update
são bastante distantes IO
, na verdade. Vou tentar dar um exemplo simples em uma resposta.
acid-state
parece estar perto do que você está descrevendo .