Anche le "gerarchie della composizione profonda" non sono male?


19

Mi scuso se "Gerarchia della composizione" non è una cosa, ma spiegherò cosa intendo con questo nella domanda.

Non c'è nessun programmatore OO che non abbia mai incontrato una variante di "Mantieni gerarchie ereditarie" o "Preferisci composizione su eredità" e così via. Tuttavia, anche le gerarchie di composizione profonda sembrano problematiche.

Supponiamo di aver bisogno di una raccolta di rapporti che descrivano in dettaglio i risultati di un esperimento:

class Model {
    // ... interface

    Array<Result> m_results;
}

Ogni risultato ha determinate proprietà. Questi includono il tempo dell'esperimento, nonché alcuni metadati di ciascuna fase dell'esperimento:

enum Stage {
    Pre = 1,
    Post
};

class Result {
    // ... interface

    Epoch m_epoch;
    Map<Stage, ExperimentModules> m_modules; 
}

Ok fantastico. Ora, ogni modulo dell'esperimento ha una stringa che descrive il risultato dell'esperimento, nonché una raccolta di riferimenti a set di campioni sperimentali:

class ExperimentalModules {
    // ... interface

    String m_reportText;
    Array<Sample> m_entities;
}

E poi ogni campione ha ... beh, ottieni l'immagine.

Il problema è che se sto modellando oggetti dal mio dominio di applicazione, questo sembra un adattamento molto naturale, ma alla fine, a Resultè solo un muto contenitore di dati! Non sembra utile creare un folto gruppo di classi per questo.

Supponendo che le strutture e le classi di dati mostrate sopra modellino correttamente le relazioni nel dominio dell'applicazione, esiste un modo migliore per modellare un tale "risultato", senza ricorrere a una gerarchia di composizione profonda? Esiste un contesto esterno che possa aiutarti a determinare se un tale progetto è valido o no?


Non capisco la parte del contesto esterno.
Tulains Córdova,

@ TulainsCórdova quello che sto cercando di chiedere qui è "ci sono situazioni in cui la composizione profonda è una buona soluzione"?
AwesomeSauce

Ho aggiunto una risposta
Tulains Córdova,

5
Si, esattamente. I problemi che le persone tentano di risolvere sostituendo l'ereditarietà con la composizione non scompaiono magicamente solo perché hai cambiato una strategia di aggregazione di funzionalità per un'altra. Sono entrambe soluzioni per gestire la complessità e quella complessità è ancora presente e deve essere gestita in qualche modo.
Mason Wheeler,

3
Non vedo nulla di sbagliato nei modelli di dati profondi. Ma non vedo nemmeno come avresti potuto modellarlo con l'ereditarietà poiché si tratta di una relazione 1: N per ogni livello.
usr

Risposte:


17

Questo è ciò di cui parla la Legge di Demetra / principio di minima conoscenza . Un utente Modelnon dovrebbe necessariamente conoscere ExperimentalModulesl'interfaccia per fare il proprio lavoro.

Il problema generale è che quando una classe eredita da un altro tipo o ha un campo pubblico con un tipo o quando un metodo accetta un tipo come parametro o restituisce un tipo, le interfacce di tutti quei tipi diventano parte dell'interfaccia efficace della mia classe. La domanda diventa: questi tipi da cui dipendo: li sta usando tutto il punto della mia classe? O sono solo dettagli di implementazione che ho esposto per caso? Perché se sono dettagli di implementazione, li ho appena esposti e ho permesso agli utenti della mia classe di dipendere da loro - i miei clienti ora sono strettamente accoppiati ai miei dettagli di implementazione.

Quindi, se voglio migliorare la coesione, posso limitare questo accoppiamento scrivendo più metodi che fanno cose utili, invece di richiedere agli utenti di raggiungere le mie strutture private. Qui, potremmo offrire metodi per cercare o filtrare i risultati. Ciò semplifica il refactoring della mia classe, poiché ora posso modificare la rappresentazione interna senza interrompere la mia interfaccia.

Ma questo non è privo di costi: l'interfaccia della mia classe esposta ora si gonfia di operazioni che non sono sempre necessarie. Ciò viola il principio di segregazione dell'interfaccia: non dovrei costringere gli utenti a dipendere da più operazioni di quante ne usino effettivamente. Ed è qui che il design è importante: il design consiste nel decidere quale principio è più importante nel proprio contesto e nel trovare un compromesso adeguato.

Quando si progetta una libreria che deve mantenere la compatibilità dei sorgenti tra più versioni, sarei più propenso a seguire il principio della conoscenza minima con maggiore attenzione. Se la mia libreria utilizza un'altra libreria internamente, non esporrei mai nulla definito da quella libreria ai miei utenti. Se dovessi esporre un'interfaccia dalla libreria interna, creerei un semplice oggetto wrapper. Qui puoi trovare motivi di design come il Bridge Bridge o il Facade Pattern.

Ma se le mie interfacce sono usate solo internamente, o se le interfacce che esponessi fossero molto fondamentali (parte di una libreria standard o di una libreria così fondamentale che non avrebbe mai senso cambiarlo), allora sarebbe inutile nasconderlo queste interfacce. Come al solito, non esiste una risposta valida per tutti.


Hai menzionato una delle maggiori preoccupazioni che stavo avendo; l'idea che ho dovuto raggiungere molto nell'implementazione per fare qualcosa di utile a livelli più alti di astrazione. Una discussione molto utile, grazie.
AwesomeSauce

7

Anche le "gerarchie della composizione profonda" non sono male?

Ovviamente. La regola dovrebbe in realtà dire "mantenere tutte le gerarchie il più piatte possibile".

Ma le gerarchie di composizione profonde sono meno fragili delle gerarchie di eredità profonde e più flessibili da cambiare. Intuitivamente, questo non dovrebbe sorprendere; le gerarchie familiari sono allo stesso modo. La tua relazione con i tuoi parenti di sangue è fissa nella genetica; le tue relazioni con i tuoi amici e il tuo coniuge non lo sono.

Il tuo esempio non mi sembra una "gerarchia profonda". Una forma eredita da un rettangolo che eredita da una serie di punti che eredita dalle coordinate x e y, e non ho ancora ottenuto una testa piena di vapore.


4

Parafrasando Einstein:

mantenere tutte le gerarchie il più possibile piatte, ma non più piatte

Significa che se il problema che stai modellando è così, non dovresti avere scrupoli a modellarlo così com'è.

L'app era solo un foglio di calcolo gigante, quindi non è necessario modellare la gerarchia della composizione così com'è. Altrimenti fallo. Non penso che la cosa sia infinitamente profonda. Mantieni pigri i getter che restituiscono raccolte se il popolamento di tali raccolte è costoso, come chiederle in un repository che le tira da un database. Con pigro intendo, non popolarli fino a quando non sono necessari.


1

Sì, puoi modellarlo come tabelle relazionali

Result {
    Id
    ModelId
    Epoch
}

ecc. che spesso può portare a una programmazione più semplice e alla mappatura semplice su un database. ma a scapito di perdere la dura codifica delle tue relazioni


0

Non so davvero come risolverlo in Java, perché Java è costruito attorno all'idea che tutto è un oggetto. Non è possibile sopprimere le classi intermedie sostituendole con una dizione perché i tipi nei BLOB di dati sono eterogenei (come un timestamp + list o string + list ...). L'alternativa di sostituire una classe container con il suo campo (ad es. Passare una variabile "m_epoch" con un "m_modules") è semplicemente sbagliata, perché sta creando un oggetto implicito (non puoi averne uno senza l'altro) senza particolari beneficiare.

Nel mio linguaggio di predilezione (Python), la mia regola empirica sarebbe quella di non creare un oggetto se nessuna logica particolare (ad eccezione di acquisizione e impostazione di base) appartiene ad esso. Ma dati i tuoi vincoli, penso che la gerarchia di composizione che hai sia il miglior design possibile.

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.