Most (Bridge)

Most

Most (Bridge) jest strukturalnym wzorcem projektowym pozwalającym na rozdzielenie dużej klasy lub zestawu spokrewnionych klas na dwie hierarchie: abstrakcję oraz implementację. Nad obiema można wówczas pracować niezależnie.

Cel

Most oddziela abstrakcję od jej implementacji, dzięki czemu obie mogą się różnić niezależnie.

Problem

Ten wzorzec ma na celu oddzielenie abstrakcji klasy od implementacji. W rezultacie zapewnia to możliwość zastąpienia implementacji inną implementacją bez modyfikowania abstrakcji. Abstrakcja w tym kontekście może być postrzegana jako sposób na uproszczenie czegoś złożonego.

Weźmy na przykład aplikację konsolową. Console.WriteLine jest abstrakcją tego, co jest zawarte w metodzie WriteLine. Na przykład każda akcja, funkcja są abstrakcjami. Nieustannie ich używamy i tworzymy. Abstrakcje zatem radzą sobie ze złożonością, ukrywając części, o których nie musimy wiedzieć. I właśnie tę abstrakcję staramy się oddzielić od jej implementacji. Kiedy słyszymy o oddzieleniu abstrakcji od implementacji, od razu myślimy o interfejsach i klasach abstrakcyjnych, ale jest to trochę bardziej skomplikowane.

Załóżmy, że tworzymy oprogramowanie do zarządzania cennikami (PriceList). I mamy dwa cenniki: cennik komputerów (ComputersPriceList) i cennik telewizorów (TVPriceList). Obie implementacje cenników zawierają metodę ObliczCennę (CalculatePrice). W zależności od cennika dostępne są inne pola, ale te w tej chwili nas to nie interesuje. Tak więc klasa, która musi zsumować całkowitą należną kwotę, może po prostu wywołać metodę ObliczCennę w każdym cenniku.

Moglibyśmy to zaimplementować za pomocą abstrakcyjnej klasy Cennik (PriceList) lub interfejsu IPriceList. I teraz wyobraź sobie, że wprowadzamy coś nowego, zniżkę (Discount), która oferuje rabat w wysokości 10 euro, i kolejną zniżką na 20 euro i tak dalej. To oczywiście wpływa na cenę obu naszych cenników. Jak to realizujemy?

Cóż, jednym ze sposobów na zrobienie tego może być utworzenie dodatkowych specjalizacji cennika, wywodzących się z cennika podstawowego dla ComputersPriceList:
ComputersPriceListWithTenEuroDiscount, ComputersPriceListWithTwentyEuroDiscount i dwóch innych wywodzących się z TVPriceList:
TVPriceListWithTenEuroDiscount, TVPriceList WithTwentyEuroDiscount. Dla mnie to nie wygląda dobrze.

Wyobraź sobie, że dodamy kolejne cenniki, RadioPricelist. W tym celu również będziemy musieli stworzyć dodatkowe podklasy. Lub wyobraź sobie dodawanie kolejnych zniżek. Będziemy potrzebować nowej podklasy dla każdego cennika. Więc jak możemy sobie wyobrazić, podejście do tego w ten sposób, poprzez dodanie podklas, prowadzi do wielu dodatkowych klas i złożoności. Dodawanie do hierarchii nowych typów spowoduje jej wykładniczy wzrost. To jest niepotrzebne i coś, czego chcemy uniknąć, i tu właśnie pojawia się wzorzec Most.

Rozwiązanie

Wzorzec Most próbuje rozwiązać ten problem poprzez przestawienie się z dziedziczenia na kompozycję obiektów. Oznacza to, że ekstrahujemy jeden z tych wymiarów i tworzymy osobną hierarchię klas, przez co pierwotne klasy będą posiadały odniesienie do obiektów z nowej hierarchii, zamiast przechowywać wszystkie swoje stany i zachowanie wewnątrz klasy.

Utwórzmy abstrakcję Cennik (PriceList) i Cennik komputerów (ComputersPriceList) i cennik telewizorów (TVPriceList), będzie dziedziczył po tej abstrakcyjnej klasie bazowej PriceList, albo po interfejsie IPriceList, w jednej hierarchii.

Abstrakcyjna klasa bazowa zawiera definicję CalculatePrice, a ComputersPriceList i TVPriceList ją implementują, ale aby móc obliczyć cenę, nasze cenniki muszą znać wartość zniżki.

