¿Cómo encaja el patrón de usar controladores de comandos para lidiar con la persistencia en un lenguaje puramente funcional, donde queremos hacer que el código relacionado con IO sea lo más delgado posible?
Al implementar el diseño controlado por dominio en un lenguaje orientado a objetos, es común usar el patrón de comando / controlador para ejecutar cambios de estado. En este diseño, los controladores de comandos se ubican encima de los objetos de su dominio y son responsables de la aburrida lógica relacionada con la persistencia, como el uso de repositorios y la publicación de eventos de dominio. Los controladores son la cara pública de su modelo de dominio; El código de la aplicación, como la IU, llama a los controladores cuando necesita cambiar el estado de los objetos de dominio.
Un boceto en 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);
}
}
El documentobjeto de dominio es responsable de la aplicación de las reglas de negocio (como "el usuario debe tener permiso para descartar el documento" o "no se puede descartar un documento que ya ha sido descartada") y para generar los eventos de dominio que necesitamos para publicar ( document.NewEventslo haría ser un IEnumerable<Event>y probablemente contendría un DocumentDiscardedevento).
Este es un diseño agradable: es fácil de extender (puede agregar nuevos casos de uso sin cambiar su modelo de dominio, agregando nuevos controladores de comandos) y es independiente de cómo persisten los objetos (puede cambiar fácilmente un repositorio de NHibernate por un Mongo repositorio, o cambiar un editor RabbitMQ por un editor EventStore), lo que facilita la prueba con falsificaciones y simulacros. También obedece a la separación modelo / vista: el controlador de comandos no tiene idea de si lo está utilizando un trabajo por lotes, una GUI o una API REST.
En un lenguaje puramente funcional como Haskell, puede modelar el controlador de comandos más o menos así:
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"
Aquí está la parte que me cuesta entender. Por lo general, habrá algún tipo de código de 'presentación' que llama al controlador de comandos, como una GUI o una API REST. Así que ahora tenemos dos capas en nuestro programa que necesitan hacer IO: el controlador de comandos y la vista, que es un gran no-no en Haskell.
Hasta donde puedo entender, hay dos fuerzas opuestas aquí: una es la separación modelo / vista y la otra es la necesidad de persistir en el modelo. Es necesario que haya un código IO para mantener el modelo en alguna parte , pero la separación modelo / vista dice que no podemos ponerlo en la capa de presentación con el resto del código IO.
Por supuesto, en un lenguaje "normal", IO puede (y ocurre) en cualquier lugar. Un buen diseño dicta que los diferentes tipos de IO se mantengan separados, pero el compilador no lo exige.
Entonces: ¿cómo conciliamos la separación modelo / vista con el deseo de llevar el código IO al borde del programa, cuando el modelo necesita persistir? ¿Cómo mantenemos los dos tipos diferentes de IO separados , pero aún lejos de todo el código puro?
Actualización : la recompensa caduca en menos de 24 horas. No creo que ninguna de las respuestas actuales haya abordado mi pregunta en absoluto. El comentario de @Ptharien's Flame sobre acid-stateparece prometedor, pero no es una respuesta y carece de detalles. ¡Odiaría que estos puntos se desperdicien!
acid-statese ve muy bien, gracias por ese enlace. En términos de diseño de API, todavía parece estar vinculado IO; Mi pregunta es acerca de cómo un marco de persistencia encaja en una arquitectura más grande. ¿Conoces las aplicaciones de código abierto que se usan acid-statejunto con una capa de presentación y logran mantener las dos separadas?
Queryy Updateestán bastante lejos de IO, en realidad. Trataré de dar un ejemplo simple en una respuesta.
acid-stateparece estar cerca de lo que estás describiendo .