¿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 document
objeto 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.NewEvents
lo haría ser un IEnumerable<Event>
y probablemente contendría un DocumentDiscarded
evento).
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-state
parece prometedor, pero no es una respuesta y carece de detalles. ¡Odiaría que estos puntos se desperdicien!
fuente
acid-state
parece estar cerca de lo que estás describiendo .acid-state
se ve muy bien, gracias por ese enlace. En términos de diseño de API, todavía parece estar vinculadoIO
; 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 usanacid-state
junto con una capa de presentación y logran mantener las dos separadas?Query
yUpdate
están bastante lejos deIO
, en realidad. Trataré de dar un ejemplo simple en una respuesta.Respuestas:
La forma general de separar componentes en Haskell es a través de pilas de transformadores de mónada. Explico esto con más detalle a continuación.
Imagine que estamos construyendo un sistema que tiene varios componentes a gran escala:
Decidimos que necesitamos mantener estos componentes débilmente acoplados para mantener un buen estilo de código.
Por lo tanto, codificamos cada uno de nuestros componentes polimórficamente, utilizando las diversas clases de MTL para guiarnos:
MonadState DataState m => Foo -> Bar -> ... -> m Baz
DataState
es una representación pura de una instantánea del estado de nuestra base de datos o almacenamientoMonadState UIState m => Foo -> Bar -> ... -> m Baz
UIState
es una representación pura de una instantánea del estado de nuestra interfaz de usuarioMonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
main :: IO ()
que hace el trabajo casi trivial de combinar los otros componentes en un sistemazoom
o un combinador similarStateT (DataState, UIState) IO
, que luego se ejecuta con el contenido real de la base de datos o el almacenamiento para producirIO
.fuente
DataState
es una representación pura de una instantánea del estado de nuestra base de datos o almacenamiento". ¡Presumiblemente no quiere cargar toda la base de datos en la memoria!¿Debería persistir el modelo? En muchos programas, es necesario guardar el modelo porque el estado es impredecible, cualquier operación podría mutar el modelo de cualquier manera, por lo que la única forma de conocer el estado del modelo es acceder a él directamente.
Si, en su escenario, la secuencia de eventos (comandos que han sido validados y aceptados) siempre puede generar el estado, entonces son los eventos los que deben persistir, no necesariamente el estado. El estado siempre se puede generar al reproducir los eventos.
Dicho esto, muchas veces el estado se almacena, pero solo como una instantánea / caché para evitar reproducir los comandos, no como datos esenciales del programa.
Una vez aceptado el comando, el evento se comunica a dos destinos (el almacenamiento de eventos y el sistema de informes) pero en la misma capa del programa.
Consulte también
Obtención de eventos
Ansioso Leer derivación
fuente
Estás tratando de poner espacio en tu aplicación intensiva de IO para todas las actividades que no son de IO; desafortunadamente, las aplicaciones CRUD típicas como las que hablas hacen poco más que IO.
Creo que entiende bien la separación relevante, pero cuando intenta colocar el código IO de persistencia en varias capas del código de presentación, el hecho general del asunto está en su controlador en algún lugar al que debería llamar. capa de persistencia, que puede parecer demasiado cercana a su presentación, pero eso es solo una coincidencia en que ese tipo de aplicación tiene poco más.
La presentación y la persistencia conforman básicamente la totalidad del tipo de aplicación que creo que estás describiendo aquí.
Si piensa en su mente acerca de una aplicación similar que tenía mucha lógica de negocios compleja y procesamiento de datos, creo que podrá imaginar cómo se separa muy bien del IO de presentación y el IO de persistencia de tal manera que no necesita saber nada tampoco. El problema que tiene en este momento es solo uno perceptivo causado por tratar de ver una solución a un problema en un tipo de aplicación que no tiene ese problema para comenzar.
fuente
Tan cerca como puedo entender su pregunta (que no puedo, pero pensé en tirar mis 2 centavos), ya que no necesariamente tiene acceso a los objetos en sí, necesita tener su propia base de datos de objetos que caduca con el tiempo).
Idealmente, los objetos mismos pueden mejorarse para almacenar su estado, de modo que cuando se "pasen", los diferentes procesadores de comandos sabrán con qué están trabajando.
Si eso no es posible, (icky icky), la única forma es tener una clave común similar a DB, que pueda usar para almacenar la información en una tienda que esté configurada para ser compartida entre diferentes comandos, y con suerte, "abra" la interfaz y / o el código para que cualquier otro escritor de comandos también adopte su interfaz para guardar y procesar metainformación.
En el área de servidores de archivos, samba tiene diferentes formas de almacenar cosas como listas de acceso y flujos de datos alternativos, dependiendo de lo que proporcione el sistema operativo host. Idealmente, samba se aloja en un sistema de archivos que proporciona atributos extendidos en los archivos. Ejemplo 'xfs' en 'linux': más comandos están copiando atributos extendidos junto con un archivo (por defecto, la mayoría de las utilidades en linux "crecieron" sin pensar en atributos extendidos).
Una solución alternativa, que funciona para múltiples procesos de samba de diferentes usuarios que operan en archivos comunes (objetos), es que si el sistema de archivos no admite adjuntar el recurso directamente al archivo como con atributos extendidos, está utilizando un módulo que implementa una capa de sistema de archivos virtual para emular atributos extendidos para procesos de samba. Solo samba lo sabe, pero tiene la ventaja de funcionar cuando el formato de objeto no lo admite, pero aún funciona con diversos usuarios de samba (cf. procesadores de comandos) que trabajan en el archivo en función de su estado anterior. Almacenará la metainformación en una base de datos común para el sistema de archivos que ayuda a controlar el tamaño de la base de datos (y no
Puede que no sea útil para usted si necesita más información específica para la implementación con la que está trabajando, pero conceptualmente, la misma teoría podría aplicarse a ambos conjuntos de problemas. Entonces, si estaba buscando algoritmos y métodos para hacer lo que desea, eso podría ayudar. Si necesitabas un conocimiento más específico en algún marco específico, entonces tal vez no sea tan útil ... ;-)
Por cierto, la razón por la que menciono 'autoexpiración' - es que no está claro si sabes qué objetos hay y cuánto tiempo persisten. Si no tiene una forma directa de saber cuándo se elimina un objeto, tendría que recortar su propia metaDB para evitar que se llene con metainformación antigua o antigua que los usuarios han eliminado hace mucho tiempo los objetos.
Si sabe cuándo caducan / eliminan los objetos, entonces está por delante del juego y puede expirarlo de su metaDB al mismo tiempo, pero no estaba claro si tenía esa opción.
¡Salud!
fuente