Devo ancora preferire la composizione sull'ereditarietà se ho bisogno che l'interfaccia e i membri della classe siano generici?

2

Secondo Perché dovrei preferire la composizione all'ereditarietà? , Preferirei la composizione sull'ereditarietà. Ma cosa succede se ho bisogno di accedere all'interfaccia e al membro della classe in modo generico? Ad esempio, ho la classe genitore Shape e alcune classi child:

public class Shape{
    private String customId;
    public String getCustomId(){
        return customId;
    }
    public void setCustomId(String aCustomId){
        customId=aCustomId;
    }

    public abstract void draw();
}

public class Circle extends Shape{
    @Override
    public void draw(){
    }
} 

Alcuni codici per utilizzare l'interfaccia Shape

for(Shape s : shapeArray){
    s.draw();
    //access customId
    if(s.getCustomId().equals(...)){
    }
}

...

public class User{
    private Shape shape;
    public void printInfo(){
        System.out.println("User Shape id:"+shape.getCustomId());
    }
}

Ma sta violando la composizione sull'ereditarietà, quindi cambio l'ereditarietà in composition + interface:

public interface Shape{
    String getCustomId();
    void setCustomId(String aCustomId);
    void draw();
}

public class Circle implements Shape{
    private String customId;
    public String getCustomId(){
        return customId;
    }
    public void setCustomId(String aCustomId){
        customId=aCustomId;
    }

    public void draw(){
    }
}

Ho trovato che la composizione è più non gestibile in questo caso perché:

  1. quando aggiungo una nuova classe, devo copiare e incollare

    private String customId;
    public String getCustomId(){
        return customId;
    }
    public void setCustomId(String aCustomId){
        customId=aCustomId;
    }
    
  2. Se ho bisogno di aggiungere un nuovo membro della classe figlio a Shape, devo anche modificare tutte le classi child per aggiungere il membro della classe e anche getter e setter al suo interno

Quindi la mia domanda è: ho bisogno di "composizione sull'ereditarietà" se devo accedere sia al membro della classe che ai metodi in modo generico?

    
posta mmmaaa 13.06.2018 - 04:05
fonte

6 risposte

8

"preferisci la composizione sull'ereditarietà" non è una regola del braindead che dice che si dovrebbe evitare l'ereditarietà in tutte le circostanze - che mancherebbe il punto di quella raccomandazione, e non sarebbe altro che una forma di programmazione cargo-cult .

Che cosa significa in realtà la regola: usare l'ereditarietà solo per riutilizzare è spesso lo strumento sbagliato, specialmente quando è possibile ottenere un riutilizzo equivalente per composizione. Lo stesso vale quando ci sono parti ereditate che sono solo necessarie a volte , o devono essere cambiate in fase di runtime. Tuttavia, per il polimorfismo, l'utilizzo dell'eredità va bene.

Nel tuo esempio, se ogni Circle è a Shape con un metodo polimorfo draw , l'utilizzo dell'ereditarietà è un'opzione perfettamente valida per questa parte del modello di classe. Se nel tuo contesto specifico ogni tipo di oggetto Shape sempre, sempre, sempre ha bisogno di un Id , senza eccezioni, quindi inserire l'implementazione correlata nella classe base Shape è almeno una soluzione pragmatica, non perfetta, ma ancora ok.

La situazione cambia se a volte si ha bisogno di forme con un Id e a volte no , rendendo la parte Id dell'implementazione della classe base piuttosto discutibile:

  • Limita la riusabilità e la manutenibilità della classe Shape , perché viola il principio di responsabilità singola - ora il Shape ha due responsabilità diverse: fornire un'interfaccia generica draw e fornire identificabilità attraverso un ID.

  • Inoltre, setCustomId rende mutabile Shape , che impone anche alcune restrizioni d'uso.

  • E come svantaggio minore, ogni oggetto Shape allocherà un piccolo blocco di memoria per un riferimento di stringa aggiuntivo, richiesto o meno.

Quindi, se si desidera creare una classe di forma altamente riutilizzabile, consiglio vivamente di rimuovere completamente la parte Id da Shape e di introdurre un'altra classe come TaggedShape , che è composta da un ID e una forma :

 class TaggedShape
 {
     private Shape shape;
     private String customId;
     // ... add some public getters and maybe setters for shape and customId here ...
     public TaggedShape(Shape s, String id)
     {
          shape=s;
          customId=id;
     }

     public Shape getShape(){return shape;}

     public void draw(){shape.draw();}
 }

Ora, usa TaggedShape ogni volta che hai bisogno di forme con un ID nel tuo programma e uno Shape standard ogni volta che non hai bisogno di un ID. E ogni volta che hai bisogno di un TaggedShape come Shape , usa taggedShape.getShape() .

