Pattern del ponte con ereditarietà (applicazione del pattern bridge alla sottoclasse)

1

Tutti gli esempi di pattern bridge mostrano solo un livello di ereditarietà, ma vorrei applicarlo anche a una sottoclasse della classe base. RefinedClass aggiunge funzionalità a BaseClass . Alla fine vuoi funzionalità di 4 classi (Feature1Foo,Feature2Foo) , (Feature1Foo,Feature2Bar) , (Feature1Bar,Feature2Foo) , (Feature1Bar,Feature2Bar)

RefinedClassFoopotrebbeereditaredaBaseClassFoo,quindiavereRefindedClassFooFooinherintdaquestoeimplementareFeature2FooeRefindedClassFooBarereditaredaquestoedimplementareFeature2Bar.Econtinuacosì.Ilnumerodiclassicrescerebbeinmodoesponenzialeconilnumerodifunzionielivellidiereditarietà.InoltrelalorosarebbeunsaccodiduplicazionedelcodicepoichéRefinedClassFooeRefinedClassBarentrambiaggiungonolastessafunzionalità.

Lasoluzionemigliorechehotrovatoèstatal'aggiuntadellafunzionalitàBaseClasscomecampodidipendenzainjectedinRefinedClass,mentrecontinuavoadereditareda %codice%.TuttiimetodipubblicieprotettiereditatidaBaseClassvengonoinoltratialladipendenza.CiòsignificacheBaseClassconterràunsaccodicodicecheinoltrasoloalcampoRefinedClassecheladipendenzadovràessereiniettatainfasediruntime.

C'èunasoluzionepiùsanaperquesto?

-----EDIT------

Perrenderel'esempiopiùconcreto.baseClassèunBaseClass,conleinterfacceperPointchedefinisconocomedisegnarle,usandouncerchio(barra)ounacroce(pippo).IncimaaquestoaggiungounFeature1cheèRefinedClass,ilColorPointchedefiniscequalecoloreusare,rosso(bar)onero(foo).Voglioessereingradodiutilizzarequalsiasicombinazionediquestoconfacilitàeconsentire,senelprossimofuturounarichiestaperaggiungereplus,formediromboocolorirosa,violaèingradodiestenderel'architetturaconfacilità.Allostessotempo,senonvogliovisualizzarliesemplicementecalcolarelospaziocheoccupanosulloschermo,possoutilizzaresololaclasseFeature2.Comeraggiungerebbeunsimileobiettivo?

----------EDIT---------------

Venitealavorarelunedì.Haiquestodiagrammadiclasse

Si decide che per client xy computeFeature1 () deve essere modificato in algoFeature1_Y, ma si vorrebbe comunque mantenere il vecchio algoritmo algoFeature1_X poiché è necessario per il client xx.

Allo stesso tempo, lo stesso vale per computeFeature2 (), dove client yx ha bisogno di computeFeature2 per essere modificato, ma client xx no.

Ora devi supportare questo

  • xx - algoFeature1_X algoFeature2_X
  • xy - algoFeature1_Y algoFeature2_X
  • yx - algoFeature1_X algoFeature2_Y
  • yy - algoFeature1_Y algoFeature2_Y

Come si fa?

La soluzione facile sarebbe utilizzare l'iniezione di dipendenza, ma non voglio che l'utente abbia il sovraccarico di quello. client xx dovrebbe semplicemente creare un'istanza della sua classe senza conoscenza delle altre implementazioni (non per nascondere, ma per comodità).

    
posta Radu Ionescu 03.03.2017 - 11:18
fonte

2 risposte

2

OK procediamo passo dopo passo e finiamo con il modello di ponte che cerchi.

Ai vecchi tempi

Nel passato, dovremmo semplicemente dire al client di creare un'istanza dell'oggetto giusto e usarlo, in questo modo:

public class TextFeature
{
    void WriteText(string s) 
    { 
        //Code that writes the text
    }
}

public class PointFeature
{
    void PlotPoint(int x, int y)
    {
        //Code that plots the point
    }
}

class CodeWrittenByClient
{
    static void SampleCall()
    {
        TextFeature f1 = new TextFeature();
        f1.WriteText("Hello world");
        PointFeature f2 = new PointFeature();
        f1.PlotPoint(10,10);  
    }
}

Con DI e IoC

