I tesori nascosti della composizione degli oggetti

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 |
"Composizione dell'oggetto Assemblaggio o composizione di oggetti per ottenere comportamenti più complessi". ~ Gang of Four, "Design Patterns: Elements of Reusable Object-Oriented Software"
"Favorisci la composizione dell'oggetto rispetto all'eredità di classe". ~ Gang of Four, "Design Patterns".

Uno degli errori più comuni nello sviluppo del software è la tendenza a abusare dell'eredità di classe. L'ereditarietà delle classi è un meccanismo di riutilizzo del codice in cui la forma delle istanze è una relazione con le classi di base. Se sei tentato di modellare il tuo dominio usando le relazioni is-a (ad esempio, un'anatra è-un uccello) sei destinato a creare problemi, perché l'eredità di classe è la forma più stretta di accoppiamento disponibile nel design orientato agli oggetti, che porta a molti problemi comuni, tra cui (tra gli altri):

  • Il fragile problema della classe base
  • Il problema gorilla / banana
  • La duplicazione per problema di necessità

L'ereditarietà delle classi realizza il riutilizzo estraendo un'interfaccia comune in una classe base da cui le sottoclassi possono ereditare, aggiungere e sovrascrivere. Ci sono due parti importanti dell'astrazione:

  • Generalizzazione Processo di estrazione solo delle proprietà e dei comportamenti condivisi che servono il caso d'uso generale
  • Specializzazione Il processo di fornitura dei dettagli di implementazione richiesti per servire il caso speciale

Esistono molti modi per realizzare la generalizzazione e la specializzazione nel codice. Alcune buone alternative all'ereditarietà delle classi includono funzioni semplici, funzioni di ordine superiore e composizione dell'oggetto.

Sfortunatamente, la composizione dell'oggetto è molto fraintesa e molte persone hanno difficoltà a pensare in termini di composizione dell'oggetto. È tempo di esplorare l'argomento in modo più approfondito.

Cos'è la composizione dell'oggetto?

"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

Uno dei motivi della confusione che circonda la composizione dell'oggetto è che qualsiasi insieme di tipi primitivi per formare un oggetto composito è una forma di composizione dell'oggetto, ma le tecniche di ereditarietà sono spesso discusse in contrasto con la composizione dell'oggetto come se fossero cose diverse. Il motivo del duplice significato è che esiste una differenza tra la grammatica e la semantica della composizione dell'oggetto.

Quando parliamo di composizione degli oggetti contro ereditarietà delle classi, non stiamo parlando di tecniche specifiche: stiamo parlando delle relazioni semantiche e del grado di accoppiamento tra gli oggetti componenti. Stiamo parlando di significato anziché di grammatica. Le persone spesso non riescono a fare la distinzione e si impantanano nei dettagli grammaticali. Non riescono a vedere la foresta per gli alberi.

Esistono molti modi diversi per comporre gli oggetti. Diverse forme di composizione produrranno diverse strutture composite e diverse relazioni tra gli oggetti. Quando gli oggetti dipendono dagli oggetti a cui sono correlati, tali oggetti vengono accoppiati, il che significa che la modifica di un oggetto potrebbe interrompere l'altro.

Il consiglio della Banda dei Quattro di "favorire la composizione dell'oggetto rispetto all'eredità di classe" ci invita a pensare ai nostri oggetti come una composizione di oggetti più piccoli, liberamente accoppiati piuttosto che come eredità all'ingrosso da una classe base monolitica. Il GoF descrive gli oggetti strettamente accoppiati come "sistemi monolitici, in cui non è possibile cambiare o rimuovere una classe senza comprendere e cambiare molte altre classi. Il sistema diventa una massa densa che è difficile da imparare, portare e mantenere ".

Tre diverse forme di composizione dell'oggetto

In "Design Patterns", la Gang of Four afferma, "vedrai la composizione degli oggetti applicata più e più volte nei pattern di progettazione" e continua descrivendo vari tipi di relazioni compositive, tra cui aggregazione e delega.

Gli autori di "Design Patterns" stavano lavorando principalmente con Smalltalk e C ++ (in seguito, gli esempi sono stati tradotti in Java). Costruire e modificare le relazioni di oggetti in fase di esecuzione in quelle lingue è molto più complicato di quanto non sia in JavaScript, quindi comprensibilmente non includevano molti dettagli sull'argomento. Tuttavia, nessuna discussione sulla composizione degli oggetti in JavaScript sarebbe completa senza una discussione sull'estensione dinamica degli oggetti, nota anche come concatenazione.

Per motivi di applicabilità a JavaScript e per formare generalizzazioni più pulite, divergeremo leggermente dalle definizioni utilizzate in "Modelli di progettazione". Ad esempio, non richiederemo che le aggregazioni implichino il controllo sui cicli di vita degli oggetti secondari. Questo semplicemente non è vero in una lingua con estensione dinamica degli oggetti.

Selezionare gli assiomi sbagliati può limitare inutilmente una generalizzazione utile e costringerci a trovare un altro nome per un caso speciale della stessa idea generale. Agli sviluppatori di software non piace ripeterci quando non è necessario.

  • Aggregazione Quando un oggetto è formato da una raccolta enumerabile di oggetti secondari. In altre parole, un oggetto che contiene altri oggetti. Ogni oggetto secondario conserva la propria identità di riferimento, in modo tale da poter essere distrutto dall'aggregazione senza perdita di informazioni, ad esempio array, alberi, ecc.
  • Concatenazione Quando si forma un oggetto aggiungendo nuove proprietà a un oggetto esistente. Le proprietà possono essere concatenate una alla volta o copiate da oggetti esistenti, ad esempio i plug-in jQuery vengono creati concatenando nuovi metodi al prototipo del delegato jQuery, jQuery.fn.
  • Delega Quando un oggetto viene inoltrato o delegato a un altro oggetto. ad esempio, il blocco note di Ivan Sutherland (1962) (pubblicato nella sua influente tesi del 1963) includeva esempi con riferimenti a "maestri" a cui erano delegate proprietà condivise. Photoshop include "oggetti intelligenti" che fungono da proxy locali che delegano a una risorsa esterna. Anche i prototipi di JavaScript sono delegati: le istanze di array inoltrano il metodo di array incorporato alle chiamate verso Array.prototype, gli oggetti a Object.prototype, ecc ...

È importante notare che queste diverse forme di composizione non si escludono a vicenda. È possibile implementare la delega utilizzando l'aggregazione e l'ereditarietà delle classi viene implementata utilizzando la delega in JavaScript. Molti sistemi software utilizzano più di un tipo di composizione, ad esempio i plug-in di jQuery utilizzano la concatenazione per estendere il prototipo di delegato jQuery, jQuery.fn. Quando il codice client chiama un metodo plugin, la richiesta viene delegata al metodo concatenato al prototipo delegato.

Nota sugli esempi di codice Gli esempi di codice seguenti condivideranno il seguente codice di installazione:
const objs = [
  {a: 'a', b: 'ab'},
  {b: 'b'},
  {c: 'c', b: 'cb'}
];

Aggregazione

L'aggregazione è quando un oggetto è formato da una raccolta enumerabile di oggetti secondari. Un aggregato è un oggetto che contiene altri oggetti. Ogni oggetto secondario in un'aggregazione mantiene la propria identità di riferimento e potrebbe essere distrutto senza perdita dall'aggregato. Gli aggregati possono essere rappresentati in un'ampia varietà di strutture.

Esempi

  • Array
  • Mappe
  • Imposta
  • grafici
  • Alberi
  • Nodi DOM (un nodo DOM può contenere nodi figlio)
  • Componenti dell'interfaccia utente (un componente può contenere componenti figlio)

Quando usare

Ogni volta che ci sono raccolte di oggetti che devono condividere operazioni comuni, come iterabili, pile, code, alberi, grafici, macchine a stati o il modello composito (quando si desidera che un singolo oggetto condivida la stessa interfaccia di più elementi).

considerazioni

Le aggregazioni sono ottime per l'applicazione di astrazioni universali, come l'applicazione di una funzione a ciascun membro di un aggregato (ad esempio array.map (fn)), la trasformazione di vettori come se fossero valori singoli e così via. Se esistono potenzialmente centinaia di migliaia o milioni di oggetti secondari, tuttavia, l'elaborazione del flusso potrebbe essere più efficiente.

Esempi di codice

Aggregazione di array:

const collection = (a, e) => a.concat ([e]);
const a = objs.reduce (collection, []);
console.log (
  "aggregazione delle raccolte",
  un,
  un [1] .b,
  a [2] .c,
  `chiavi enumerabili: $ {Object.keys (a)}`
);

Questo produrrà:

aggregazione delle raccolte
[{ "A": "a", "b": "ab"}, { "b": "b"}, { "c": "c", "b": "cb"}]
avanti Cristo
chiavi enumerabili: 0,1,2

Aggregazione elenco collegato usando coppie:

coppia const = (a, b) => [b, a];
const l = objs.reduceRight (coppia, []);
console.log (
  "aggregazione dell'elenco collegato",
  l,
  `chiavi enumerabili: $ {Object.keys (l)}`
);
/ *
aggregazione di elenchi collegati
[
  {"a": "a", "b": "ab"}, [
    {"b": "b"}, [
      { "C": "c", "b": "cb"},
      []
    ]
  ]
]
chiavi enumerabili: 0,1
* /

Gli elenchi collegati formano la base di molte altre strutture di dati e aggregazioni, come array, stringhe e vari tipi di alberi. Esistono molti altri possibili tipi di aggregazione. Non li tratteremo in modo approfondito qui.

Concatenazione

La concatenazione si verifica quando si forma un oggetto aggiungendo nuove proprietà a un oggetto esistente.

Esempi

  • I plugin vengono aggiunti a jQuery.fn tramite concatenazione
  • Riduttori di stato (ad es. Redux)
  • Mixin funzionali

Quando utilizzarlo: in qualsiasi momento sarebbe utile assemblare progressivamente strutture di dati in fase di runtime, ad esempio unendo oggetti JSON, idratando lo stato dell'applicazione da più origini, creando aggiornamenti allo stato immutabile (unendo lo stato precedente con nuovi dati), ecc ...

considerazioni

  • Fai attenzione a mutare gli oggetti esistenti. Lo stato mutabile condiviso è una ricetta per molti bug.
  • È possibile imitare le gerarchie di classi e le relazioni is-a con concatenazione. Si applicano gli stessi problemi. Pensa in termini di composizione di piccoli oggetti indipendenti piuttosto che ereditare oggetti di scena da un'istanza "base" e applicare l'ereditarietà differenziale.
  • Fai attenzione alle dipendenze implicite tra i componenti.
  • Le collisioni dei nomi di proprietà vengono risolte in base all'ordine di concatenazione: vincite last-in. Ciò è utile per comportamenti predefiniti / sostituiti, ma può essere problematico se l'ordine non ha importanza.
const concatenate = (a, o) => ({... a, ... o});
const c = objs.reduce (concatenate, {});
console.log (
  'concatenazione',
  c,
  `chiavi enumerabili: $ {Object.keys (c)}`
);
// concatenazione {a: 'a', b: 'cb', c: 'c'} chiavi enumerabili: a, b, c

Delegazione

La delega è quando un oggetto inoltra o delega a un altro oggetto.

Esempi

  • I tipi predefiniti di JavaScript utilizzano la delega per inoltrare le chiamate del metodo integrato alla catena di prototipi. ad esempio, [] .map () delega a Array.prototype.map (), obj.hasOwnProperty () delega a Object.prototype.hasOwnProperty () e così via.
  • I plug-in jQuery si basano sulla delega per condividere i metodi integrati e plug-in tra tutte le istanze di oggetti jQuery.
  • I "maestri" di Sketchpad erano delegati dinamici. Le modifiche al delegato si rifletterebbero istantaneamente in tutte le istanze di oggetto.
  • Photoshop utilizza delegati chiamati "oggetti intelligenti" per fare riferimento a immagini e risorse definite in file separati. Le modifiche all'oggetto a cui fanno riferimento gli oggetti intelligenti si riflettono in tutte le istanze dell'oggetto intelligente.

Quando usare

  1. Conservare la memoria: ogni volta che ci possono essere potenzialmente molte istanze di un oggetto e sarebbe utile condividere proprietà o metodi identici tra ciascuna istanza che altrimenti richiederebbero di allocare più memoria.
  2. Aggiorna dinamicamente molte istanze: ogni volta che molte istanze di un oggetto devono condividere uno stato identico che potrebbe essere necessario aggiornare dinamicamente e le modifiche si riflettono istantaneamente in ogni istanza, ad esempio i "master" di Sketchpad o gli "oggetti intelligenti" di Photoshop.

considerazioni

  • La delega viene comunemente utilizzata per imitare l'ereditarietà delle classi in JavaScript (cablata dalla parola chiave extends), ma molto raramente è effettivamente necessaria.
  • La delega può essere utilizzata per imitare esattamente il comportamento e i limiti dell'eredità di classe. In effetti, l'ereditarietà delle classi in JavaScript si basa su delegati statici tramite la catena di deleghe del prototipo. Evita il pensiero è.
  • Gli oggetti di scena delegati non sono enumerabili usando meccanismi comuni come Object.keys (instanceObj).
  • La delega consente di risparmiare memoria a scapito delle prestazioni di ricerca della proprietà e alcune ottimizzazioni del motore JS vengono disattivate per i delegati dinamici (i delegati che cambiano dopo essere stati creati). Tuttavia, anche nel caso più lento, le prestazioni di ricerca delle proprietà sono misurate in milioni di operazioni al secondo - è probabile che questo non sia il collo di bottiglia a meno che non si stia creando una libreria di utilità per operazioni su oggetti o programmazione grafica, ad esempio RxJS o tre. js.
  • È necessario distinguere tra stato dell'istanza e stato delegato.
  • Lo stato condiviso su delegati dinamici non è sicuro. Le modifiche sono condivise tra tutte le istanze. Lo stato condiviso su delegati dinamici è comunemente (ma non sempre) un bug.
  • Le classi ES6 non creano delegati riassegnabili in ES6. Potrebbero sembrare funzionare in Babel, ma falliranno duramente in ambienti ES6 reali.

Esempio di codice

const delegate = (a, b) => Object.assign (Object.create (a), b);

const d = objs.reduceRight (delegato, {});

console.log (
  'delegazione',
  d,
  `chiavi enumerabili: $ {Object.keys (d)}`
);

// delega {a: 'a', b: 'ab'} chiavi enumerabili: a, b

console.log (d.b, d.c); // ab c

Conclusione

Abbiamo imparato:

  • Tutti gli oggetti realizzati con altri oggetti e primitive di linguaggio sono oggetti compositi.
  • L'atto di creare un oggetto composito è noto come composizione.
  • Esistono diversi tipi di composizione dell'oggetto.
  • Le relazioni e le dipendenze che formiamo quando componiamo gli oggetti differiscono a seconda della composizione degli oggetti.
  • Le relazioni Is-a (il tipo formato dall'eredità di classe) sono la forma più stretta di accoppiamento nella progettazione di OO e dovrebbero essere generalmente evitate quando sono pratiche.
  • La Banda dei Quattro ci ammonisce di comporre oggetti assemblando caratteristiche più piccole per formare un tutto più grande, piuttosto che ereditare da una classe base monolitica o oggetto base. "Favorisci la composizione degli oggetti rispetto all'eredità di classe."
  • L'aggregazione compone gli oggetti in raccolte enumerabili in cui ogni membro della raccolta conserva la propria identità, ad esempio array, albero DOM, ecc ...
  • La delega compone gli oggetti collegando una catena di delega di oggetti in cui un oggetto inoltra o delega le ricerche di proprietà a un altro oggetto. ad es. [] .map () delega ad Array.prototype.map ()
  • La concatenazione compone gli oggetti estendendo un oggetto esistente con nuove proprietà, ad esempio Object.assign (destinazione, a, b), {... a, ... b}.
  • Le definizioni di diversi tipi di composizione dell'oggetto non si escludono a vicenda. La delega è un sottoinsieme di aggregazione, la concatenazione può essere utilizzata per formare delegati e aggregati e così via ...

Questi non sono gli unici tre tipi di composizione dell'oggetto. È anche possibile formare relazioni sciolte e dinamiche tra oggetti attraverso relazioni di conoscenza / associazione in cui gli oggetti vengono passati come parametri ad altri oggetti (iniezione di dipendenza) e così via.

Tutto lo sviluppo del software è composizione. Esistono modi semplici e flessibili per comporre oggetti e modi fragili e artritici. Alcune forme di composizione dell'oggetto formano relazioni vagamente accoppiate tra oggetti, mentre altre formano un accoppiamento molto stretto.

Cercare modi per comporre dove una piccola modifica ai requisiti del programma richiederebbe solo una piccola modifica all'implementazione del codice. Esprimi la tua intenzione in modo chiaro e conciso e ricorda: se pensi di aver bisogno dell'eredità di classe, è molto probabile che ci sia un modo migliore per farlo.

Maggiori informazioni su EricElliottJS.com

Le lezioni video sulla composizione degli oggetti sono disponibili per i membri di EricElliottJS.com. Se non sei un membro, iscriviti oggi.

Inizia la tua lezione gratuita su EricElliottJS.com

Eric Elliott è l'autore dei libri, "Software di composizione" e "Programmazione di applicazioni JavaScript". Come co-fondatore di EricElliottJS.com e DevAnywhere.io, insegna agli sviluppatori le abilità di sviluppo software essenziali. Crea e consiglia team di sviluppo per progetti crittografici e ha contribuito alle esperienze software per Adobe Systems, Zumba Fitness, The Wall StreetJournal, 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.