Zniżki (discounts) są to implementatory, które możemy umieścić w osobnej hierarchii. Tak więc będziemy mieli abstrakcyjną klasę bazową Discount lub interfejs IDiscount, który stwierdza, że wartość zniżki musi być ujawniona.

A teraz mamy jego implementacje, TenEuroDiscount i TwentyEuroDiscount, które eksponują rzeczywistą wartość zniżki. Każdy cennik potrzebuje wtedy dostępu do zniżki, podczas obliczania ceny, i możemy to zrobić przy pomocy kompozycji.

Klasa cennik Pricelist oparta na abstrakcji będzie wymagała abstrakcyjnego obiektu zniżki lub czegoś, co implementuje interfejs IDiscount. Ponieważ oznacza to, że powinna istnieć wartość zniżki, do której można uzyskać dostęp, aby można było obliczyć prawidłową cenę. Ta relacja między cennikiem a zniżką, to Most, ponieważ łączy abstrakcję i implementację. Z tego przykładu możemy teraz wyodrębnić strukturę tego wzorca.

Struktura

Abstrakcyjna klasa PriceList jest strukturalnie nazywana abstrakcją, ponieważ ukrywa złożoność uzyskiwania wartości zniżki. Zawiera odniesienie do implementatora. Dlatego zamieniamy naszą klasę PriceList na abstrakcję.

Konkretna implementacja tej abstrakcji, podobnie jak nasze klasy ComputersPriceList i TVPriceList, noszą nazwę RefinedAbstraction.

Rozszerzają one interfejs zdefiniowany przez abstrakcję. Następnie możemy zaimplementować pobieranie wartości zniżki jako właściwość, którą możemy uzyskać na podstawie obiektu zniżki lub interfejsu, lub jako metodę. Bez względu na to, jak to zrobimy, faktem jest, że implementacja jest ukryta. Podstawa zniżki jest zatem realizatorem (implementatorem).

Definiuje interfejs dla konkretnych implementacji. Interfejs nie musi odpowiadać interfejsowi abstrakcji. W rzeczywistości przez większość czasu jest zupełnie inaczej. PriceList i IDiscount są zdecydowanie inne. Klasy takie jak TenEuroDiscount i TwentyEuroDiscount są konkretnymi implementacjami tego interfejsu Implementor.

ConcreteImplementor implementuje interfejs Implementor i definiuje jego konkretną implementację. I w ten sposób wiemy, jak zbudowany jest wzór mostu. Zaimplementujmy to teraz w kodzie.

Przykład implementacji

Zaczniemy od dodania naszej bazy abstrakcji, czyli PriceList. Użyjemy do tego klasy abstrakcyjnej, której użyjemy jako klasy bazowej dla dopracowanych abstrakcji.

/// <summary>
/// Abstraction
/// </summary>
public abstract class PriceList
{
   public abstract int CalculatePrice();
}

Klasa ma jedną metodę, CalculatePrice. Jest to metoda abstrakcyjna, ponieważ będzie musiała zostać zaimplementowana w klasach, które implementują klasę PriceList. Jak wiemy, ta klasa PriceList wymaga dostępu do operacji zaimplementowanej przez realizatora.

Zrobimy to za pomocą obiektu Discount.

/// <summary>
/// Implementor
/// </summary>
public abstract class Discount
{
   public abstract int DiscountValue { get; }
}

Może to być prosta klasa abstrakcyjna eksponująca jedną właściwość pobierającą w celu uzyskania wartości. Jest to implementacji operacji, która zostanie efektywnie zaimplementowana na klasach wywodzących się z tej abstrakcyjnej klasy Discount. Zauważ, że może to być również interfejs. W rzeczywistości zróbmy to za pomocą interfejsu, ponieważ nie zawiera żadnych szczegółów implementacji. To tylko kontrakt.

/// <summary>
/// Implementor
/// </summary>
public interface IDiscount
{
   int DiscountValue { get; }
}

IDiscount będzie potrzebować liczby całkowitej, DiscountValue. Następnie dodajmy kilka konkretnych implementacji. Zaczniemy od NoDiscount.

/// <summary>
/// ConcreteImplementor
/// </summary>
public class NoDiscount : IDiscount
{
   public int DiscountValue { get => 0; }
}