Negli ultimi cinque anni circa è stato molto trendy utilizzare interfacce anziché oggetti concreti e quindi fornire un contenitore IoC che è una fabbrica di oggetti. In questo modo:

public interface ITextFeature
{
    void WriteText(string s);
}

public class TextFeature :ITextFeature
{
    void WriteText(string s) 
    { 
        //Code that writes the text
    }
}

public interface IPointFeature
{
    void PlotPoint(int x, int y);
}

public class PointFeature : IPointFeature
{
    void PlotPoint(int x, int y)
    {
        //Code that plots the point
    }
}

class CodeWrittenByClient
{
    static void SampleCall()
    {
        ITextFeature f1 = container.Resolve<ITextFeature>(); //will return a new TextFeature, but client doesn't need to know this
        f1.WriteText("Hello world");
        IPointFeature f2 = container.Resolve<IPointFeature>(); //Will return a new PointFeature
        f1.PlotPoint(10,10);  
    }
}

In questo esempio, non esiste una relazione di ereditarietà tra le interfacce o le classi concrete. Puoi aggiungerne uno; non influisce sul modello generale. Se si aggiunge una relazione di ereditarietà tra le interfacce, non è necessario aggiungerne una alle classi concrete, né viceversa. Si potrebbe anche avere entrambe le interfacce ereditate da un'interfaccia comune oppure entrambe le classi ereditano dalla stessa classe base. Di nuovo, i due non hanno niente a che fare l'uno con l'altro. L'ereditarietà delle interfacce e l'ereditarietà delle classi hanno scopi molto diversi; il primo è di riutilizzare o associare le API delle funzioni correlate mentre il secondo è così che condividi l'implementazione.

Con il codice precedente, potresti implementare il comportamento specifico del client dicendo ad ogni client di configurare container in modo diverso, compilando diverse versioni di container o aggiungendo codice a container che controlla la licenza del client e istanzia la funzionalità più avanzata per cui hanno pagato. Questo è tutto ciò che ti serve per fare ciò, non hai bisogno di un bridge.

Aggiungi bridge semplice

Ora diciamo che le tue due funzioni hanno due verison ciascuna, ad esempio un renderer nativo e un renderer emulato / software. Quindi il tuo architetto ti dice di usare il modello del ponte. Sembrerebbe un po 'come questo:

internal interface IRenderer //Notice this is internal; client doesn't need it
{
    PlotPoint(int x, int y, Color color);
    WriteText(string s, Color color)
}

