Software di composizione: un'introduzione

Smoke Art Cubes to Smoke - MattysFlicks - (CC BY 2.0)
Nota: fa parte della serie "Composing Software" (ora un libro!) Sull'apprendimento da zero della programmazione funzionale e delle tecniche software compositive in JavaScript ES6 +. Rimanete sintonizzati. C'è molto di più a venire!
Acquista il libro | Indice | Avanti>
Composizione: "L'atto di combinare parti o elementi per formare un tutto". ~ Dictionary.com

Durante la mia prima lezione di programmazione al liceo, mi è stato detto che lo sviluppo del software è "l'atto di scomporre un problema complesso in problemi più piccoli e comporre soluzioni semplici per formare una soluzione completa al problema complesso".

Uno dei miei più grandi rimpianti nella vita è che non sono riuscito a capire presto il significato di quella lezione. Ho imparato l'essenza del design del software troppo tardi nella vita.

Ho intervistato centinaia di sviluppatori. Quello che ho imparato da quelle sessioni è che non sono solo. Pochissimi sviluppatori di software di lavoro hanno una buona conoscenza dell'essenza dello sviluppo del software. Non sono a conoscenza degli strumenti più importanti di cui disponiamo o di come utilizzarli. Il 100% ha lottato per rispondere a una o entrambe le domande più importanti nel campo dello sviluppo del software:

  • Cos'è la composizione della funzione?
  • Cos'è la composizione dell'oggetto?

Il problema è che non puoi evitare la composizione solo perché non ne sei consapevole. Lo fai ancora, ma lo fai male. Scrivi codice con più bug e rendi più difficile la comprensione da parte di altri sviluppatori. Questo è un grosso problema Gli effetti sono molto costosi. Dedichiamo più tempo alla manutenzione del software che a crearlo da zero e i nostri bug hanno un impatto su miliardi di persone in tutto il mondo.

Il mondo intero funziona oggi con software. Ogni nuova auto è un mini supercomputer su ruote e i problemi con la progettazione del software causano incidenti reali e costano vite umane reali. Nel 2013, una giuria ha ritenuto il team di sviluppo software della Toyota colpevole di "disprezzo spericolato" dopo che un'indagine per incidente ha rivelato il codice spaghetti con 10.000 variabili globali.

Gli hacker e i governi accumulano bug per spiare le persone, rubare carte di credito, sfruttare le risorse informatiche per lanciare attacchi DDoS (Distributed Denial of Service), rompere le password e persino manipolare le elezioni.

Dobbiamo fare di meglio.

Componi software ogni giorno

Se sei uno sviluppatore di software, componi funzioni e strutture di dati ogni giorno, che tu lo sappia o no. Puoi farlo consapevolmente (e meglio), o puoi farlo accidentalmente, con nastro adesivo e colla folle.

Il processo di sviluppo del software sta suddividendo grandi problemi in problemi più piccoli, costruendo componenti che risolvono quei problemi più piccoli, quindi componendo insieme quei componenti per formare un'applicazione completa.

Funzioni di composizione

La composizione della funzione è il processo di applicazione di una funzione all'output di un'altra funzione. Nell'algebra, date due funzioni, fand g, (f ∘ g) (x) = f (g (x)). Il cerchio è l'operatore di composizione. È comunemente pronunciato "composto con" o "dopo". Puoi dirlo ad alta voce come "f composto da g uguale a f di g di x" o "f dopo g uguale a f di g di x". Diciamo f dopo g perché g viene valutato per primo, quindi il suo output viene passato come argomento a f.

Ogni volta che scrivi codice in questo modo, stai componendo funzioni:

const g = n => n + 1;
const f = n => n * 2;
const doStuff = x => {
  const afterG = g (x);
  const afterF = f (afterG);
  ritorno dopo F;
};
doStuff (20); // 42

Ogni volta che scrivi una catena di promesse, stai componendo funzioni:

const g = n => n + 1;
const f = n => n * 2;
const wait = time => new Promise (
  (risolve, rifiuta) => setTimeout (
    risolvere,
    tempo
  )
);
attendere (300)
  .then (() => 20)
  .then (g)
  .then (f)
  .then (value => console.log (value)) // 42
;

Allo stesso modo, ogni volta che esegui una catena di chiamate di metodi array, metodi lodash, osservabili (RxJS, ecc ...) stai componendo funzioni. Se stai concatenando, stai componendo. Se passi valori di ritorno in altre funzioni, stai componendo. Se chiami due metodi in sequenza, stai componendo usando questo come dati di input.

Se stai concatenando, stai componendo.

Quando componi le funzioni intenzionalmente, lo farai meglio.

Composendo le funzioni intenzionalmente, possiamo migliorare la nostra funzione doStuff () in una semplice riga:

const g = n => n + 1;
const f = n => n * 2;
const doStuffBetter = x => f (g (x));
doStuffBetter (20); // 42

Un'obiezione comune a questo modulo è che è più difficile eseguire il debug. Ad esempio, come lo scriveremmo usando la composizione della funzione?

const doStuff = x => {
  const afterG = g (x);
  console.log (`after g: $ {afterG}`);
  const afterF = f (afterG);
  console.log (`after f: $ {afterF}`);
  ritorno dopo F;
};
doStuff (20); // =>
/ *
"dopo g: 21"
"dopo f: 42"
* /

Innanzitutto, riassumiamo che "dopo f", "dopo g" accedendo a una piccola utility chiamata trace ():

const trace = label => value => {
  console.log (`$ {label}: $ {value}`);
  valore di ritorno;
};

Ora possiamo usarlo in questo modo:

const doStuff = x => {
  const afterG = g (x);
  trace ('after g') (afterG);
  const afterF = f (afterG);
  trace ('after f') (afterF);
  ritorno dopo F;
};
doStuff (20); // =>
/ *
"dopo g: 21"
"dopo f: 42"
* /

Le librerie di programmazione funzionale popolari come Lodash e Ramda includono utilità per facilitare la composizione delle funzioni. Puoi riscrivere la funzione sopra in questo modo:

importare pipe da 'lodash / fp / flow';
const doStuffBetter = pipe (
  g,
  trace ('after g'),
  f,
  trace ('after f')
);
doStuffBetter (20); // =>
/ *
"dopo g: 21"
"dopo f: 42"
* /

Se vuoi provare questo codice senza importare qualcosa, puoi definire pipe in questo modo:

// pipe (... fns: [... Function]) => x => y
const pipe = (... fns) => x => fns.reduce ((y, f) => f (y), x);

Non preoccuparti se non stai ancora seguendo come funziona. Più avanti esploreremo la composizione delle funzioni in modo molto più dettagliato. In effetti, è così essenziale, lo vedrai definito e dimostrato molte volte in questo testo. Il punto è aiutarti a familiarizzarti così tanto che la sua definizione e il suo utilizzo diventano automatici. Sii tutt'uno con la composizione.

pipe () crea una pipeline di funzioni, passando l'output di una funzione all'input di un'altra. Quando usi pipe () (e il suo gemello, compose ()) Non hai bisogno di variabili intermedie. Scrivere funzioni senza menzionare gli argomenti si chiama stile senza punti. Per farlo, chiamerai una funzione che restituisce la nuova funzione, piuttosto che dichiararla esplicitamente. Ciò significa che non avrai bisogno della parola chiave funzione o della sintassi della freccia (=>).

Lo stile senza punti può essere portato troppo lontano, ma un po 'qua e là è fantastico perché quelle variabili intermedie aggiungono complessità inutili alle tue funzioni.

La riduzione della complessità comporta numerosi vantaggi:

Memoria di lavoro

Il cervello umano medio ha solo poche risorse condivise per quanti discreti nella memoria di lavoro e ogni variabile consuma potenzialmente uno di quei quanti. Man mano che aggiungi più variabili, la nostra capacità di richiamare con precisione il significato di ciascuna variabile diminuisce. I modelli di memoria di lavoro implicano in genere 4-7 quanti discreti. Al di sopra di questi numeri, i tassi di errore aumentano notevolmente.

Usando il modulo pipe, abbiamo eliminato 3 variabili, liberando quasi la metà della nostra memoria di lavoro disponibile per altre cose. Ciò riduce significativamente il nostro carico cognitivo. Gli sviluppatori di software tendono ad essere più bravi a spostare i dati nella memoria di lavoro rispetto alla persona media, ma non tanto a indebolire l'importanza della conservazione.

Rapporto segnale-rumore

Il codice conciso migliora anche il rapporto segnale-rumore del codice. È come ascoltare una radio: quando la radio non è sintonizzata correttamente sulla stazione, si sente un sacco di rumore interferente ed è più difficile ascoltare la musica. Quando lo sintonizzi sulla stazione corretta, il rumore scompare e ricevi un segnale musicale più forte.

Il codice è allo stesso modo. Un'espressione più concisa del codice porta a una migliore comprensione. Alcuni codici ci forniscono informazioni utili e alcuni di essi occupano solo spazio. Se riesci a ridurre la quantità di codice che utilizzi senza ridurre il significato che viene trasmesso, renderai il codice più facile da analizzare e comprendere per le altre persone che hanno bisogno di leggerlo.

Superficie per bug

Dai un'occhiata alle funzioni prima e dopo. Sembra che la funzione sia andata a dieta e abbia perso un sacco di peso. Questo è importante perché un codice aggiuntivo significa una superficie extra in cui nascondersi i bug, il che significa che più bug si nasconderanno al suo interno.

