1

All of the examples of bridge pattern show only one level of inheritance, but I would want to apply it to a subclass of the base class too. The RefinedClass adds functionality to the BaseClass. In the end you want 4 classes functionality (Feature1Foo,Feature2Foo) , (Feature1Foo,Feature2Bar) , (Feature1Bar,Feature2Foo), (Feature1Bar,Feature2Bar)

enter image description here

RefinedClassFoo could inherit from BaseClassFoo, and then have RefindedClassFooFoo inherint from this one and implement Feature2Foo and RefindedClassFooBar inherit from this one and implement Feature2Bar. And continue like this. The number of classes would grow exponentially with the number of features and levels of inheritance. Plus their would be a lot of code duplication since RefinedClassFoo and RefinedClassBar both add the same functionality.

enter image description here

The best solution I found was to add BaseClass functionality as a dependency injected field in RefinedClass, while still inheriting from BaseClass. All public and protected methods inherited from BaseClass are forwarded to the dependency. This means that RefinedClass will contain a lot of code which just forwards to the field baseClass and the dependency will need to get injected at runtime.

enter image description here

Is there a more sane solution to this?

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

To make the example more concrete. BaseClass is a Point, whith the interfaces for Feature1 defining how to draw them, using a circle (bar) or a cross (foo). On top of this I add a RefinedClass which is ColorPoint, the Feature2 defining what color to use, red (bar) or black (foo). I want to be able to use any combination of this with ease and allow for, if the near future a request for adding plus,romb shapes or pink, purple colors comes to be able to extend the architecture with ease. At the same time if I do not want to display them and simply compute the space they occupy on screen I can use just the Point class. How would you achieve such a goal?

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

You come to work on Monday. You have this class diagram

enter image description here

It is decided that for client xy computeFeature1() needs to be changed to algoFeature1_Y, but you still would want to keep the old algorithm algoFeature1_X since you need it for client xx.

At the same time the same goes for computeFeature2(), where client yx needs computeFeature2 to be changed, but client xx does not.

You now have to support this

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

How do you do that?

Easy solution would be to use dependecy injection, but I do not want the user to have the overhead of that. client xx should simply instantiate his class without knowledge of the other implementations (not to hide, but for convenience).

  • 1
    It's hard to offer feedback on the solution when the problem is not defined. What exactly is this object model supposed to accomplish? – John Wu Mar 3 '17 at 10:39
  • @JohnWu I want the first picture in essence, where if you look at the superclass and subclass you have a bridge pattern for both, but if you actually introduce the inheritance in the picture, everything breaks – Radu Ionescu Mar 3 '17 at 10:44
  • Not what I mean. An object model is typically a solution for a problem. What is the problem? Or is this just a thought experiment? – John Wu Mar 3 '17 at 10:55
  • @JohnWu I want in the subclass RefinedClass to vary behaviors of the method call to feature1, independent of the choices I make for feature2 – Radu Ionescu Mar 3 '17 at 11:14
  • Why all the interfaces? They make the diagram hard to read, and probably the code as well. The interfaces have nothing to do with the design question. – Frank Hileman Mar 3 '17 at 14:17
2

OK let's go through this step by step, and end with the bridge pattern you seek.

In the old days

Back in the day, we would just tell the client to instantiate the right object and use it, like this:

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);  
    }
}

With DI and IoC

In the past five years or so it has been very trendy to use interfaces instead of concrete objects, and then provide an IoC container which is a object factory. Like this:

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 this example, there is no inheritance relationship between the interfaces or the concrete classes. You can add one; it doesn't affect the overall model. If you add an inheritance relationship between the interfaces you don't have to add one to the concrete classes, nor vice versa. You could also have both interfaces inherit from a common interface, or have both classes inherit from the same base class. Again, the two have nothing to do with each other. Interface inheritance and class inheritance serve very different purposes; the former is to re-use or associate related feature APIs while the latter is so you share implementation.

With the above code, you could implement client-specific behavior by telling each client to configure the container differently, by compiling different versions of container, or by adding code to container which checks the client's license and instantiates the most advanced feature that they have paid for. This is all you need to accomplish this, you don't need a bridge.

Add simple bridge

Now let's say your two features have two verisons each, say a native renderer and an emulated/software renderer. So your architect tells you to use the bridge pattern. That would look a bit like this:

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);  
    }
}

Notice the code written by the client has not changed at all.

