È ancora valido parlare del modello anemico nel contesto della programmazione funzionale?


40

La maggior parte dei modelli di progettazione tattica di DDD appartiene al paradigma orientato agli oggetti e il modello anemico descrive la situazione in cui tutta la logica aziendale viene messa nei servizi anziché negli oggetti, rendendoli così una sorta di DTO. In altre parole, il modello anemico è sinonimo di stile procedurale, che non è consigliato per un modello complesso.

Non ho molta esperienza nella pura programmazione funzionale, ma vorrei sapere come il DDD si adatta al paradigma FP e se il termine "modello anemico" esiste ancora in quel caso.

Aggiornamento : libro pubblicato recentemente e video sull'argomento.


1
Se stai dicendo quello che penso tu stia dicendo qui, i DTO sono oggetti anemici, ma di prima classe in DDD, e che esiste una separazione naturale tra i DTO e i servizi che li elaborano. Sono d'accordo in linea di principio. Anche questo post sul blog , a quanto pare.
Robert Harvey,


1
"se il termine" modello anemico "esiste ancora in quel caso" Risposta breve, il termine modello anemico è stato coniato nel contesto di OO. Parlare di un modello anemico nel contesto di FP non ha alcun senso. Potrebbero esserci equivalenti nel senso di descrivere ciò che è FP idiomatico, ma non ha nulla a che fare con i modelli anemici.
plalx,

5
A Eric Evans una volta è stato chiesto cosa dice alle persone che lo accusano che ciò che descrive nel suo libro è solo un buon design orientato agli oggetti, e ha risposto che non è un'accusa, è la verità, DDD è solo un buon OOD, ha appena scritto giù alcune ricette e modelli e ha dato loro i nomi, in modo che sia più facile seguirli e parlarne. Quindi, non è una sorpresa che DDD sia collegato a OOD. La domanda più ampia sarebbe: quali sono le intersezioni e le differenze tra OOD e FPD, anche se prima dovresti definire cosa intendi per "programmazione funzionale".
Jörg W Mittag,

2
@ JörgWMittag: intendi altro che la solita definizione? Ci sono molte piattaforme illustrative, Haskell è la più ovvia.
Robert Harvey,

Risposte:


24

Il modo in cui viene descritto il problema del "modello anemico" non si traduce bene in FP così com'è. Innanzitutto deve essere adeguatamente generalizzato. Al suo centro, un modello anemico è un modello che contiene conoscenze su come usarlo correttamente che non è incapsulato dal modello stesso. Invece, quella conoscenza è diffusa in un mucchio di servizi correlati. Tali servizi dovrebbero essere solo clienti del modello, ma a causa della sua anemia ne sono ritenuti responsabili . Ad esempio, considera una Accountclasse che non può essere utilizzata per attivare o disattivare gli account o persino cercare informazioni su un account se non gestita tramite una AccountManagerclasse. L'account dovrebbe essere responsabile delle operazioni di base su di esso, non di alcune classi manager esterne.

Nella programmazione funzionale, esiste un problema simile quando i tipi di dati non rappresentano accuratamente ciò che dovrebbero modellare. Supponiamo di dover definire un tipo che rappresenti gli ID utente. Una definizione "anemica" indicherebbe che gli ID utente sono stringhe. È tecnicamente fattibile, ma si verificano enormi problemi perché gli ID utente non vengono utilizzati come stringhe arbitrarie. Non ha senso concatenarli o suddividere le loro sottostringhe, Unicode non dovrebbe davvero importare, e dovrebbero essere facilmente incorporabili in URL e altri contesti con rigide limitazioni di carattere e formato.

La risoluzione di questo problema si verifica in genere in alcune fasi. Un semplice primo taglio è dire "Beh, a UserIDè rappresentato in modo equivalente a una stringa, ma sono tipi diversi e non puoi usarne uno dove ti aspetti l'altro". Haskell (e alcuni altri linguaggi funzionali digitati) fornisce questa funzione tramite newtype:

newtype UserID = UserID String

