Sei sulla pagina 1di 5

Capitoli 1-6

------------
- Immutabilità ovunque possibile

Capitolo 7
----------
- rappresentare gli stati della Entity come tipi espliciti invece che
implicitamente (flag, valori opzionali, ...) ==> uniti poi in un choice type che
rappresenta la Entity in tutto il suo ciclo di vita
- permette di definire una macchina a stati finiti ed esaminare tutte le casistiche
applicabili
- ogni transizione di stato prende in ingresso un parametro di state (choice type
complessivo), e con pattern matching su uno degli state componenti decide la
trasformazione da applicare e quindi quale (nuovo) stato restituire, che può essere
uguale al precedente, oppure diverso con lo stesso o un altro stato
- anche le dipendenze (es. funzioni di appoggio) possono essere modellate come tipi
e definite come parametri nella firma della funzione che le utilizza. Di solito
messe come primi parametri per permettere l'applicazione parziale (~ dependency
injection). Si può seguire questa guideline:
• For functions exposed in a public API, hide dependency information from
callers.
• For functions used internally, be explicit about their dependencies

----------
Capitolo 8
----------
- Functional programming paradigm: functions are used everywhere, for everything
- CURRYING: convertire funzione multiparametro in una serie di funzioni da un
parametro tramite applicazione parziale
- Cercare di fare funzioni TOTALI (ogni input ha un output) per rendere il tutto il
più esplicito possibile: se un caso dà eccezione, allora non è del tutto corretto
dire che la firma della mia funzione è (int -> int) per esempio. Le due opzioni per
ottenere funzioni totali sono:
* restringere l'input definendo il tipo dei soli valori validi, es. (NonZeroInt -
> int)
* estendere l'output definendo un choice type fra i possibili risultati, es. (int
-> int option)
- FUNCTION COMPOSITION: comporre la funzione f:a->b con la funzione g:b->c permette
di creare una nuova funzione a->c
* un modo per farlo è tramite piping, che in F# si fa con l'operatore |>
- La composizione è possibile solo se il tipo in output da una funzione è l'esatto
tipo di input per la successiva. Se questo non succede, serve convertire entrambi i
tipi al "minimo comune multiplo"
* esempio: output=int, input=Option<int> ==> possiamo interporre nella pipeline
una funzione che converte da int a Option<int> (ovvero Some)

----------------------------------
Capitolo 9 - comporre una pipeline
----------------------------------
- Workflow: implementato come pipeline, cioè serie di trasformazioni di un
documento (ogni step è una pipe)
* Ogni step dev'essere una funzione stand-alone, stateless e senza side-effects
=> indipendente e testabile
* Poi vengono composte in una pipeline. La difficoltà è ottenere match corretti
fra output e input, sia a causa di argomenti extra (dipendenze) sia a causa di
effetti come asincronia (Async) o gestione errori (Result)
- In questo capitolo: come gestire le dipendenze ~= "dependency injection"
funzionale

- SIMPLE TYPES: come garantire che semplici tipi "wrapper" ci garantiscano la


correttezza del dato (es.il prezzo deve essere >0, il codice prodotto deve iniziare
per X ed essere di 6 caratteri, ...)?
* CONSTRAINED TYPES con "smart constructor": costruttore privato + funzione che
costruisce il valore o dà errore ==> avremo almeno due funzioni "create" per fare
il wrap del tipo primitivo e "value" per estrarlo (con la garanzia che sia
corretto)

/// OrderId -> string


let value (OrderId str) = // unwrap in the parameter! --> pattern-matching
str // return the inner value

- Usare i FUNCTION TYPES: in F# si possono definire funzioni senza specificare i


tipi parametri (inference), o in alternativa definire la funzione annotata con un
tipo, ed implementarla come lambda

// define a function signature


type MyFunctionSignature = Param1 -> Param2 -> Result
// define a function that implements that signature
let myFunc: MyFunctionSignature =
fun param1 param2 ->
...

- Esempio di PARTIAL APPLICATION: se doSomethingFunction è una dipendenza della


funzione mainFunction
con firma: "mainFunction doSomethingFunction mainInput", allora si può applicare
come
"valoreIniziale |> step1 |> ... |> stepN |> mainFunction doSomethingFunction"
- L'operatore PIPE |> passa sempre il suo argomento di sinistra all'argomento di
destra (che è una funzione) come ULTIMO parametro

