Ereditarietà rispetto al contenimento durante l'estensione di un progetto legacy di grandi dimensioni

4

Ho un progetto Java legacy con un sacco di codice. Il codice utilizza pattern MVC ed è ben strutturato e ben scritto. Ha anche un sacco di test unitari ed è ancora attivamente mantenuto (bug fixing, aggiunta di funzionalità minori). Pertanto voglio mantenere la struttura originale e lo stile del codice il più possibile.

Ho l'obiettivo di iniettare un nuovo comportamento in una classe esistente. Come un'anatra addestrata a saltare. Poi chiedo a tutte le mie anatre di saltare. Coloro che possono - saltare. Quelli che non possono fare nulla che è perfettamente OK con me.

La nuova funzione che sto per aggiungere è concettuale, quindi devo apportare le mie modifiche a tutto il codice. Per minimizzare le modifiche, ho deciso di non estendere le classi esistenti ma di usare il contenimento:

class ExistingClass
{
   // .... existing code


   //  my code adding new functionality
   private ExistingClassExtension extension = new ExistingClassExtension();
   public ExistingClassExtension getExtension() {return extension;}
} 

...
// somewhere in code
ExistingClass instance = new ExistingClass();

...
// when I need a new functionality
instance.getExtension().newMethod1();

Tutte le funzionalità che sto aggiungendo si trovano all'interno di una nuova classe ExistingClassExtension. In realtà sto aggiungendo solo queste 2 linee a ogni classe che deve essere estesa.

Così facendo non ho bisogno di istanziare nuove classi estese su tutto il codice e posso usare test esistenti per assicurarmi che non ci sia regressione.

Tuttavia, i miei colleghi sostengono che in questa situazione non si tratta di un approccio OOP appropriato e ho bisogno di ereditare da ExistingClass per aggiungere una nuova funzionalità.

Che ne pensi? Sono a conoscenza di numerose domande sull'eredità / contenimento qui, ma ritengo che la mia domanda sia diversa.

EDIT: La maggior parte delle persone che rispondono alla mia domanda presumono che una relazione di contenimento sembri così:

class NewClass
{
   OldClass instanceOfOldClass;
   void oldFunctionality() { instanceOfOldClass.oldFunctionality();}

   void newFunctionality() { // new code here }
}

Ma non è una specie di relazione di contenimento?

class OldClass
{
   void OldFunctionality();
   NewClass instanceOfNewClass;

   void NewFunctionality {instanceOfNewClass.NewFunctionality();}
}
    
posta Flot2011 26.05.2014 - 09:36
fonte

3 risposte

1

Il tuo approccio è quello che Michael Feather chiama una classe di germogli , vedi "Funzionante in modo efficace con codice legacy ". È una tecnica ben conosciuta per evitare di cambiare troppe cose nella tua vecchia classe (il che rende il rischio di rompere accidentalmente qualcosa di piccolo). Inoltre, ti dà la possibilità di testare la funzionalità estesa in isolamento (scrivendo unit test per il tuo ExistingClassExtension ), e mantiene le responsabilità separate tra la vecchia e la nuova classe.

L'alternativa (probabilmente peggiore) a questo approccio è di modificare ExistingClass direttamente e implementare direttamente la nuova funzionalità (e se lo fai più di 5 volte, ti ritroverai facilmente con una grossa palla di fango ).

L'ereditarietà è probabilmente l'approccio sbagliato qui, almeno quando il tuo ExistingClassExtension non ha bisogno di accedere ai meccanismi interni di ExistingClass , e dovresti modificare il codice che usa la classe esistente in molti punti sostituendo il nome della vecchia classe dal nome della classe ereditata (probabilmente anche se quel codice non utilizza la funzionalità estesa). Ovviamente, l'ereditarietà proibisce i test in isolamento. Inoltre, pensa a cosa succederà se non hai bisogno di una sola estensione, ma di cinque. Costruirai un albero ereditario a cinque livelli di profondità? In quale ordine?

Quindi parla ai tuoi colleghi, di 'loro che ci sono usi giusti e sbagliati dell'ereditarietà, e questo è molto probabilmente un tipico caso di uso sbagliato .

    
risposta data 29.05.2014 - 10:35
fonte
1

Qui fraintendi l'argomento composizione-eredità. L'idea è che piuttosto che ereditare i diversi comportamenti della tua classe / modulo / componente, tu inietti e contenerlo, in questo modo:

class Duck {
    private ICanFly flyAbility;
    private ICanSpeak soundAbility;
    private ICanSwim swimAbility;

