In che modo la persistenza si inserisce in un linguaggio puramente funzionale?

17

In che modo il modello di utilizzo dei gestori di comandi per gestire la persistenza si inserisce in un linguaggio puramente funzionale, in cui vogliamo rendere il codice relativo all'IO il più sottile possibile?

Quando si implementa il Domain-Driven Design in un linguaggio orientato agli oggetti, è comune usare il schema Command / Handler per eseguire cambiamenti di stato. In questa progettazione, i gestori di comandi si trovano in cima ai tuoi oggetti di dominio e sono responsabili della logica relativa alla persistenza, come l'utilizzo di repository e la pubblicazione di eventi di dominio. I gestori sono la faccia pubblica del tuo modello di dominio; il codice dell'applicazione come l'interfaccia utente chiama i gestori quando è necessario modificare lo stato degli oggetti di dominio.

Uno schizzo in 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);
    }
}

L'oggetto dominio document è responsabile dell'implementazione delle regole aziendali (come "l'utente dovrebbe avere il permesso di scartare il documento" o "non puoi scartare un documento che è già stato scartato") e per generare gli eventi del dominio dobbiamo pubblicare ( document.NewEvents sarebbe un IEnumerable<Event> e probabilmente conterrebbe un evento DocumentDiscarded ).

Questo è un bel progetto - è facile da estendere (puoi aggiungere nuovi casi d'uso senza cambiare il tuo modello di dominio, aggiungendo nuovi gestori di comandi) ed è agnostico su come gli oggetti sono persistenti (puoi facilmente scambiare un repository di NHibernate per un repository Mongo, o scambiare un editore RabbitMQ per un editore di EventStore) che rende facile testare utilizzando falsi e mock. Obbedisce anche alla separazione modello / vista: il gestore comandi non ha idea se viene utilizzato da un processo batch, una GUI o un'API REST.

In un linguaggio puramente funzionale come Haskell, potresti modellare il gestore di comandi più o meno così:

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"

Ecco la parte che sto cercando di capire. Tipicamente, ci sarà una sorta di codice di 'presentazione' che chiama nel gestore comandi, come una GUI o un'API REST. Così ora abbiamo due livelli nel nostro programma che devono fare IO - il gestore di comandi e la vista - che è un grande no-no in Haskell.

Per quanto posso capire, ci sono due forze opposte qui: una è la separazione modello / vista e l'altra è la necessità di mantenere il modello. Ci deve essere un codice IO per mantenere il modello da qualche parte , ma la separazione modello / vista dice che non possiamo metterlo nel livello di presentazione con tutti gli altri codici IO.

Naturalmente, in una lingua "normale", IO può (e succede) accadere ovunque. Il buon design impone che i diversi tipi di IO siano tenuti separati, ma il compilatore non lo applica.

Quindi: come conciliare la separazione modello / vista con il desiderio di spingere il codice IO fino al limite del programma, quando il modello deve essere persistente? Come possiamo mantenere separati i due diversi tipi di IO , ma siamo comunque lontani da tutto il codice puro?

Aggiornamento : la taglia scade tra meno di 24 ore. Non credo che nessuna delle risposte attuali abbia affrontato la mia domanda. @ Il commento di Ptharien's Flame su acid-state sembra promettente, ma non è una risposta e manca nei dettagli. Odio per questi punti sprecare!

    
posta Benjamin Hodgson 10.03.2014 - 23:43
fonte

4 risposte

6

Il modo generale per separare i componenti in Haskell è attraverso gli stack di trasformatori monad. Spiego questo in maggior dettaglio qui sotto.

Immagina di costruire un sistema con diversi componenti su larga scala:

  • un componente che parla con il disco o il database (sottomodello)
  • un componente che esegue le trasformazioni sul nostro dominio (modello)
  • un componente che interagisce con l'utente (visualizza)
  • un componente che descrive la connessione tra vista, modello e submodel (controller)
  • un componente che avvia kickstart dell'intero sistema (driver)

Decidiamo che dobbiamo mantenere questi componenti accoppiati liberamente per mantenere uno stile di codice buono.

Pertanto codifichiamo ciascuno dei nostri componenti in modo polimorfico, utilizzando le varie classi MTL per guidarci:

  • ogni funzione nel submodel è di tipo MonadState DataState m => Foo -> Bar -> ... -> m Baz
    • DataState è una pura rappresentazione di un'istantanea dello stato del nostro database o storage
  • ogni funzione nel modello è pura
  • ogni funzione nella vista è di tipo MonadState UIState m => Foo -> Bar -> ... -> m Baz
    • UIState è una pura rappresentazione di un'istantanea dello stato della nostra interfaccia utente
  • ogni funzione nel controller è di tipo MonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
    • Si noti che il controllore ha accesso sia allo stato della vista che allo stato del sottomodello
  • il driver ha una sola definizione, main :: IO () , che fa il lavoro quasi banale di combinare gli altri componenti in un unico sistema
    • la vista e il sottomodello dovranno essere rimossi nello stesso tipo di stato del controller usando zoom o un combinatore simile
    • il modello è puro e quindi può essere utilizzato senza restrizioni
    • alla fine, tutto vive in (un tipo compatibile con) StateT (DataState, UIState) IO , che viene quindi eseguito con i contenuti effettivi del database o dello storage per produrre IO .
risposta data 11.03.2014 - 16:05
fonte
4

So: how do we reconcile model/view separation with the desire to push IO code to the very edge of the program, when the model needs to be persisted?

Il modello deve essere persistente? In molti programmi, è necessario salvare il modello perché lo stato è imprevedibile, qualsiasi operazione potrebbe modificare il modello in qualsiasi modo, quindi l'unico modo per conoscere lo stato del modello è quello di accedervi direttamente.

Se, nel tuo scenario, la sequenza di eventi (comandi che sono stati convalidati e accettati) può sempre generare lo stato, allora sono gli eventi che devono essere persistenti, non necessariamente lo stato. Lo stato può sempre essere generato ripetendo gli eventi.

Detto questo, spesso lo stato viene memorizzato, ma solo come uno snapshot / cache per evitare di riprodurre i comandi, non come dati essenziali del programma.

So now we have two layers in our program which need to do IO - the command handler and the view - which is a big no-no in Haskell.

Una volta accettato il comando, l'evento viene comunicato a due destinazioni (la memoria eventi e il sistema di segnalazione) ma allo stesso livello del programma.

Vedi anche
Sourcing di eventi
Frequente lettura Derivazione

    
risposta data 01.03.2014 - 13:30
fonte
2

Stai provando a inserire spazio nella tua applicazione intensiva di IO per tutte le attività non IO; sfortunatamente le app tipiche di CRUD di cui parli non fanno altro che IO.

Penso che tu abbia compreso bene la separazione pertinente, ma dove stai cercando di posizionare il codice IO di persistenza a un certo numero di livelli lontano dal codice di presentazione, il fatto generale è nel controller da qualche parte dovresti chiamare sul tuo livello di persistenza, che potrebbe sembrare troppo vicino alla presentazione per te - ma è solo una coincidenza che in quel tipo di app non ha molto altro.

La presentazione e la persistenza costituiscono fondamentalmente l'insieme del tipo di app che penso tu stia descrivendo qui.

Se pensi nella tua testa a un'applicazione simile con una complessa logica di business e l'elaborazione dei dati al suo interno, penso che ti troverai in grado di immaginare come sia ben separato dall'IO di presentazione e dalla persistenza IO roba tale che non ha bisogno di sapere nulla su entrambi. Il problema che hai adesso è solo percettivo causato dal tentativo di vedere una soluzione ad un problema in un tipo di applicazione che non ha quel problema per cominciare.

    
risposta data 11.03.2014 - 00:00
fonte
1

Per quanto possa capire la tua domanda (cosa che non posso, ma ho pensato di buttare i miei 2 centesimi), dal momento che non hai necessariamente accesso agli oggetti stessi, devi avere il tuo database di oggetti che si auto-espira nel tempo).