- <p.169> Choice type in ingresso -> eseguo conversioni diverse per ogni caso, e
l'ultima conversione fa in modo che il tipo in uscita sia lo stesso choice type per
ogni caso

- <p.170> ADAPTER FUNCTION (= function transfomer): riceve in ingresso una funzione


con una certa firma e restituisce una funzione con firma diversa, "adattata" al
caso d'uso
* es. di adapter generico (da predicato a pass-through adatto per una pipeline)
val predicateToPassthru : errorMsg:string -> f:('a -> bool) -> x:'a -> 'a
* altro esempio di un function transformer è List.map, che trasforma una funziona
'a -> 'b in una funzione ('a list -> 'b list)

- <p.177> The approach of converting or “LIFTING” non-compatible things to a shared


type is a key technique for handling composition problems.

- <p.182> L'equivalente della dependency injection è un passaggio esplicito di


tutte le funzioni / servizi che sono dipendenze per la nostra implementazione. Ci
sonbo approcci più avanzati, ma il più semplice è quello di passare tutte le
dipendenze dalla funzione di alto livello che funge da "composition root" via via
verso le funzioni di basso livello che ne hanno bisogno.
* la fase di setup nella "composition root" dovrebbe essere il più vicino
possibile all'entry point dell'applicazione (funzione main, callback su start-
up, ...)
* e se le funzioni di basso livello hanno altri parametri oltre alle dipendenze
espresse come funzioni? (es.URI, credenziali, ...) => nella composition root si
possono trasformare in funzioni con meno parametri, tramite una partial application

- TEST: semplificato dall'uso di una composition root => per tutte le dipendenze è
semplice creare dei mock
* benefici derivanti da Functional Programming:
> funzioni stateless (e pure) - dato un input, l'output è sempre lo stesso
> dipendenze passate sempre esplicitamente, quindi chiare
> side-effects sono incapsulati nei parametri e non nella funzione

- Riassunto tecniche di composizione:


* ADAPTER FUNCTION
* LIFTING to common types
* PARTIAL APPLICATION to "bake in" dependencies

------------------------------------
Capitolo 10 - errori e altri effetti
------------------------------------
- Functional programming cerca di rendere tutto esplicito ed avere funzioni totali.
Questo include i possibili errori, col tipo Result<'r,'e> e creando tipi specifici
anche per gli errori, ad esempio choice type delle possibili cause
- Tipi di errori:
* DOMAIN ERRORS, parte normale del processo di business (es. codice errato) ==>
da modellare come il resto del dominio
* PANICS, non gestibili (out of memory, null ref.) ==> opportuno gestirli
sollevando eccezione di cui si fa il catch al più alto livello, abortendo il
workflow
* INFRASTRUCTURE ERRORS, inevitabili (es. timeout di rete) ma non dovuti al
dominio ==> va visto caso per caso come gestirli: es. se un servizio di
autenticazione è offline, va cambiato il processo di business? Verificare con
esperti di dominio
- Usare choice type per gli errori di dominio garantisce a compile-time una
gestione completa dei casi di errore (pattern matching che mancano un caso danno
errore)
- SWITCH FUNCTION = MONADIC FUNCTION = una funzione con un input che dà un Result
in uscita
- Two-track programming: ogni funzione nella pipeline ha due ingressi (success,
error) e due uscite (idem), in modo da poterle concatenare. Per ottenere una two-
track function da una switch function, serve un adapter, di solito chiamato BIND o
FLAT_MAP (se entra success, restituisce il risultato della funzione applicata al
valore; se entra error, lo restituisce)
- Per convertire single-track functions in two-track functions, si usa invece MAP.
Se entra success, usa il valore e lo restituisce wrappato in un success. Altrimenti
restituisce l'errore. Esempio di uso:
resultXYZ |> map singleTrackFunction
- <p.202> Oltre all'uso di two-track functions in tutta la pipeline, è necessaria
compatibilità di tipi. Ad esempio
F1 = A -> Result<B,...> può essere concatenata (usando bind) con F2 = B ->
Result<C,...> ma non con F3 = C -> ...
* Discorso simile vale sugli errori, che però devono avere un tipo uniforme lungo
tutta la pipeline, ad esempio un choice type che unisce gli errori di tutte le
funzioni che ne fanno parte. Per fare questo dobbiamo trasformare i tipi di errore
specifici nel tipo unico, usando una MAP_ERROR (come MAP ma sul failure track)
- Come gestire funzioni che sollevano eccezioni? O si lasciano passare (v.Tipi di
errori) o si usa un altro adapter per trasformarle in funzioni che ritornano
Result<..,ErroreDiDominio> (generico o specifico per singoli casi, es.errori db)
- E funzioni dead-end (senza output)? Si possono trasformare innanzitutto in
funzioni pass-through usando TEE. In pseudocodice se ho f(x){log(x)} posso ottenere
f'(x){f(x);return x}. Ovvero:
* let tee f x =
f x
x
- <p.211> COMPUTATION EXPRESSIONS: in F permettono di nascondere la complessità
facendo il wrap di blocchi che usano Result e permettendo di "sottintenderlo"
usando la parola chiave let! (con !) che fa unwrap dei Result
- <p.214> Applicare List.map con delle funzioni che restituiscono Result produce
una List<Result,...>, ma in genere vogliamo un Result<List,...> che è in errore se
almeno un elemento lo è. Si può definire questo con una helper function che usa una
versione ridefinita dell'operazione di prepend (detta CONS in FP): prende
Result<'a> e Result<'a list> e crea una nuova Result<'a list> che è Ok se entrambi
sono ok, Error se almeno uno lo è. Applicandola alla Result list di partenza con
foldBack, otteniamo una nuova helper function detta SEQUENCE.

- <p.217> MONADI
A monad is a programming pattern that allows you to chain “monadic”
functions together in series.
What’s a“monadic”function? It’s a function that takes a “normal” value and
returns some kind of “enhanced” value.
E.g. the “enhanced” value can be something wrapped in the Result type, so a
monadic function is exactly the kind
of Result-generating “switch” functions that we’ve been working with.
Technically, a “monad” is simply a term for something with three components:
• A data structure
• Some related functions
• Some rules about how the functions must work
The data structure in our case is the Result type.
To be a monad, the data type must have two related functions as well:
• RETURN (also known as PURE) is a function that turns a normal value into a
monadic type.
Since the type we’re using is Result, the return function is the Ok
constructor.
• BIND (also known as FLATMAP) is a function that lets you chain together
monadic functions
(in our case, Result-generating functions).
The rules about how these functions should work are called the "monad laws"
- <p.217> APPLICATIVES
Permettono di combinare valori monadici in parallelo (le monadi permettono di
farlo in serie)

------------------------------------
Capitolo 11 - (De)serializzare
------------------------------------
- Domain type <--> DTO <--> serializzazione in bytes/JSON/....
- Il DTO type è quello che attraversa i confini del bounded context.
- La deserializzazione verso DTO dovrebbe sempre funzionare (salvo dati corrotti),
validazioni e gestioni errori vanno fatti nella conversione da DTO a tipo di
dominio, dove abbiamo pieno controllo (siamo dentro il bounded context)
- DTOs as a Contract Between Bounded Contexts

------------------------------------
Capitolo 12 - Persistenza
------------------------------------
- Princìpi di base
* Push persistence to the edges.
- Il "core" è fatto di funzioni pure. L'interazione I/O è aggiunta solo
all'inizio e alla fine del workflow, in modo da avere la massima testabilità ===>
sandwich [I/O]->[pure]->[I/O], eventualmente ripetibile con più passaggi [I/O]-
>[pure]->[I/O]->[pure]->[I/O]
* Separate commands (updates) from queries (reads).
- CQS = Command-Query Separation
- Design principle: code that returns data (“queries”) should not be mixed up
with code that updates data (“commands”).
- Nel mondo FP: se una funzione ha side-effects non deve ritornare valori
(ritorna Unit); se ritorna dati non deve avere side-effects
- Segregazione dei tipi: non è una buona idea usare lo stesso type per
scrivere e per leggere da DB ==> i dati a disposizione possono essere diversi (id
automatici o dati derivati non li ho in write); evitiamo il coupling fra comandi e
query; in lettura i dati possono essere aggregati da varie tabelle/...
- L'effetto di queste considerazioni porta ad avere due moduli separati: da
un lato il READ MODEL e dall'altro il WRITE MODEL.
- ovvero CQRS = Command-Query Responsibility Segregation
* Bounded contexts must own their own data store

Potrebbero piacerti anche