5
\$\begingroup\$

I want to learn how/when/why to use the GoF Design Patterns. These last days are dedicated to the Bridge Pattern, which means:

Decouple an abstraction from its implementation so that the two can vary independently.

Everyone has a different understanding of this vague description, but if I well-understood the global point, it cuts an abstraction into multiple independant implementations which relies on each other, in a chain.


On this premise, let's try to implement a four-arrow remote control. First, let's define what's awaited with interfaces:

public interface ITopBottomButtonsControl
{
    void TopButtonPressed();
    void BottomButtonPressed();
}

public interface ILeftRightButtonsControl
{
    void LeftButtonPressed();
    void RightButtonPressed();
}

public interface IRemoteControl : 
    ILeftRightButtonsControl, 
    ITopBottomButtonsControl
{ }

Here is an example of the BP implementation:

public class ChannelControl : ILeftRightButtonsControl
{
    public void LeftButtonPressed() => Console.WriteLine("Previous channel");
    public void RightButtonPressed() => Console.WriteLine("Next channel");
}

public class ChapterControl : ILeftRightButtonsControl
{
    public void LeftButtonPressed() => Console.WriteLine("Previous chapter");
    public void RightButtonPressed() => Console.WriteLine("Next chapter");
}

public abstract class BPRemoteControl : IRemoteControl
{
    ILeftRightButtonsControl LeftRightControl { get; }

    public BPRemoteControl(ILeftRightButtonsControl leftRight)
    {
        LeftRightControl = leftRight;
    }

    public void LeftButtonPressed() => LeftRightControl.LeftButtonPressed();
    public void RightButtonPressed() => LeftRightControl.RightButtonPressed();

    public abstract void TopButtonPressed();
    public abstract void BottomButtonPressed();
}

public class SpeedControl : BPRemoteControl
{
    public SpeedControl(ILeftRightButtonsControl leftRight) : base(leftRight) { }

    public override void BottomButtonPressed() => Console.WriteLine("Slow down");
    public override void TopButtonPressed() => Console.WriteLine("Speed up");
}

public class VolumeControl : BPRemoteControl
{
    public VolumeControl(ILeftRightButtonsControl leftRight) : base(leftRight) { }

    public override void BottomButtonPressed() => Console.WriteLine("Volume down");
    public override void TopButtonPressed() => Console.WriteLine("Volume up");
}

The fact is that, when I implemented this to test the pattern, it made me think back the Entity Component System. So, I tried to implement the same application, but this time with an ECS approach.
The classes ChannelControl and ChapterControl remains the same, but the rest is different:

public class SpeedControl : ITopBottomButtonsControl
{
    public void BottomButtonPressed() => Console.Write("Slow down");
    public void TopButtonPressed() => Console.WriteLine("Speed up");
}

public class VolumeControl : ITopBottomButtonsControl
{
    public void BottomButtonPressed() => Console.WriteLine("Volume down");
    public void TopButtonPressed() => Console.WriteLine("Volume up");
}

public class ECSRemoteController : IRemoteControl
{
    private ITopBottomButtonsControl TopBottomButtonsComponent { get; }
    private ILeftRightButtonsControl LeftRightButtonsComponent { get; }

    public ECSRemoteController(ITopBottomButtonsControl topBottom, ILeftRightButtonsControl leftRight)
    {
        TopBottomButtonsComponent = topBottom;
        LeftRightButtonsComponent = leftRight;
    }

    public void TopButtonPressed() => TopBottomButtonsComponent.TopButtonPressed();
    public void BottomButtonPressed() => TopBottomButtonsComponent.BottomButtonPressed();

    public void LeftButtonPressed() => LeftRightButtonsComponent.LeftButtonPressed();
    public void RightButtonPressed() => LeftRightButtonsComponent.RightButtonPressed();
}

Finally, these two solutions seems to be the same, with the following difference:

The Bridge Pattern is vertically-organized: S(a -> b -> c)
The Entity-Component-System is horizontally-organized: S(a, b, c)