Meno codice = meno area di superficie per i bug = meno bug.

Comporre oggetti

"Favorisci la composizione dell'oggetto rispetto all'eredità di classe", la banda di quattro, "Modelli di progettazione: elementi di software orientato agli oggetti riutilizzabili"
"Nell'informatica, un tipo di dati composito o un tipo di dati composti è qualsiasi tipo di dati che può essere costruito in un programma utilizzando i tipi di dati primitivi del linguaggio di programmazione e altri tipi compositi. […] L'atto di costruire un tipo composito è noto come composizione. ”~ Wikipedia

Questi sono primitivi:

const firstName = 'Claude';
const lastName = 'Debussy';

E questo è un composito:

const fullName = {
  nome di battesimo,
  cognome
};

Allo stesso modo, tutti gli array, set, mappe, mappe deboli, array tipizzati, ecc ... sono tipi di dati compositi. Ogni volta che costruisci una struttura di dati non primitiva, stai eseguendo una sorta di composizione di oggetti.

Si noti che Gang of Four definisce un modello chiamato pattern composito che è un tipo specifico di composizione ricorsiva di oggetti che consente di trattare i singoli componenti e i composti aggregati in modo identico. Alcuni sviluppatori si confondono, pensando che il modello composito sia l'unica forma di composizione dell'oggetto. Non confonderti. Esistono molti diversi tipi di composizione dell'oggetto.

The Gang of Four continua, "vedrai la composizione degli oggetti applicata più e più volte nei modelli di progettazione", e poi catalogano tre tipi di relazioni compositive degli oggetti, inclusa la delega (come usata nello stato, la strategia e i modelli di visitatori), conoscenza (quando un oggetto conosce un altro oggetto per riferimento, generalmente passato come parametro: una relazione usi-una, ad esempio, un gestore di richieste di rete potrebbe passare un riferimento a un logger per registrare la richiesta - il gestore di richieste utilizza un logger), e aggregazione (quando gli oggetti figlio fanno parte di un oggetto padre: una relazione has-a, ad esempio, i figli DOM sono elementi componenti in un nodo DOM - Un nodo DOM ha figli).

L'ereditarietà delle classi può essere utilizzata per costruire oggetti compositi, ma è un modo restrittivo e fragile per farlo. Quando la banda di quattro dice "favorisci la composizione degli oggetti rispetto all'eredità di classe", ti consigliano di utilizzare approcci flessibili alla costruzione di oggetti compositi, piuttosto che l'approccio rigido e strettamente accoppiato dell'eredità di classe.

Useremo una definizione più generale di composizione dell'oggetto da "Metodi categorici in informatica: con aspetti di topologia" (1989):

"Gli oggetti compositi si formano unendo gli oggetti in modo tale che ciascuno di questi sia" parte "del primo."

Un altro buon riferimento è "Software affidabile attraverso il design composito", Glenford J Myers, 1975. Entrambi i libri sono fuori stampa da molto tempo, ma puoi ancora trovare venditori su Amazon o eBay se desideri esplorare l'argomento della composizione degli oggetti in più profondità tecnica.

L'eredità di classe è solo un tipo di costruzione di oggetti compositi. Tutte le classi producono oggetti compositi, ma non tutti gli oggetti compositi sono prodotti da classi o eredità di classe. "Favorire la composizione degli oggetti rispetto all'ereditarietà delle classi" significa che è necessario formare oggetti compositi da piccole parti componenti, piuttosto che ereditare tutte le proprietà di un antenato in una gerarchia di classi. Quest'ultimo causa una grande varietà di problemi ben noti nella progettazione orientata agli oggetti:

  • Il problema dell'accoppiamento stretto: poiché le classi figlio dipendono dall'implementazione della classe padre, l'ereditarietà delle classi è l'accoppiamento più stretto disponibile nella progettazione orientata agli oggetti.
  • Il fragile problema della classe base: a causa dell'accoppiamento stretto, le modifiche alla classe base possono potenzialmente rompere un gran numero di classi discendenti, potenzialmente nel codice gestito da terze parti. L'autore potrebbe violare il codice di cui non sono a conoscenza.
  • Il problema della gerarchia inflessibile: con le tassonomie dei singoli antenati, dato il tempo e l'evoluzione sufficienti, tutte le tassonomie di classe sono infine sbagliate per i nuovi casi d'uso.
  • La duplicazione per problema di necessità: a causa di gerarchie inflessibili, i nuovi casi d'uso sono spesso implementati per duplicazione, anziché per estensione, portando a classi simili che sono inaspettatamente divergenti. Una volta avviata la duplicazione, non è chiaro da quale classe dovrebbero discendere le nuove classi o perché.
  • Il problema gorilla / banana: "... il problema con i linguaggi orientati agli oggetti è che hanno tutto questo ambiente implicito che portano con sé. Volevi una banana ma quello che hai ottenuto è stato un gorilla che regge la banana e l'intera giungla. "~ Joe Armstrong," Coders at Work "

