BridgeWzór.19 Programowanie obiektowe tworzy wiele problemów, jeśli nie będziemy się pilnować. Jeden z tych problemów nazywa się "state space explosion" . Ten problem polega  na tym, że liczba encji, czyli klas wręcz może eksplodować, gdy próbujemy opisać każdy możliwy stan danego przedmiotu

Załóżmy, że w swoim programie masz kwadraty i prostokąty, które jeszcze mają inne kolory i inne style ich renderowania.

W takim wypadku ile byś klas stworzył, aby to przestawić.

Istnieje parę wzorców projektowych, które starają się rozwiązać ten problem. Na przykład wzorzec "dekorator". Możesz też do tego problemu podejść jeszcze inaczej i określić, że dana właściwość jak "kolor" kwadratu i prostokąta to po prostu typ wyliczeniowy w tej klasie.

Z drugiej strony, jeśli kolor ma coś więcej niż informację o swoim stanie, czyli nasz kolor musi także zawierać informację o swoim zachowaniu wtedy typ wyliczeniowy nam nie wystarczy. 

Natomiast skończymy wtedy z grupą warunków if-else, które będą wykonywał akcję w zależności od wartości tego typu wyliczeniowego. Zapewne ten kod  także wyląduje w jakieś niezależnej klasy od figur.

Czy można to zrobić lepiej?

Nasz wzorzec projektowy "Bridge" czyli most zajmuje się rozwiązaniem tego problemu. Ja sama nazwa jego wskazuje próbuje on połączyć dwie rozdzielone relacje. 

Wracając do naszych prostokątów i kwadratów używając wzorca "Bridge" abyśmy mogli rozdzielić dodatkowe funkcje rysowania jak "powiększanie i zmniejszanie" od samego stylu rysowania, jak i koloru.

Zaraz ten diagram UML stanie się bardziej oczywisty.

Diagram UML Bridge

Chcemy mieć więc aplikację, która będzie nam rysowała różne wzory. Tę funkcję będzie wykonywał mój interfejs "IStampMaker". Od tej pory też figury do rysowania stają się znaczkami.

public interface IStampMaker
{
    void Render(int size);
}

Mój kreator znaczków chciałby, by rysować różne znaczki w różnym stylu i w różnym kolorze. Każda implementacja tego interfejsu będzie to określać.

Teraz nadchodzi nasz łącznik, czyli most. W nim będziemy mieć funkcje "powiększania" i "zmniejszania" naszego rysunku. W przyszłości, jeśli dojdą nam kolejne funkcje, które będą modyfikowały rysowanie to umieścimy je tutaj.

public abstract class GraphicsBridge
{
    protected IStampMaker stamper;
    // a bridge between the graphics that's being drawn and
    // the component stampMaker that actually draws it

    public GraphicsBridge(IStampMaker stamper)
    {
        this.stamper = stamper;
    }

    public abstract void Draw();
    public abstract void Magnifying(int factor);
    public abstract void Reducing(int factor);
}

Oto przykładowa implementacja tej klasy abstrakcyjnej.

public class Graphics : GraphicsBridge
{
    private int size;
    public Graphics(IStampMaker stamper, int size) : base(stamper)
    {
        this.size = size;
    }
    public override void Draw()
    {
        stamper.Render(size);
    }
    public override void Magnifying(int factor)
    {
        size *= factor;
    }

    public override void Reducing(int factor)
    {
        if (factor != 0)
            size /= factor;
    }
}

Teraz pokaże Ci implementacje poszczególnych znaczków. Ten będzie rysowała żółte prostokąty w konsoli.

public class ConsoleRectangleYellowStampMaker : IStampMaker
{
    public void Render(int size)
    {
        int s = Math.Abs(size) + 2;

        Console.ForegroundColor = ConsoleColor.Yellow;

        Console.WriteLine(new string('#', s));
        Console.Write('#');
        Console.Write(new string('+', s - 2));
        Console.Write("#\n");

        Console.WriteLine(new string('#', s));
        Console.ResetColor();
    }
}

Ta klasa będzie rysować niebieskie kwadraty.

public class ConsoleSquareBlueStampMaker : IStampMaker
{
    public void Render(int size)
    {
        int s = Math.Abs(size) + 2;
        int p = size * 2;

        Console.ForegroundColor = ConsoleColor.Blue;

        Console.WriteLine(new string('#', p));

        for (int i = 0; i < (s - 2); i++)
        {
            Console.Write('#');
            Console.Write(new string('+', p - 2));
            Console.Write("#\n");
        }

        Console.WriteLine(new string('#', p));
        Console.ResetColor();
    }
}

Teraz zatrzymajmy się na chwilę. Co ten wzorzec projektowy "Bridge" chce osiągnąć? Przed czym się tutaj bronimy?

Lubię się czasem zatrzymywać przed implementacją taki wzorów, ponieważ to, że należą do grupy wzorców "Gang of Four" automatycznie nie oznacza, że są one dobre. Przykładowo we wpisie o wzorcu "State" powiedziałem Ci, dlaczego jest on fatalny w 90% przypadku lepiej napisać kod if-else. Warto więc kwestionować użycie każdego wzorca projektowego.