A teraz dodajemy jeszcze 2 zniżki, jedna w wysokości 10 euro, druga w wysokości 20 euro.

/// <summary>
/// ConcreteImplementor
/// </summary>
public class TenEuroDiscount : IDiscount
{
   public int DiscountValue { get => 10; }
}

/// <summary>
/// ConcreteImplementor
/// </summary>
public class TwentyEuroDiscount : IDiscount
{
   public int DiscountValue { get => 20; }
}

Wróćmy do naszej abstrakcji. Tutaj możemy teraz dodać zniżkę jako pole. Mówimy, że jest pole tylko do odczytu. Nie chcemy, aby to się zmieniło po zbudowaniu cennika. Dodajemy tutaj również konstruktora, który akceptuje taką zniżkę i ustawia pole.

/// <summary>
/// Abstraction
/// </summary>
public abstract class PriceList
{
   public readonly IDiscount _discount;

   public abstract int CalculatePrice();

   public PriceList(IDiscount discount)
   {
      _discount = discount;
   }
}

A teraz stwórzmy nasze dwa typy cennika. Są to tak zwane wyrafinowane abstrakcje. Zaczynamy od ComputersPriceList. Przy pomocy konstruktora wywołujemy konstruktor bazowy, upewniając się, że ustawiona jest wartość zniżki. W ten sposób zapewniamy, że zniżka nie jest pusta. W metodzie CalculatePrice używamy gettera DiscountValue, aby uzyskać rzeczywistą wartość DiscountValue, którą odejmujemy od ceny standardowego cennika.

/// <summary>
/// RefinedAbstraction
/// </summary>
public class ComputersPriceList : PriceList
{
   public ComputersPriceList(IDiscount discount) : base(discount)
   {
   }

   public override int CalculatePrice()
   {
      return 2000 - _discount.DiscountValue;
   }
}

Następnie robimy to samo dla TVPriceList. No i to tyle, jeśli chodzi o implementację wzorca.

/// <summary>
/// RefinedAbstraction
/// </summary>
public class TVPriceList : PriceList
{
   public TVPriceList(IDiscount coupon) : base(coupon)
   {
   }
   
   public override int CalculatePrice()
   {
      return 500 - _discount.DiscountValue;
   }
}

Teraz możemy to przetestować, przechodzimy do program.cs

var computersPriceList = new ComputersPriceList(new NoDiscount());
Console.WriteLine($"Cena komputera, bez rabatu: {computersPriceList.CalculatePrice()} euro.");

computersPriceList = new ComputersPriceList(new TenEuroDiscount());
Console.WriteLine($"Cena komputera, z rabatem 10 euro: {computersPriceList.CalculatePrice()} euro.");

var tvPriceList = new TVPriceList(new NoDiscount());
Console.WriteLine($"Cena telewizoru, bez rabatu: {tvPriceList.CalculatePrice()} euro.");

tvPriceList = new TVPriceList(new TenEuroDiscount());
Console.WriteLine($"Cena telewizoru, z rabatem 10 euro: {tvPriceList.CalculatePrice()} euro.");

Console.ReadKey();

Tworzymy cennik dla komputera ComputersPriceList i przekazujemy zniżkę NoDiscount, następnie wypisujemy cenę. Następnie tworzymy kilka nowych cenników i przekazujemy inną zniżkę.

Wynik działania

Cena komputera, bez rabatu: 2000 euro.
Cena komputera, z rabatem 10 euro: 1990 euro.
Cena telewizoru, bez rabatu: 500 euro.
Cena telewizoru, z rabatem 10 euro: 490 euro.

Nasze zniżki są stosowane do różnych cenników. Dzięki wzorowi mostu bardzo łatwo jest teraz dodać nowe cenniki lub nowe zniżki. Nie będzie to już skutkowało eksplozją klas.

/// <summary>
/// RefinedAbstraction
/// </summary>
public class RadioPricelist : PriceList
{
   public RadioPricelist(IDiscount coupon) : base(coupon)
   {
   }

   public override int CalculatePrice()
   {
      return 300 - _discount.DiscountValue;
   }
}
/// <summary>
/// ConcreteImplementor
/// </summary>
public class HundredEuroDiscount : IDiscount
{
   public int DiscountValue { get => 100; }
}

I wykorzystać je w klasie program.cs

using Bridge;
Console.Title = "Bridge";