internal class SoftwareRenderer : IRenderer
{
    PlotPoint(int x, int y, Color color) { //Plot point using software rendering };
    WriteText(string s, Color color)     { //Write text using software rendering };
}

internal class NativeRenderer : IRenderer
{
    PlotPoint(int x, int y, Color color) { //Plot point using native rendering };
    WriteText(string s, Color color)     { //Write text using native  rendering };
}


public interface ITextFeature
{
    void WriteText(string s);
}

public class TextFeature :ITextFeature
{
    internal TextFeature(IRenderer renderer) {}; //DI will set to either software or native renderer
    private readonly IRenderer _renderer;

    void WriteText(string s) 
    { 
        _renderer.WriteText(s, Color.Black);
    }
}

interface IPointFeature
{
    void PlotPoint(int x, int y);
}

class PointFeature : IPointFeature
{
    internal PointFeature(IRenderer renderer) {}; //DI will set to either software or native renderer
    private readonly IRenderer _renderer;

    void PlotPoint(int x, int y)
    {
        _renderer.PlotPoint(x, y, Color.Black); 
    }
}

class CodeWrittenByClient
{
    static void SampleCall()
    {
        ITextFeature f1 = container.Resolve<ITextFeature>(); //will return a new TextFeature, but client doesn't need to know this
        f1.WriteText("Hello world");
        IPointFeature f2 = container.Resolve<IPointFeature>(); //Will return a new PointFeature
        f1.PlotPoint(10,10);  
    }
}

Si noti che il codice scritto dal client non è stato modificato affatto.

Le classi di feature concrete contengono ora un riferimento a un'altra classe interna che esegue il lavoro. Nota che la classe interna ha capacità di colore, ma non è esposta al client.

Usando questo schema, diversi client che tentano di utilizzare la stessa funzionalità potrebbero finire con un'implementazione diversa, a seconda di come viene impostato il factory di classe interno. Così alcuni client potrebbero ottenere il renderer del software e alcuni potrebbero ottenere il renderer nativo, e non sarebbero in grado di distinguere perché lo chiamano con la stessa interfaccia e classe.

Si noti inoltre che esiste solo un Renderer interno che esegue sia il testo che i punti. Potremmo avere due classi se vuoi; non importa. Ne includo solo uno nell'esempio per salvare la digitazione.

Quanto sopra, a proposito, è un bridge.

Bridge con funzioni aggiuntive (perfezionate)

Ora diciamo che vogliamo esporre quella capacità di colore al client, ma anche mantenere la vecchia interfaccia per compatibilità con le versioni precedenti. Sarebbe come questo:

internal interface IRenderer //Notice this is internal; client doesn't need it
{
    PlotPoint(int x, int y, Color color);
    WriteText(string s, Color color)
}

internal class SoftwareRenderer : IRenderer
{
    PlotPoint(int x, int y, Color color) { //Plot point using software rendering };
    WriteText(string s, Color color)     { //Write text using software rendering };
}

internal class NativeRenderer : IRenderer
{
    PlotPoint(int x, int y, Color color) { //Plot point using native rendering };
    WriteText(string s, Color color)     { //Write text using native  rendering };
}


public interface ITextFeature
{
    void WriteText(string s);
}

public interface ITextFeature2 : ITextFeature
{
    void WriteText(string s);           //We have to include the old prototype because we inherited the interface
    void WriteText(string s, Color c);
}


public class TextFeature :ITextFeature2
{
    internal TextFeature(IRenderer renderer) {}; //DI will set to either software or native renderer
    private readonly IRenderer _renderer;

    void WriteText(string s) 
    { 
        _renderer.WriteText(s, Color.Black);
    }
    void WriteText(string s, Color c) 
    { 
        _renderer.WriteText(s, c);
    }
}

interface IPointFeature
{
    void PlotPoint(int x, int y);
}

interface IPointFeature2 : IPointFeature
{
    void PlotPoint(int x, int y);
    void PlotPoint(int x, int y, Color c);
}

class PointFeature : IPointFeature2
{
    internal PointFeature(IRenderer renderer) {}; //DI will set to either software or native renderer
    private readonly IRenderer _renderer;

    void PlotPoint(int x, int y)
    {
        _renderer.PlotPoint(x, y, Color.Black); 
    }
    void PlotPoint(int x, int y, Color c)
    {
        _renderer.PlotPoint(x, y, c); 
    }
}

class CodeWrittenByClient
{
    static void SampleCall()
    {
        ITextFeature2 f1 = container.Resolve<ITextFeature2>(); //will return a new TextFeature, but client doesn't need to know this
        f1.WriteText("Hello world", Color.Red);
        IPointFeature2 f2 = container.Resolve<IPointFeature2>(); //Will return a new PointFeature
        f1.PlotPoint(10,10, Color.Blue);  

    }
}

Si noti che esiste ancora una sola classe concreta per caratteristica. Non abbiamo necessariamente bisogno di esporre sia una vecchia classe (che supporta solo il vecchio set di funzionalità) sia una nuova classe (che include la nuova funzionalità). La nuova classe farà entrambe le cose, e sarà comunque compatibile con la vecchia interfaccia. Non riesco a pensare a nessuna buona ragione per mantenere due versioni della classe, a meno che forse non vogliate limitare alcuni client dall'istanziazione di una o dell'altra. Quindi, facciamolo dopo, che risponderà alla tua domanda:

Bridge con ereditarietà nella classe concreta

internal interface IRenderer //Notice this is internal; client doesn't need it
{
    PlotPoint(int x, int y, Color color);
    WriteText(string s, Color color)
}

internal class SoftwareRenderer : IRenderer
{
    PlotPoint(int x, int y, Color color) { //Plot point using software rendering };
    WriteText(string s, Color color)     { //Write text using software rendering };
}

internal class NativeRenderer : IRenderer
{
    PlotPoint(int x, int y, Color color) { //Plot point using native rendering };
    WriteText(string s, Color color)     { //Write text using native  rendering };
}


public interface ITextFeature
{
    void WriteText(string s);
}

public interface ITextFeature2 : ITextFeature
{
    void WriteText(string s);           //We have to implement the old prototype because we inherited the interface
    void WriteText(string s, Color c);
}


public class TextFeature :ITextFeature
{
    internal TextFeature(IRenderer renderer) {}; //DI will set to either software or native renderer
    private readonly IRenderer _renderer;

    void WriteText(string s) 
    { 
        _renderer.WriteText(s, Color.Black);
    }
}
public class TextFeature2 :TextFeature, ITextFeature2
{
    void WriteText(string s, Color c) 
    { 
        _renderer.WriteText(s, c);
    }
}

interface IPointFeature
{
    void PlotPoint(int x, int y);
}

interface IPointFeature2 : IPointFeature
{
    void PlotPoint(int x, int y, Color c);
}

class PointFeature : IPointFeature
{
    internal PointFeature(IRenderer renderer) {}; //DI will set to either software or native renderr
    private readonly IRenderer _renderer;

    void PlotPoint(int x, int y)
    {
        _renderer.PlotPoint(x, y, Color.Black); 
    }
}

class PointFeature2 : PointFeature, IPointFeature2
{
    void PlotPoint(int x, int y, Color c)
    {
        _renderer.PlotPoint(x, y, c); 
    }
}

class CodeWrittenByClient
{
    static void SampleCall()
    {
        ITextFeature2 f1 = container.Resolve<ITextFeature2>(); //will return a new TextFeature, but client doesn't need to know this
        f1.WriteText("Hello world", Color.Red);
        IPointFeature2 f2 = container.Resolve<IPointFeature2>(); //Will return a new PointFeature
        f1.PlotPoint(10,10, Color.Blue);  

    }
}

Notate che il codice client non è diverso (per lui, sta solo usando un'interfaccia), ma diversi client finiranno per ottenere diverse feature class e potrebbero eventualmente finire con classi interne differenti ( IRenderer ) come bene. Questo dà la variazione 2x2 che stai cercando.

Abbiamo un'esplosione geometrica di classi e interfacce? No. Aggiungi un'interfaccia quando devi aggiungere un'interfaccia; aggiungi una classe quando devi aggiungere una classe. L'aggiunta di una nuova classe non richiede una nuova interfaccia, né viceversa. Dipende dal problema reale che stai cercando di risolvere.

    
risposta data 04.03.2017 - 10:29
fonte
1
  1. Questo design è, um, ambizioso. È giustificato? Se c'è una logica dietro tutto questo non è chiaro dal tuo post.

  2. Un lignaggio di classe può avere esattamente una classe base. Ne hai tre. È come avere un albero con tre tronchi. Se una classe eredita da un'altra classe, non è realmente la classe base.

  3. Se questo è davvero un bridge, non dovrebbero esserci relazioni di dipendenza dal lato sinistro del diagramma sul lato destro. Le classi a destra dovrebbero contenere gli elementi interfaccia a sinistra. Se all'inizio dovessero esserci frecce di dipendenza, dovrebbero puntare da destra a sinistra.

  4. Non ci dovrebbero essere frecce di dipendenza per cominciare. L'intero punto del modello è il disaccoppiamento. L'unica freccia dovrebbe essere la freccia di contenimento dal lato destro all'interfaccia a sinistra.

  5. Non è necessario disporre di un'interfaccia sulla destra per ogni implementazione di una funzione. Hai solo bisogno di avere un'interfaccia per caratteristica. L'intero punto è di variare le interfacce indipendentemente dall'implementazione; non dovrebbero avere bisogno di esistere in lockstep.

  6. Non sono sicuro di quale sia il dominio del problema qui, ma sembra improbabile che le funzionalità debbano eredarsi l'una dall'altra.

  7. Se il problema che stai cercando di risolvere è "Devo fornire un set di funzionalità ai clienti in cui l'interfaccia è fissa ma l'implementazione è personalizzata per i clienti", non è necessario necessariamente un bridge. Probabilmente dovresti semplicemente utilizzare un adattatore . Il bridge è per la situazione in cui sia l'implementazione che l'interfaccia cambiano frequentemente. Se questo sta accadendo nella tua situazione, dovremmo probabilmente parlarne (in una domanda separata). L'idea dietro un'interfaccia client è che è relativamente stabile.

risposta data 03.03.2017 - 18:54
fonte

Leggi altre domande sui tag