Is there really a difference between these two solutions? When should one be prefered to the other?


HOW TO TEST: separate the two solutions into two folders BridgePattern and EntityComponentPattern, and fill the console application with the following code:

    static void Main(string[] args)
    {
        BridgePattern();
        Console.WriteLine();
        EntityComponentSystem();
        Console.ReadLine();
    }

    static void BridgePattern()
    {
        Console.ForegroundColor = ConsoleColor.Cyan;
        Console.WriteLine("BridgePattern version");
        Console.WriteLine("----------");

        IRemoteControl TVRemoteControl = new BridgePattern.VolumeControl(new BridgePattern.ChannelControl());
        IRemoteControl DVDRemoteControl = new BridgePattern.SpeedControl(new BridgePattern.ChapterControl());

        Console.WriteLine($"1. {nameof(TVRemoteControl)}:");
        UseRemoteControl(TVRemoteControl);

        Console.WriteLine($"2. {nameof(DVDRemoteControl)}:");
        UseRemoteControl(DVDRemoteControl);
    }

    static void EntityComponentSystem()
    {
        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.WriteLine("EntityComponentSystem version");
        Console.WriteLine("----------");

        IRemoteControl TVRemoteControl = new EntityComponentSystem.ECSRemoteController(new EntityComponentSystem.VolumeControl(), new EntityComponentSystem.ChannelControl());
        IRemoteControl DVDRemoteControl = new EntityComponentSystem.ECSRemoteController(new EntityComponentSystem.SpeedControl(), new EntityComponentSystem.ChapterControl());

        Console.WriteLine($"1. {nameof(TVRemoteControl)}:");
        UseRemoteControl(TVRemoteControl);

        Console.WriteLine($"2. {nameof(DVDRemoteControl)}:");
        UseRemoteControl(DVDRemoteControl);
    }

    static void UseRemoteControl(IRemoteControl remoteControl)
    {
        remoteControl.LeftButtonPressed();
        remoteControl.RightButtonPressed();
        remoteControl.TopButtonPressed();
        remoteControl.BottomButtonPressed();
    }
\$\endgroup\$
  • 5
    \$\begingroup\$ I find it's very cool that you've created this code with volume and remote controls rather than with meaningless foos and bars ;-) \$\endgroup\$ – t3chb0t Nov 8 '18 at 11:49
  • \$\begingroup\$ I for some reason cannot post an answer (CR throws errors and CAPTCHA on me), so, short answer is: those two sub-interfaces appear to be independent (you don't need the other for implementation), therefore I would use ECS. BP is suitable e.g. for IList extending ICollection (IList without Count from ICollection would be nonsense and you need access to internal data - array of elements and size). \$\endgroup\$ – user52292 Nov 9 '18 at 9:24
  • 1
    \$\begingroup\$ So, BP is when a step rely on an other one, and ECS is when interfaces are autonomous? PS: if you want, I can create an answer for you, you'll edit it and I'll accept \$\endgroup\$ – Maxime Recuerda Nov 9 '18 at 9:47
  • \$\begingroup\$ @MaximeRecuerda: Let us wait for somebody with better understanding of the theory. I am more practical than theoretical guy, so, your ECS example appears more appropriate to me, because the two interfaces are independent. I am not so sure how BP is actually specified and used, IList/ICollection was just a counter example where the other approach would be appropriate (because you simply cannot use ECS for it). \$\endgroup\$ – user52292 Nov 9 '18 at 12:23
3
\$\begingroup\$

Hybrid Pattern

The bridge pattern you have implemented is a combination of the composite pattern and inheritance. Why did you decide to let your ILeftRightButtonsControl implementation use composition and ITopBottomButtonsControl use inheritance?

public abstract class BPRemoteControl : IRemoteControl
{
    ILeftRightButtonsControl LeftRightControl { get; }

    public BPRemoteControl(ILeftRightButtonsControl leftRight)
    {
        LeftRightControl = leftRight;
    }

    public void LeftButtonPressed() => LeftRightControl.LeftButtonPressed();
    public void RightButtonPressed() => LeftRightControl.RightButtonPressed();

    public abstract void TopButtonPressed();
    public abstract void BottomButtonPressed();
}

Bridge Pattern

A useful implementation of the bridge pattern would be if ChannelControl, ChapterControl were third-party classes from let's say OtherDomain that do not adhere to your interfaces. If these other classes would have other interfaces, you might also consider the adapter pattern instead.

You would then create a bridge for OtherDomain.ChannelControl.

public class ChannelControlBridge : ILeftRightButtonsControl
{
    private OtherDomain.ChannelControl impl;
    public ChannelControlBridge (OtherDomain.ChannelControl impl) {
        this.impl = impl;
    }
    // the name of the operations happens to be the same in their domain
    // but this is not a requirement
    public void LeftButtonPressed() => impl.LeftButtonPressed();
    public void RightButtonPressed() => impl.RightButtonPressed();
}

And a bridge for OtherDomain.ChapterControl.

public class ChapterControlBridge : ILeftRightButtonsControl
{
    private OtherDomain.ChapterControl impl;
    public ChapterControlBridge (OtherDomain.ChapterControl impl) {
        this.impl = impl;
    }
    // the name of the operations happens to be the same in their domain
    // but this is not a requirement
    public void LeftButtonPressed() => impl.LeftButtonPressed();
    public void RightButtonPressed() => impl.RightButtonPressed();
}

Adapter Pattern

The adapter pattern could be implemented if ChannelControl, ChapterControl were third-party classes from let's say OtherDomain that do not adhere to your interfaces, but use their own interfaces instead.

You would then create and adapter for OtherDomain.IChannelControl.

public class ChannelControlAdapter : ILeftRightButtonsControl
{
    private OtherDomain.IChannelControl impl;
    public ChannelControlAdapter (OtherDomain.IChannelControl impl) {
        this.impl = impl;
    }
    // the name of the operations happens to be the same in their domain
    // but this is not a requirement
    public void LeftButtonPressed() => impl.LeftButtonPressed();
    public void RightButtonPressed() => impl.RightButtonPressed();
}

And an adapter for OtherDomain.IChapterControl.

public class ChapterControlAdapter : ILeftRightButtonsControl
{
    private OtherDomain.IChapterControl impl;
    public ChapterControlAdapter (OtherDomain.IChapterControl impl) {
        this.impl = impl;
    }
    // the name of the operations happens to be the same in their domain
    // but this is not a requirement
    public void LeftButtonPressed() => impl.LeftButtonPressed();
    public void RightButtonPressed() => impl.RightButtonPressed();
}

Composite Pattern

Your ECS is a perfect example of the composite pattern by forwarding the operations to your inner components.

public class ECSRemoteController : IRemoteControl
{
    private ITopBottomButtonsControl TopBottomButtonsComponent { get; }
    private ILeftRightButtonsControl LeftRightButtonsComponent { get; }

    public ECSRemoteController(ITopBottomButtonsControl topBottom, 
        ILeftRightButtonsControl leftRight)
    {
        TopBottomButtonsComponent = topBottom;
        LeftRightButtonsComponent = leftRight;
    }

    public void TopButtonPressed() => TopBottomButtonsComponent.TopButtonPressed();
    public void BottomButtonPressed() => TopBottomButtonsComponent.BottomButtonPressed();

    public void LeftButtonPressed() => LeftRightButtonsComponent.LeftButtonPressed();
    public void RightButtonPressed() => LeftRightButtonsComponent.RightButtonPressed();
}
\$\endgroup\$
  • 2
    \$\begingroup\$ This is the kind of answer that everyone would receive. Clear, examples, explanation, thank you so much. \$\endgroup\$ – Maxime Recuerda May 27 at 7:17

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.