Questo definisce una UserIDfunzione che quando viene data Stringcostruisce un valore che viene trattato come un UserIDsistema di tipo, ma che è ancora solo Stringin fase di esecuzione. Ora le funzioni possono dichiarare che richiedono UserIDuna stringa anziché una stringa; usando UserIDs dove in precedenza si utilizzavano protezioni stringhe contro il codice che concatena due UserIDs insieme. Il sistema di tipi garantisce che ciò non può avvenire, non sono richiesti test.

Il punto debole qui è che il codice può ancora prendere qualsiasi Stringlike arbitrario "hello"e costruirne uno UserID. Ulteriori passaggi includono la creazione di una funzione "costruttore intelligente" che quando viene data una stringa controlla alcuni invarianti e restituisce a solo UserIDse sono soddisfatti. Poi il "muto" UserIDcostruttore è fatto in modo privato, se un cliente vuole un UserIDessi devono utilizzare il costruttore intelligente, impedendo in tal modo UserIDs malformati da venire all'esistenza.

Anche ulteriori passaggi definiscono il UserIDtipo di dati in modo tale che sia impossibile costruirne uno malformato o "improprio", semplicemente per definizione. Ad esempio, definendo a UserIDcome un elenco di cifre:

data Digit = Zero | One | Two | Three | Four | Five | Six | Seven | Eight | Nine
data UserID = UserID [Digit]

Per costruire un UserIDelenco di cifre deve essere fornito. Data questa definizione, è banale dimostrare che è impossibile UserIDche esista un che non può essere rappresentato in un URL. La definizione di modelli di dati come questo in Haskell è spesso aiutata da funzionalità di sistema di tipo avanzato come tipi di dati e tipi di dati algebrici generalizzati (GADT) , che consentono al sistema di tipi di definire e dimostrare più invarianti sul codice. Quando i dati sono disaccoppiati dal comportamento, la definizione dei dati è il solo mezzo per imporre il comportamento.


2
E che dire degli aggregati e delle radici aggregate che proteggono gli invarianti, anche gli ultimi possono essere espressi e facilmente compresi dagli sviluppatori in seguito? Per me la parte più preziosa di DDD è una mappatura diretta del modello di business al codice. E la tua risposta riguarda esattamente questo.
Pavel Voronin,

2
Bel discorso, ma nessuna risposta alla domanda OP.
SerG,

10

In larga misura, l'immutabilità rende superfluo accoppiare strettamente le tue funzioni ai tuoi dati come sostenitori di OOP. Puoi fare tutte le copie che vuoi, anche creando strutture di dati derivati, in codice molto rimosso dal codice originale, senza timore che la struttura di dati originale cambi inaspettatamente da sotto di te.

Tuttavia, un modo migliore per fare questo confronto è probabilmente quello di guardare ai quali funzioni si assegnano al modello strato di rispetto dei servizi strato . Anche se non sembra lo stesso di OOP, è un errore abbastanza comune in FP cercare di stipare quelli che dovrebbero essere più livelli di astrazione in una funzione.

Per quanto ne so, nessuno lo chiama un modello anemico, in quanto si tratta di un termine OOP, ma l'effetto è lo stesso. Puoi e dovresti riutilizzare le funzioni generiche laddove applicabile, ma per operazioni più complesse o specifiche dell'applicazione, dovresti anche fornire un ricco set di funzioni solo per lavorare con il tuo modello. La creazione di strati di astrazione appropriati è una buona progettazione in qualsiasi paradigma.


2
"In larga misura, l'immutabilità rende superfluo accoppiare strettamente le tue funzioni ai tuoi dati come sostenitori di OOP.": Un'altra ragione per accoppiare dati e procedure è implementare il polimorfismo attraverso l'invio dinamico.
Giorgio,

2
Il vantaggio principale di associare il comportamento ai dati nel contesto di DDD è fornire un'interfaccia aziendale significativa; è sempre a portata di mano. Abbiamo un modo naturale di auto-documentare il codice (almeno è quello a cui sono abituato), ed è la chiave per comunicazioni di successo con esperti di business. Come si compie quindi in FP? Probabilmente le tubazioni aiutano, ma cos'altro? La natura generica di FP non rende i requisiti aziendali più difficili da decodificare dal codice?
Pavel Voronin,