The concrete feature classes now contain a reference to another, internal class which does the work. Notice the internal class has color capability, but it is not exposed to the client.

Using this scheme, different clients trying to use the same feature could end up with a different implementation, depending how the internal class factory is set up. Thus some clients could get the software renderer and some could get the native renderer, and they wouldn't be able to tell the difference because they are calling it with the same interface and class.

Notice also there is only one internal Renderer which does both text and points. We could have two classes if you want; it doesn't matter. I include only one in the example to save typing.

The above, by the way, is a bridge.

Bridge with additional (refined) features

Now let's say we want to expose that color capability to the client, but also keep the old interface for backward compatibility. It would look like this:

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);  

    }
}

Notice that there is still only one concrete class per feature. We don't necessarily need to expose both an old class (that only supports the old feature set) and a new class (which includes the new feature). The new class will do both, and will still be compatible with the old interface. I can't think of any good reason to keep two versions of the class, unless perhaps you wish to restrict some clients from instantiating one or the other. So let's do that next, which will answer your question:

Bridge with inheritance in concrete class

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);  

    }
}

Notice the client code isn't any different (to him, he is just using an interface), but different clients will end up getting different feature classes, and could possibly end up with different internal (IRenderer) classes as well. This gives the 2x2 variation you are looking for.

Do we have geometric explosion of classes and interfaces? No. You add an interface when you need to add an interface; you add a class when you need to add a class. Adding a new class does not require a new interface, nor vice versa. It depends on the actual problem you are trying to solve.

  • This is a very good answer. It took me a long time to read and understand it and I can not even imagine the time it took to write it. Thank you for taking the time to answer my question and for the detailed explanations. I now understand what you meant by it being justified . Unfortunately for me, I will have to follow your design since it is what I need to achieve, but luckily it is needed only in just few places :) – Radu Ionescu Mar 4 '17 at 10:53
1
  1. This design is, um, ambitious. Is it justified? If there is a rationale behind all this it is not clear from your post.

  2. A class lineage can have exactly one base class. You have three. This is like having a tree with three trunks. If a class inherits from another class, it is not really the base class.

  3. If this is really a bridge, there should not be any dependency relationships from the left side of your diagram to the right side. Classes on the right should contain the interface items on the left. If there should be any dependency arrows to begin with, they should point right to left.

  4. There should not be any dependency arrows to begin with. The whole point of the pattern is decoupling. The only arrow should be the containment arrow from the right side to the interface on the left.

  5. You do not need to have an interface on the right for every implementation of a feature. You just need to have one interface per feature. The whole point is to vary interfaces independent of implementation; they should not need to exist in lockstep.

  6. I am not sure what the problem domain here is but it seems unlikely that features should inherit from each other.

  7. If the problem you are trying to solve is "I need to deliver a set of functionality to customers where the interface is fixed but the implementation is custom per customers," you don't necessarily need a bridge. You should probably just use an adapter. The bridge is for the situation where both implementation and interface change frequently. If this is happening in your situation, we should probably talk about it (in a separate question). The idea behind a client-facing interface is that it is relatively stable.

  • Thank you for the response. I agree with you that the design is ambitious and maybe not clear. I am not an expert at UML diagrams and thought that I was depicting class inheritance always on the left and interface implementations on the right. Maybe it was not obvious. I put the dependency arrows just to say that in fact this subclass is using this particular implementation. Regardless of the actual application or what problem I am trying to solve, I am still curious about what I posted. How to apply the bridge pattern both to the subclass and the superclass. – Radu Ionescu Mar 3 '17 at 20:00
  • All resources offer this simple example. I would have to disagree with the adapter pattern, since it is not easy to switch to a new implementation. You are basically fixing the implementation for a client by a particular Adaptor class. The simple solution as I said is to use dependecy injection. You can achieve this by relying on it being autowired, but you lose the control in your code. Another solution is to use an Abstract Factory which will wire the dependecies in concrete factories. You can deviate from the pattern by not constructing concrete classes for each choice of implementation – Radu Ionescu Mar 3 '17 at 20:09
  • I will vote your post, but I can not accept it as answer – Radu Ionescu Mar 3 '17 at 20:10
  • @RaduIonescu dependency injection in a vector graphics system would only make sense to me if you are talking about an editor to be extended by third parties. Any other use really is over-engineering. – Frank Hileman Mar 3 '17 at 21:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.