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.