Come vedi, questa soluzione non ti obbliga ad implementare lo stesso codice ID per ogni nuova classe derivata di Shape . Utilizza composizione per la parte ID e ereditarietà per la parte draw . Quindi ogni nuova derivazione Shape può essere combinata con l'ID, o no, senza alcuna necessità di implementare nuovamente un'interfaccia ID. E la classe Shape ha solo "un motivo per cambiare" - quando cambiano le regole del disegno, tutto qui. Se è necessario modificare la parte ID, la gerarchia di classi Shape non viene più direttamente influenzata.

Come nota finale: in questo esempio, il metodo draw è puramente astratto, quindi Shape potrebbe anche essere un'interfaccia. Ma tutto ciò che ho scritto sopra rimane lo stesso, anche se ci sarebbe qualche coinvolgimento nell'implementazione, a patto che appartenga alla "responsabilità di disegno". Ad esempio, draw migh non è astratto, ma fornisce un'implementazione predefinita sovrascrivibile in Shape . Questo è ancora un caso d'uso valido per l'ereditarietà.

    
risposta data 13.06.2018 - 19:47
fonte
12

Scusa ma non vedo alcuna composizione qui. Vedo solo l'adorazione dell'interfaccia.

Per favorire la composizione sull'ereditarietà devi fare più che schiaffeggiare ciecamente su un'interfaccia. Devi comporre e delegare.

public interface Id{
    String getCustomId();
    void setCustomId(String aCustomId);
}

public class IdPlain implements Id { 
    public String getCustomId(){ return customId; }
    public void setCustomId(String aCustomId){ customId=aCustomId; }
    private String customId;
}

public class Circle {
    Circle(Id id){ this.id = id; } // Circle is composed of Id
    public String getCustomId(){ return id.getCustomId(); } // and deligates to Id
    public void setCustomId(String aCustomId){ id.setCustomId(aCustomId); }
    private Id id;

    public void draw(){ // yet Circle can still do it's own thing.
    }
} 

new Circle( new IdPlain() ).draw();

Sembra molto lavoro solo per evitare l'ereditarietà vero? Ma pensa a questo:

new Circle( new IdFancy() ).draw();

Se dici che non ti serve quella flessibilità al costo di tutta questa digitazione della tastiera, allora va bene. Mantieni con l'ereditarietà.

Potresti aver notato che non ho pubblicato un'interfaccia per Circle . Questo perché non sono sicuro di come Circle verrà utilizzato ancora. I client che utilizzano Circle sono quelli che possederanno le interfacce. Pubblicare un'interfaccia per Circle ora, senza sapere come verrà utilizzata, è design dell'interfaccia prematura tm .

Sto discutendo per le interfacce di ruolo qui. Il principio di fantasia per questo è Interface Segregation. Ciò significa che, no, non progetterò interfacce per Circle da implementare finché non vedrò come verrà effettivamente utilizzato Circle . Altrimenti ho un esplosione speculativa di interfacce tm : Id , Drawable , ICircle , ReadOnlyId , WriteOnlyId . Bleh, no. Lo progetterò quando so cosa è veramente necessario.

Potresti sostenere che devi assolutamente terminare di scrivere la classe Circle ora e non apportare modifiche a Circle più tardi quando puoi vedere come verrà utilizzato. Beh, questo fa schifo, ma non puoi ancora usarlo come argomento per progettare un'interfaccia prima di sapere come verrà utilizzata. Perché? Per via dell'eredità:

public CircleDrawer inherits Circle implements Drawable {}