var tvPriceList = new TVPriceList(new NoDiscount());
Console.WriteLine($"Cena telewizoru, bez rabatu: {tvPriceList.CalculatePrice()} euro.");

tvPriceList = new TVPriceList(new TenEuroDiscount());
Console.WriteLine($"Cena telewizoru, z rabatem 10 euro: {tvPriceList.CalculatePrice()} euro.");

tvPriceList = new TVPriceList(new TwentyEuroDiscount());
Console.WriteLine($"Cena telewizoru, z rabatem 20 euro: {tvPriceList.CalculatePrice()} euro.");

tvPriceList = new TVPriceList(new HundredEuroDiscount());
Console.WriteLine($"Cena telewizoru, z rabatem 100 euro: {tvPriceList.CalculatePrice()} euro.");


var computersPriceList = new ComputersPriceList(new NoDiscount());
Console.WriteLine($"Cena komputera, bez rabatu: {computersPriceList.CalculatePrice()} euro.");

computersPriceList = new ComputersPriceList(new TenEuroDiscount());
Console.WriteLine($"Cena komputera, z rabatem 10 euro: {computersPriceList.CalculatePrice()} euro.");

computersPriceList = new ComputersPriceList(new TwentyEuroDiscount());
Console.WriteLine($"Cena komputera, z rabatem 20 euro: {computersPriceList.CalculatePrice()} euro.");

computersPriceList = new ComputersPriceList(new HundredEuroDiscount());
Console.WriteLine($"Cena komputera, z rabatem 100 euro: {computersPriceList.CalculatePrice()} euro.");


var radioPricelist = new RadioPricelist(new NoDiscount());
Console.WriteLine($"Cena radia, bez rabatu: {radioPricelist.CalculatePrice()} euro.");

radioPricelist = new RadioPricelist(new TenEuroDiscount());
Console.WriteLine($"Cena radia, z rabatem 10 euro: {radioPricelist.CalculatePrice()} euro.");

radioPricelist = new RadioPricelist(new TwentyEuroDiscount());
Console.WriteLine($"Cena radia, z rabatem 20 euro: {radioPricelist.CalculatePrice()} euro.");

radioPricelist = new RadioPricelist(new HundredEuroDiscount());
Console.WriteLine($"Cena radia, z rabatem 100 euro: {radioPricelist.CalculatePrice()} euro.");

Console.ReadKey();

Zastosowanie

Powód, dla którego ten wzór jest bardzo powszechny, to przede wszystkim sytuacja, w której chcemy uniknąć trwałego powiązania abstrakcji z jej implementacją, na przykład, gdy implementacja musi zostać wybrana w czasie wykonywania. Nasz przykład pokazał to. Innymi słowy, wzorzec mostu umożliwia możliwości wyboru implementacji w trakcie działania programu.

Używamy tego wzorca, gdy chcemy rozszerzyć klasę na kilku niezależnych płaszczyznach. Gdy abstrakcja i implementacje powinny być rozszerzalne przez podklasy. Wzorzec mostu umożliwia łączenie różnych abstrakcji i implementacji oraz niezależnie je rozszerza.

Innym dobrym przypadkiem użycia jest sytuacja, gdy nie chcemy, aby zmiany w implementacji abstrakcji miały wpływ na klienta. Jeśli chodzi o klienta, powinno to być całkowicie przejrzyste dzięki luźnemu sprzężeniu.

Zalety i wady

Implementacja nie jest na stałe związana z abstrakcją. Można ją konfigurować, a potencjalnie nawet zmieniać w czasie wykonywania. Ze względu na to rozdzielenie zachęca się do tworzenia warstw, co zwykle skutkuje lepszą strukturą systemu.

Zasada otwarte/zamknięte. Możemy wprowadzać nowe abstrakcje i implementacje niezależnie od siebie. Ponieważ hierarchie abstrakcji i implementacji są oddzielone, mogą ewoluować i być niezależnie rozszerzane.

Zasada pojedynczej odpowiedzialności. W abstrakcji możesz skupić się na wysokopoziomowej logice, w implementacji na szczegółach platformy. Szczegóły implementacji można ukryć przed klientami, co jak wiemy, jest konsekwencją abstrakcji. Wzorzec pozwala również skupić się na logice wysokiego poziomu w abstrakcji oraz na szczegółach implementacji.

Kod źródłowy

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *