Anche le "gerarchie di composizione profonde" non sono buone?

18

Ci scusiamo se "Gerarchia di composizione" non è una cosa, ma spiegherò cosa intendo con la domanda.

Non esiste alcun programmatore OO che non abbia riscontrato una variazione di "Mantieni le gerarchie di ereditarietà piatte" o "Preferisci la composizione sull'ereditarietà" e così via. Tuttavia, anche le gerarchie di composizione profonde sembrano problematiche.

Diciamo che abbiamo bisogno di una raccolta di rapporti che descrivono 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 e alcuni metadati di ogni fase dell'esperimento:

enum Stage {
    Pre = 1,
    Post
};

class Result {
    // ... interface

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

Ok, fantastico. Ora, ogni modulo sperimentale ha una stringa che descrive il risultato dell'esperimento, nonché una raccolta di riferimenti a insiemi 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 applicativo, questo sembra un adattamento molto naturale, ma alla fine della giornata, un Result è solo un contenitore di dati stupido! Non sembra utile creare un vasto gruppo di classi per questo.

Supponendo che le strutture dati e le classi mostrate sopra modellano 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 potrebbe aiutarti a determinare se un tale progetto è buono o no?

    
posta AwesomeSauce 04.11.2016 - 20:23
fonte

5 risposte

17

Questo è ciò che la Legge di Demetra / principio di minima conoscenza riguarda. Un utente di Model non deve necessariamente conoscere l'interfaccia ExperimentalModules 'per fare il proprio lavoro.

Il problema generale è che quando una classe eredita da un altro tipo o ha un campo pubblico con qualche tipo o quando un metodo prende un parametro come parametro o restituisce un tipo, le interfacce di tutti questi tipi diventano parte dell'interfaccia effettiva di la mia classe. La domanda diventa: questi tipi di cui dipendo: sta usando quelli dell'intero punto della mia classe? O sono solo dettagli di implementazione che ho accidentalmente esposto? Perché se si tratta di dettagli di implementazione, li ho appena esposti e consentito agli utenti della mia classe di dipendere da loro: i miei clienti ora sono strettamente associati 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ò rende la mia classe più facile da refactoring, dal momento che ora posso modificare la rappresentazione interna senza un'interruzione della mia interfaccia.

Ma questo non è esente da costi: l'interfaccia della mia classe esposta ora diventa 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 quelle che effettivamente usano. Ed è qui che design è importante: il design riguarda decidere quale principio è più importante nel tuo contesto e trovare un compromesso adeguato.

Quando si progetta una libreria che deve mantenere la compatibilità tra sorgenti su più versioni, sarei più incline a seguire più attentamente il principio della minima conoscenza. Se la mia libreria utilizza internamente un'altra libreria, non esporrò mai nulla definito da quella libreria ai miei utenti. Se ho bisogno di esporre un'interfaccia dalla libreria interna, creerei un semplice oggetto wrapper. I modelli di progettazione come il motivo a ponte o il motivo a facciate possono aiutarti qui.

Ma se le mie interfacce sono usate solo internamente, o se le interfacce che vorrei esporre sono molto fondamentali (parte di una libreria standard, o di una libreria così fondamentale che non avrebbe mai senso cambiarla), allora potrebbe essere inutile nascondere queste interfacce. Come al solito, non ci sono risposte a taglia unica.

    
risposta data 04.11.2016 - 21:01
fonte
7

Aren't “deep composition hierarchies” bad too?

Naturalmente. La regola dovrebbe effettivamente dire "mantenere tutte le gerarchie il più piatte possibile".

Ma le gerarchie di composizione profonde sono meno fragili delle gerarchie di ereditarietà profonda e più flessibili da cambiare. Intuitivamente, questo non dovrebbe sorprendere; le gerarchie familiari sono allo stesso modo. Il tuo rapporto con i tuoi parenti di sangue è fissato in genetica; i tuoi rapporti con i tuoi amici e il tuo coniuge non lo sono.

Il tuo esempio non mi sembra una "gerarchia profonda". Un modulo eredita da un rettangolo che eredita da un insieme di punti che eredita dalle coordinate xey, e non ho ancora ottenuto una testa piena di vapore ancora.

    
risposta data 04.11.2016 - 20:31
fonte
4

Parafrasando Einstein:

keep all hierarchies as flat as possible, but not flatter

Se il problema che stai modellando è così, non dovresti avere problemi a modellarlo così com'è.

L'app era solo un gigantesco foglio di calcolo, quindi non è necessario modellare la gerarchia della composizione così com'è. Altrimenti fallo. Non penso che la cosa sia infinitamente profonda. È sufficiente mantenere i getter che restituiscono le raccolte pigre se la compilazione di tali raccolte è costosa, come chiederle a un repository che le attira da un database. Per pigro intendo, non popolarli finché non sono necessari.

    
risposta data 04.11.2016 - 20:53
fonte
1

Sì, puoi modellarlo come tabelle relazionali

Result {
    Id
    ModelId
    Epoch
}

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

    
risposta data 04.11.2016 - 20:38
fonte
0

Non so davvero come risolvere questo in Java, perché Java è costruito attorno all'idea che tutto sia un oggetto. Non è possibile sopprimere le classi intermedie sostituendole con una dizione perché i tipi nei blob di dati sono eterogenei (ad esempio un elenco di timestamp + o un elenco di stringhe + ...). L'alternativa di sostituire una classe contenitore con il suo campo (ad esempio passare una variabile "m_epoch" con un "m_modules") è semplicemente cattiva, perché sta facendo un oggetto implicito (non puoi averne uno senza l'altro) senza particolari beneficio.

Nel mio linguaggio di predilezione (Python), la mia regola del pollice sarebbe quella di non creare un oggetto se nessuna logica particolare (ad eccezione di ottenere e impostare di base) appartiene ad esso. Ma visti i tuoi limiti, penso che la gerarchia di composizione che hai sia la migliore possibile.

    
risposta data 04.11.2016 - 20:35
fonte

Leggi altre domande sui tag