7

Quando si utilizza DDD in OOP, uno dei motivi principali per inserire la logica aziendale negli oggetti dominio stessi è che la logica aziendale viene generalmente applicata mutando lo stato dell'oggetto. Questo è legato all'incapsulamento: Employee.RaiseSalaryprobabilmente muta il salarycampo Employeedell'istanza, che non dovrebbe essere pubblicamente impostabile.

In FP, la mutazione viene evitata, quindi implementare questo comportamento creando una RaiseSalaryfunzione che accetta Employeeun'istanza esistente e restituisce una nuova Employee istanza con il nuovo stipendio. Quindi nessuna mutazione è coinvolta: solo la lettura dell'oggetto originale e la creazione del nuovo oggetto. Per questo motivo, una tale RaiseSalaryfunzione non deve essere definita come un metodo sulla Employeeclasse, ma può vivere ovunque.

In questo caso, diventa naturale separare i dati dal comportamento: una struttura rappresenta i Employeedati come (completamente anemici), mentre uno (o più) moduli contengono funzioni che operano su quei dati (preservando l'immutabilità).

Si noti che quando si accoppiano dati e comportamenti come in DDD, in genere si viola il principio di responsabilità singola (SRP): Employeepotrebbe essere necessario modificare se cambiano le regole per lo stipendio; ma potrebbe anche essere necessario modificare se le regole per il calcolo del bonus EOY cambiano. Con l'approccio disaccoppiato non è così, dal momento che è possibile disporre di più moduli, ognuno con una responsabilità.

Quindi, come al solito l'approccio FP offre una maggiore modularità / componibilità.


-1

Penso che l'essenza della questione sia che un modello anemico con tutta la logica di dominio nei servizi che operano sul modello sia fondamentalmente una programmazione procedurale - al contrario della "reale" programmazione OO in cui si hanno oggetti che sono "intelligenti" e contengono non solo dati ma anche la logica più strettamente legata ai dati.

E lo stesso contrasto esiste con la programmazione funzionale: FP "reale" significa usare funzioni come entità di prima classe, che vengono passate come parametri, così come costruite al volo e restituite come valore di ritorno. Ma quando non riesci a usare tutta quella potenza e hai solo funzioni che operano su strutture di dati che vengono passate tra di loro, allora finisci nello stesso posto: stai fondamentalmente facendo una programmazione procedurale.


5
Sì, questo è fondamentalmente ciò che dice l'OP nella sua domanda. Sembra che entrambi abbiate perso il punto di poter ancora avere una composizione funzionale.
Robert Harvey,

-3

Mi piacerebbe sapere come DDD si adatta al paradigma FP

Penso che lo sia, ma in gran parte come approccio tattico alla transizione tra oggetti valore immutabili o come modo per innescare metodi sulle entità. (Dove la maggior parte della logica vive ancora nell'entità.)

e se il termine "modello anemico" esiste ancora in quel caso.

Bene, se intendi "in un modo analogo al tradizionale OOP", aiuta a ignorare i soliti dettagli di implementazione e tornare alle basi: che lingua usano i tuoi esperti di dominio? Quali intenti stai catturando dai tuoi utenti?

Supponiamo che parlino di concatenare processi e funzioni insieme, quindi sembra che le funzioni (o almeno gli oggetti "do-er") siano fondamentalmente i tuoi oggetti di dominio!

Quindi, in quello scenario, probabilmente si verificherebbe un "modello anemico" quando le tue "funzioni" non sono effettivamente eseguibili e sono invece solo costellazioni di metadati che sono interpretate da un Servizio che fa il vero lavoro.


1
Si potrebbe verificare un modello anemico quando si passano tipi di dati astratti, come tuple, record o elenchi, a diverse funzioni per l'elaborazione. Non hai bisogno di qualcosa di così esotico come una "funzione che non esegue" (qualunque cosa sia).
Robert Harvey,

Da qui le virgolette attorno alle "funzioni", per sottolineare quanto inappropriate diventino le etichette quando sono anemiche.
Darien,

Se sei ironico, è un po 'sottile.
Robert Harvey,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.