Dla mnie sens użycia wzorca "Bridge" nie był taki oczywisty. To był nawet jeden z powodów, dla których ten właśnie wzorzec czekał tak długo na swój wpis. Na szczęście w końcu do mnie dotarło co ten wzorzec chce osiągnąć.

Jak widzisz nasz wzorzec "Bridge" próbuje przejąć kontrolę na dwoma sytuacjami, które mogą negatywnie wpłynąć na nasze oprogramowanie. 

  • Co się stanie, jeśli będziemy musieli dodać kolejną figurę/znaczek do rysowania w naszym systemie
  • Co się stanie, jeśli dojdą nam nowe funkcje jak "powiększanie" i "zmniejszanie" do tych znaczków i figur?

Czego nie chcemy? Nie chcemy, aby każda z tych zmian nie rozpowszechniła się na wszystkie klasy.  Jakby to wyglądało, gdybyś nie użyli wzorca "Bridge".

  • Jeśli nowy znaczek zostanie dodany to wtedy ten nowy znaczek musi  implementować interfejs "IStampMaker" . Musimy więc do niego dodać implementację rysowania i tych dodatkowych funkcji jak "powiększanie" i "pomniejszanie". 
  • Jeśli dojdzie nam nowa funkcja jak "powiększanie" i "pomniejszanie" to wtedy do każdej istniejącego znaczka będziemy musieli zrobić implementacje tej funkcji.

Możesz też zadać sobie pytanie, czy kod w tym znaczkach w niektórych miejscach nie będzie się powtarzać. 

Mając wzorzec "Bridge" możemy wydzielić funkcję, które wiemy, że dla wielu znaczków będą zachowywać się tak samo. Dodatkowo mając ten wzorzec będziemy mieć X klas znaczków i Y metod opisujących dodatkowe funkcje w naszym moście.

Liczba pisanego kodu równa się wtedy X + Y.

Gdybyś nie skorzystali ze wzorca "Bridge" to wtedy moglibyśmy doświadczyć problem wielokrotności zmian, która byłaby równa X * Y. 

public class ConsoleRectangleYellowStampMaker : IStampMaker
{
    public void Render(int size);

    public void Magnifying(int factor);

    public void Reducing(int factor);
}

public class ConsoleSquareBlueStampMaker : IStampMaker
{
    public void Render(int size);

    public void Magnifying(int factor);

    public void Reducing(int factor);
}

...

Oto użycie naszego wzorca Bridge w praktyce. Ten kod narysuje niebieski kwadrat w konsoli.

var rectangleStamp = new ConsoleRectangleYellowStampMaker();
var squareStamp = new ConsoleSquareBlueStampMaker();

var squareG = new Graphics(squareStamp, 6);
squareG.Draw();
Console.WriteLine("\n");
squareG.Magnifying(4);
squareG.Draw();
Console.WriteLine("\n");
squareG.Reducing(2);
squareG.Draw();
Console.WriteLine("\n");

Ten kod narysuje żółty prostokąt.

var rectangleStamp = new ConsoleRectangleYellowStampMaker();
var squareStamp = new ConsoleSquareBlueStampMaker();

var rectangleG = new Graphics(rectangleStamp, 4);
rectangleG.Draw();
Console.WriteLine("\n");
rectangleG.Magnifying(4);
rectangleG.Draw();
Console.WriteLine("\n");
rectangleG.Reducing(2);
rectangleG.Draw();
Console.WriteLine("\n");

Oto efekty tego kodu.

Niebieski kwadrat rysowany dzięki wzorcu Bridge

Żółte prostokąty rysowany dzięki wzorcu Bridge

Oczywiście nic nie stoi na przeszkodzie, abyś skorzystał z tego wzorca w ASP.NET Core korzystając z kontenera wstrzykiwania zależności.

Oto przykład użycia.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IStampMaker, ConsoleSquareBlueStampMaker>();
builder.Services.AddSingleton<GraphicsBridge, Graphics>
    (
    (provider) => 
        { 
            var stamp = provider.GetService<IStampMaker>();
            return new Graphics(stamp, 10);
        }
    );

var app = builder.Build();

Korzystając z minimalnego stylu w ASP.NET Core użycie naszych klas wymaga małej ilości kodu.

app.MapGet("/", (GraphicsBridge graphics) => graphics.Draw());

app.MapGet("/m/{number:int}", 
    (GraphicsBridge graphics, int number) => graphics.Magnifying(number));

app.MapGet("/r/{number:int}", 
    (GraphicsBridge graphics, int number) => graphics.Reducing(number));
app.Run();

Podsumowanie

Główny cel wzorca Bridge jest uniknięcie powtarzalności kodu oraz uniknięcia pisania wszystkich metod do wszystkich możliwych klas. 

Oczywiście moglibyśmy podejść do problemu inaczej. Zamiast tworzyć klasy jak "ConsoleRectangleYellowStampMaker" czy "ConsoleSquareBlueStampMaker" moglibyśmy skorzystać z typu wyliczeniowego, którego stan byłby sprawdzany w trakcie rysownia.

Nie zawsze jednak jest możliwe, ponieważ być może potrzebuje więcej informacji i stan wyliczeniowy Ci już do tego Ci nie wystarczy.

Pozostaje Ci wtedy tylko wydzielić metody i zastanowić gdzie one powinny się łączyć.