La forma più comune di composizione di oggetti in JavaScript è nota come concatenazione di oggetti (aka composizione di mixin). Funziona come un gelato. Si inizia con un oggetto (come il gelato alla vaniglia), quindi si mescolano le caratteristiche desiderate. Aggiungi un po 'di noci, caramello, ricciolo di cioccolato e finisci con il gelato di ricciolo di cioccolato al caramello.

Creazione di compositi con eredità di classe:

classe Foo {
  constructor () {
    this.a = 'a'
  }
}
class Bar estende Foo {
  costruttore (opzioni) {
    super (opzioni);
    this.b = 'b'
  }
}
const myBar = new Bar (); // {a: 'a', b: 'b'}

Creazione di compositi con composizione mixin:

const a = {
  aa'
};
const b = {
  b: 'b'
};
const c = {... a, ... b}; // {a: 'a', b: 'b'}

Esploreremo altri stili di composizione degli oggetti in modo più approfondito in seguito. Per ora, la tua comprensione dovrebbe essere:

  1. C'è più di un modo per farlo.
  2. Alcuni modi sono migliori di altri.
  3. Si desidera selezionare la soluzione più semplice e flessibile per l'attività da svolgere.

Conclusione

Non si tratta di programmazione funzionale (FP) vs programmazione orientata agli oggetti (OOP) o di una lingua rispetto a un'altra. I componenti possono assumere la forma di funzioni, strutture di dati, classi, ecc. Diversi linguaggi di programmazione tendono ad offrire diversi elementi atomici per i componenti. Java offre classi, Haskell offre funzioni, ecc ... Ma indipendentemente dal linguaggio e dal paradigma che preferisci, non puoi evitare di comporre funzioni e strutture di dati. Alla fine, questo è tutto ciò che si riduce.

Parleremo molto della programmazione funzionale, perché le funzioni sono le cose più semplici da comporre in JavaScript e la comunità di programmazione funzionale ha investito molto tempo e sforzi nel formalizzare le tecniche di composizione delle funzioni.

Quello che non faremo è dire che la programmazione funzionale è migliore della programmazione orientata agli oggetti o che devi scegliere l'uno rispetto all'altro. OOP vs FP è una falsa dicotomia. Ogni vera applicazione Javascript che ho visto negli ultimi anni mescola ampiamente FP e OOP.

Useremo la composizione degli oggetti per produrre tipi di dati per la programmazione funzionale e la programmazione funzionale per produrre oggetti per OOP.

Non importa come scrivi il software, dovresti comporlo bene.

L'essenza dello sviluppo del software è la composizione.

Uno sviluppatore di software che non capisce la composizione è come un costruttore di casa che non conosce bulloni o chiodi. La creazione di software senza consapevolezza della composizione è come un costruttore di case che unisce pareti con nastro adesivo e colla folle.

È tempo di semplificare e il modo migliore per semplificare è arrivare all'essenza. Il problema è che quasi nessuno nel settore ha una buona conoscenza degli elementi essenziali. Come industria abbiamo fallito te, lo sviluppatore del software. È nostra responsabilità come industria formare meglio gli sviluppatori. Dobbiamo migliorare. Dobbiamo assumerci la responsabilità. Oggi tutto funziona con il software, dall'economia alle attrezzature mediche. Non c'è letteralmente angolo della vita umana su questo pianeta che non è influenzato dalla qualità del nostro software. Dobbiamo sapere cosa stiamo facendo.

È tempo di imparare a comporre il software.

Acquista il libro | Indice | Avanti>

Maggiori informazioni su EricElliottJS.com

Le lezioni video con sfide di codice interattivo sono disponibili per i membri di EricElliottJS.com. Se non sei un membro, iscriviti oggi.

Eric Elliott è un esperto di sistemi distribuiti e autore dei libri "Composing Software" e "Programmazione di applicazioni JavaScript". Come co-fondatore di DevAnywhere.io, insegna agli sviluppatori le competenze di cui hanno bisogno per lavorare in remoto e abbracciare l'equilibrio tra lavoro e vita privata. Crea e fornisce consulenza a team di sviluppo per progetti crittografici e ha contribuito alle esperienze software per Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC e i migliori artisti discografici tra cui Usher, Frank Ocean, Metallica e molti altri.

Gode ​​di uno stile di vita remoto con la donna più bella del mondo.