(Non sto agitando la mano qui. Funziona così com'è. Una linea.)

Siamo spiacenti. Non c'è davvero alcuna scusa per le interfacce cerebrali.

    
risposta data 13.06.2018 - 05:22
fonte
1

Probabilmente, l'approccio migliore nel tuo caso è creare 2 interfacce separate:

public interface IdAware<T> {
    T getId();
    void setId(T id);
}

public interface Shape {
    void draw();
}

E un'interfaccia combinata:

public interface ShapeIdAware<T> extends Shape extends IdAware<T> {}

Forme particolari possono implementare interfacce Id, Shape, ShapeIdAware.

public class Circle implements ShapeIdAware<String> {
    private String id;
    public String getId(){
        return id;
    }
    public void setId(String id){
        this.id=id;
    }

    public void draw(){
    }
}

public class Rectangle implements Shape {

    public void draw(){
    }
}

L'utilizzo di tipi generici per Id offre la flessibilità di utilizzare String, Integer o qualsiasi altro tipo per l'Id.

È possibile fornire un'implementazione di base astratta per l'interfaccia IdAware:

public abstract class AbstractIdAware<T> implements IdAware<T> {
     private T id;
     public T getId() {
           return id;
     }
     public setId(T id) {
        this.id = id;
     }
}

Quindi la classe Circle avrà il seguente aspetto:

public class Circle extends AbstractIdAware<String> implements Shape {    
    public void draw() {
    }
}
    
risposta data 13.06.2018 - 23:53
fonte
0

La composizione è flessibile, l'ereditarietà dell'implementazione è breve. La scelta dipende dal contesto.

Se la tua classe base è interna e non cambierà mai, l'ereditarietà va bene.

Sfortunatamente, tale classe è spesso solo una piccola utility utilizzata in un ambito ristretto, quindi i vantaggi del riutilizzo del codice sono minuscoli.

    
risposta data 13.06.2018 - 04:20
fonte
0

Sono d'accordo con @candied_orange che la tua soluzione di "composizione" manca della composizione.

Nel tuo esempio, hai due responsabilità ortogonali. Hai la responsabilità del disegno, che varia a seconda del tipo di forma (cerchio, quadrato, ecc.). Hai una diversa responsabilità di essere identificabile. Questo non varia in base al tipo di forma, ma cambia per i suoi motivi. Idealmente miriamo a responsabilità separate il più possibile. Ciò migliora la manutenibilità perché un cambiamento nei requisiti di una responsabilità non influisce sulle altre responsabilità perché sono pezzi di codice separati. Ciò migliora anche la composabilità perché possiamo riunire molti piccoli pezzi in molti modi diversi per portare a termine il compito.

L'altra cosa di cui abbiamo bisogno è tenere traccia delle nostre dipendenze. Dovremmo mirare ad avere cose meno stabili dipendono da cose più stabili . Ciò migliora la mantenibilità proteggendoci da modifiche non correlate al codice.

Tenendo presente questo, possiamo ora guardare la gerarchia delle forme. Vediamo che il codice di disegno varia per forma (meno stabile), ma quella complessità è ben nascosta dietro una semplice interfaccia (piccola i): il metodo draw . Questa interfaccia è molto stabile sia per i fornitori di servizi (le implementazioni di forma) che per i chiamanti. Abbiamo anche gli attributi che si applicano a ogni forma (identità personalizzata in questo caso). Come hai notato, questo requisito può cambiare di volta in volta e quindi non è molto stabile. Sembra non più stabile delle implementazioni di forma e meno stabile dell'interfaccia draw .

Prima creiamo un'interfaccia di codice (grande I) per il concetto draw :

public interface Drawable {
    void draw();
}

Ora possiamo implementare cose che sono Drawable :

public class Circle implements Drawable {
    private Point center;
    private int radius;

    public Circle(Point center, int radius) {
        this.center = center;
        this.radius = radius;
    }

    @Override
    public void draw() { /* Draw stuff */ }
}

// etc with more drawable shapes

Noterai che non abbiamo aggiunto membri o metodi per l'identità personalizzata alle classi di forme. La responsabilità dell'identità non è più stabile della responsabilità di essere Drawable , ed è separata dall'essere Drawable . Invece, abbiamo bisogno di un posto separato per incapsulare la responsabilità dell'identità e fornire un posto per composibilità :

public class Displayable {
    private Drawable shape;
    private String customId;

    public Displayable(Drawable shape, String customId) {
        this.shape = shape;
        this.customId = customId;
    }

    public String getCustomId() {
        return this.customId;
    }

    public void draw() {
        this.shape.draw();
    }
}

Abbiamo creato una nuova classe per contenere la responsabilità dell'identità. È composto con un Drawable tramite il suo costruttore, e delega tutte le attività di disegno a quel composto Drawable . Disponiamo inoltre dei metodi per accedere all'identità personalizzata dell'istanza di forma identificabile.

Ora, se avessimo bisogno di più proprietà per l'identificabilità, potremmo aggiungerle alla classe Displayable e sarebbero disponibili in un unico posto ma componibili con tutte le diverse forme. Se abbiamo bisogno di più forme, possono essere facilmente composte in Displayable .

    
risposta data 16.06.2018 - 01:35
fonte
-1

Forse potresti usare la composizione che ha una classe astratta Shape che implementa getCustomId e setCustomId, e che ha un draw del metodo sovrascrivibile. Quindi avrai classi per forme personalizzate (Circle, Square, ecc.) Che erediteranno da Shape e dovranno implementare draw. Se non hai bisogno di conoscere il tipo specifico di forma, lo userai come forma. Ma, nel caso tu abbia bisogno di sapere quale forma specifica è l'istanza, devi accedere al suo tipo, potresti usare operatori come isinstanceof

    
risposta data 13.06.2018 - 04:44
fonte

Leggi altre domande sui tag