Idealmente gli oggetti stessi possono essere migliorati per memorizzare il loro stato in modo che quando vengono "passati intorno", diversi processori di comando sapranno con cosa stanno lavorando.

Se ciò non è possibile, (icky icky), l'unico modo è avere qualche comune chiave tipo DB, che puoi usare per memorizzare le informazioni in un negozio che è configurato per essere condivisibile tra diversi comandi - e si spera che "apra" l'interfaccia e / o il codice in modo che tutti gli altri scrittori di comandi adotteranno anche la tua interfaccia per il salvataggio e l'elaborazione delle meta-informazioni.

Nell'area dei file server samba ha diversi modi di memorizzare le cose come gli elenchi di accesso e i flussi di dati alternativi, a seconda di ciò che fornisce il sistema operativo host. Idealmente, samba è ospitato su un file system che fornisce attributi estesi sui file. Esempio "xfs" su "linux" - più comandi copiano gli attributi estesi insieme a un file (per impostazione predefinita, la maggior parte degli utils su linux "cresciuti" non sono pensati come attributi estesi).

Una soluzione alternativa, che funziona per più processi di samba da diversi utenti che operano su file (oggetti) comuni, è che se il file system non supporta l'associazione della risorsa direttamente al file come con attributi estesi, sta utilizzando un modulo che implementa un livello di file system virtuale per emulare attributi estesi per i processi samba. Solo samba ne è a conoscenza, ma ha il vantaggio di lavorare quando il formato dell'oggetto non lo supporta, ma funziona ancora con diversi utenti di samba (processori di comandi di comando) che lavorano sul file in base al suo stato precedente. Memorizzerà le meta informazioni in un database comune per il file system che aiuta a controllare la dimensione del database (e non ha bisogno della scadenza delle voci a meno che i file non vengano cancellati) - se samba è l'unico accessor (l'interfaccia comune ) ai file, quindi può fornire in modo trasparente le stesse funzionalità ai file su qualsiasi file system, che altrimenti sarebbe formato di file system specifico (simile a diversi formati di rappresentazione dei dati quando gli oggetti sono fatti da persone o squadre diverse).

Potrebbe non essere utile se avessi bisogno di più informazioni specifiche dell'implementazione con cui stai lavorando, ma concettualmente, la stessa teoria potrebbe essere applicata ad entrambi i set di problemi. Quindi, se stavi cercando algoritmi e metodi per fare ciò che vuoi, potrebbe essere d'aiuto. Se avessi bisogno di conoscenze più specifiche in qualche framework specifico, allora forse non così utile ...; -)

BTW - il motivo per cui ho menzionato "l'auto-scadenza" - è che non è chiaro se tu sai quali oggetti sono là fuori e per quanto tempo persistono. Se non hai un modo diretto per sapere quando un oggetto viene eliminato, devi tagliare il tuo metaDB per evitare che si riempia di vecchio o antico meta informazioni che gli utenti hanno da tempo eliminato per gli oggetti.

Se sai quando gli oggetti sono scaduti / cancellati, sei in testa al gioco e puoi espirarlo fuori dal tuo metaDB allo stesso tempo, ma non era chiaro se avessi questa opzione.

Cheers!

    
risposta data 10.03.2014 - 18:38
fonte