    public Duck(ICanFly wings, ICanSpeak beak, ICanSwim fins) { ... }
}

Quindi le ali possono essere sostituite con un JetPack, il becco con una VoiceCard e le pinne con UnderwaterPropeller, fantastico Duck huh?

L'idea è che i tuoi diversi oggetti possano essere ulteriormente suddivisi in base ai loro comportamenti e che puoi avere Duck che è un uccello che nuota, Bat che è un mammifero che vola e Platypus che è un mammifero che nuota e depone le uova.

Nel tuo caso, continuare con la struttura esistente ed estenderla (la struttura, non necessariamente gli oggetti!) sarebbe la mossa migliore. La domanda è come le cose sono costruite oggigiorno. Non rompere la convenzione.

    
risposta data 27.05.2014 - 10:05
fonte
1

Penso che il tuo approccio sia solo equivalente a modificare ExistingClass . Se inline il ExistingClassExtension all'interno di ExistingClass , vedrai che hai dichiarato una classe interna, che è accessibile attraverso un metodo della classe esterna. Se si aggiungono ulteriormente i metodi della classe interna e i membri pubblici (che non fa differenza per i chiamanti di questo codice, tranne che si salva una chiamata a getExtension ), si noterà che in effetti è stato aggiunto chiaramente alcuni nuovi metodi sotto ExistingClass .

Attualmente hai

class ExistingClass {
    private ExistingClassExtension extension = new ExistingClassExtension();
    public ExistingClassExtension getExtension() {return extension;}
}

class ExistingClassExtension {
    public boolean extendingMethod() { return false; }
}

In linea la classe

class ExistingClass {
    class ExistingClassExtension {
        // ... extendingMethod ...
    }
    public ExistingClassExtension getExtension() {return new ExistingClassExtension();}
}

In linea il metodo

class ExistingClass {
    public boolean extendingMethod() {
        // ... extendingMethod ...
    }
}

Ho erroneamente interpretato "containment" per "composition" e mi aspettavo di trovare un design come questo:

class ExistingClass {
    boolean existingMethod() { return true; }
}

class ExtendingClass {
    private ExistingClass instance = new ExistingClass();
    boolean existingMethod() { return instance.existingMethod();}
    boolean extendingMethod() { return false;}
}

mentre l'opzione di ereditarietà dichiarerebbe semplicemente:

class ExtendingClass extends ExistingClass {
    boolean extendingMethod() { return false; }
}

Il criterio per decidere se comporre o ereditare è stato sintetizzato da Scott Myers in " efficace C ++ " come "Assicurati che i modelli di ereditarietà pubblica siano una relazione" ".

Senza sapere cosa fanno o rappresentano entrambe le classi, è impossibile decidere se uno 'è un' sottotipo dell'altra, ma lasciatemi osservare che in " Efficace Java ", Joshua Bloch consiglia di preferire la composizione sull'ereditarietà nella maggior parte delle situazioni, poiché l'ereditarietà fornisce alla tua nuova classe un'interfaccia che potrebbe essere troppo grande o fuori dal suo controllo. Ciò potrebbe portare a bug sottili o build non funzionanti in futuro, quando una di quelle correzioni minori al codice legacy altera uno dei metodi privati su cui fa affidamento il% di% su di esso.

Suggerirei di prendere in considerazione la possibilità di sostituire questa strategia di "contenimento" con una composizione effettiva delle classi precedenti all'interno di nuove classi. Otterrai tutti i vantaggi elencati nella tua domanda, oltre al vantaggio che tu non hai ereditato un'interfaccia che potrebbe non essere ciò che pensavi o potrebbe cambiare sotto i tuoi piedi.

    
risposta data 27.05.2014 - 12:18
fonte

Leggi altre domande sui tag