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!
fonte
acid-state
parece estar perto do que você está descrevendo .acid-state
parece ótimo, obrigado por esse link. Em termos de design da API, ainda parece estar vinculadoIO
; minha pergunta é sobre como uma estrutura de persistência se encaixa em uma arquitetura maior. Você conhece algum aplicativo de código aberto usadoacid-state
junto com uma camada de apresentação e consegue manter os dois separados?Query
eUpdate
são bastante distantesIO
, na verdade. Vou tentar dar um exemplo simples em uma resposta.Respostas:
A maneira geral de separar componentes no Haskell é através de pilhas de transformadores de mônada. Eu explico isso com mais detalhes abaixo.
Imagine que estamos construindo um sistema que possui vários componentes de grande escala:
Decidimos que precisamos manter esses componentes fracamente acoplados para manter um bom estilo de código.
Portanto, codificamos cada um de nossos componentes polimorficamente, usando as várias classes MTL para nos guiar:
MonadState DataState m => Foo -> Bar -> ... -> m Baz
DataState
é uma representação pura de um instantâneo do estado do nosso banco de dados ou armazenamentoMonadState UIState m => Foo -> Bar -> ... -> m Baz
UIState
é uma representação pura de um instantâneo do estado da nossa interface de usuárioMonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
main :: IO ()
que faz o trabalho quase trivial de combinar os outros componentes em um sistemazoom
ou um combinador semelhanteStateT (DataState, UIState) IO
, que é executado com o conteúdo real do banco de dados ou armazenamento a ser produzidoIO
.fonte
DataState
é uma representação pura de um instantâneo do estado do nosso banco de dados ou armazenamento". Presumivelmente, você não pretende carregar todo o banco de dados na memória!O modelo deve ser persistido? Em muitos programas, é necessário salvar o modelo porque o estado é imprevisível, qualquer operação pode alterar o modelo de qualquer forma, portanto, a única maneira de conhecer o estado do modelo é acessá-lo diretamente.
Se, no seu cenário, a sequência de eventos (comandos que foram validados e aceitos) sempre pode gerar o estado, são os eventos que precisam ser persistidos, não necessariamente o estado. O estado sempre pode ser gerado reproduzindo os eventos.
Dito isto, muitas vezes o estado é armazenado, mas apenas como um instantâneo / cache para evitar a repetição dos comandos, não como dados essenciais do programa.
Depois que o comando é aceito, o evento é comunicado a dois destinos (o armazenamento de eventos e o sistema de relatórios), mas na mesma camada do programa.
Consulte também Derivação de leitura ansiosa de
fornecimento de eventos
fonte
Você está tentando colocar espaço em seu aplicativo intensivo de E / S para todas as atividades que não sejam de E / S; infelizmente, aplicativos CRUD típicos como o que você fala fazem pouco além de IO.
Eu acho que você entende bem a separação relevante, mas onde você está tentando colocar o código de persistência IO em um número de camadas distante do código de apresentação, o fato geral da questão está no seu controlador em algum lugar em que você deveria estar chamando o seu camada de persistência, que pode parecer muito próxima da sua apresentação para você - mas isso é apenas uma coincidência nesse tipo de aplicativo.
Apresentação e persistência compõem basicamente o tipo de aplicativo que você está descrevendo aqui.
Se você pensa em um aplicativo semelhante com muita lógica de negócios e processamento de dados complexos, acho que conseguirá imaginar como isso é bem separado das coisas de IO de apresentação e IO de persistência, de modo que também não precisa saber nada. O problema que você tem agora é apenas um perceptivo causado pela tentativa de encontrar uma solução para um problema em um tipo de aplicativo que não possui esse problema para começar.
fonte
O mais próximo que eu puder entender sua pergunta (o que talvez não seja, mas pensei em gastar 2 centavos), já que você não necessariamente tem acesso aos objetos em si, precisa ter seu próprio banco de dados de objetos que expira com o tempo).
Idealmente, os próprios objetos podem ser aprimorados para armazenar seu estado; assim, quando eles são "repassados", diferentes processadores de comando saberão com o que estão trabalhando.
Se isso não for possível (icky icky), a única maneira é ter uma chave comum do tipo DB, que você pode usar para armazenar as informações em uma loja configurada para ser compartilhável entre diferentes comandos - e, esperançosamente, "abra" a interface e / ou o código para que outros escritores de comando também adotem sua interface para salvar e processar meta-informações.
Na área de servidores de arquivos, o samba tem maneiras diferentes de armazenar itens como listas de acesso e fluxos de dados alternativos, dependendo do que o sistema operacional host fornece. Idealmente, o samba está sendo hospedado em um sistema de arquivos e fornece atributos estendidos nos arquivos. Exemplo 'xfs' no 'linux' - mais comandos estão copiando atributos estendidos junto com um arquivo (por padrão, a maioria dos utilitários no linux "cresceu" sem os atributos estendidos).
Uma solução alternativa - que funciona para vários processos samba de diferentes usuários que operam em arquivos (objetos) comuns, é que, se o sistema de arquivos não suportar a conexão direta do recurso ao arquivo, como nos atributos estendidos, está usando um módulo que implementa uma camada de sistema de arquivos virtual para emular atributos estendidos para processos de samba. Somente o samba sabe disso, mas tem a vantagem de trabalhar quando o formato do objeto não o suporta, mas ainda trabalha com diversos usuários do samba (cf. processadores de comando) que trabalham no arquivo com base em seu estado anterior. Ele armazenará as meta informações em um banco de dados comum para o sistema de arquivos, o que ajuda a controlar o tamanho do banco de dados (e não
Pode não ser útil se você precisar de mais informações específicas para a implementação com a qual está trabalhando, mas conceitualmente, a mesma teoria pode ser aplicada aos dois conjuntos de problemas. Portanto, se você estava procurando algoritmos e métodos para fazer o que deseja, isso pode ajudar. Se você precisava de conhecimento mais específico em alguma estrutura específica, talvez não seja tão útil ... ;-)
Entre - a razão pela qual menciono 'auto-expiração' - é que não está claro se você sabe quais objetos estão lá fora e por quanto tempo eles persistem. Se você não tem uma maneira direta de saber quando um objeto é excluído, é necessário aparar seu próprio metaDB para impedir que ele seja preenchido com meta-informações antigas ou antigas, para as quais os usuários há muito excluíram os objetos.
Se você souber quando os objetos expiraram / foram excluídos, estará à frente do jogo e poderá expirá-lo do seu metaDB ao mesmo tempo, mas não ficou claro se você tem essa opção.
Felicidades!
fonte