Bridge (Most)

bridge uml

Opis

Wzorzec projektowy Bridge (Most) należy do grupy wzorców strukturalnych. Pozwala rozdzielić abstrakcję od implementacji odpowiednio delegując zachowania. Strukturą klas może przypominać także Adapter, czy Strategię. Wszystko to ze względu na agregację, a dokładniej kompozycję z której korzysta wiele wzorców projektowych. Ważne jednak, że za użyciem każdego z nich stoją zupełnie inne powody.

Problem i rozwiązanie

Istnieją przypadki, w których hierarchia klas mogłaby zostać rozszerzana na różnych płaszczyznach. O ile w ogóle istnieje jakaś hierarchia klas, a nie jest to jedna wielka plątanina zapakowana w jeden plik. Jako, że prezentuję wzorce programowania obiektowego to pominę ten przypadek. Nie oznacza to że podczas refaktoryzacji nie da się takiego spaghetti zamienić na właściwie zbudowane komponenty.

W każdym razie, wracając do rozszerzania klas. Oprę się na koncepcie przedstawionym w dalszej części wpisu. System benefitów przydzielanych na podstawie stanowiska. Diagram klas najlepiej zobrazuje problem.

Błędna struktura klas UML

Teraz, gdyby doszedł nowy poziom (Regular) trzeba będzie rozszerzyć hierarchię w dwóch miejscach. Za to, gdyby doszedł kolejny benefit, trzeba będzie powielić hierarchię również dla niego. Widać więc, że klasy rosną w tempie wykładniczym. Nie jest to jedyna trudność. Wielopoziomowe dziedziczenie z pomieszaną logiką i często brak pojedynczej odpowiedzialności to znacznie większy kłopot.

Bridge pozwoli rozbić tę drzewiastą strukturę na dwie mniejsze przy użyciu kompozycji. Wydeleguje odpowiednie zachowania do nowego interfejsu JobLevel będącego abstrakcją dla klas: Junior i Senior. Przy takim wykorzystaniu polimorfizmu wszystko układa się w odpowiednią całość. Rozszerzenie jednej hierarchii nie powoduje zmian w drugiej i odwrotnie. Odbywa się to niezależne na dwóch płaszczyznach. Do tego wszystkiego zachodzi odpowiednia hermetyzacja logiki, ale o zaletach i wadach to już osobny paragraf.

Plusy i minusy

Jedyny minus jaki dostrzegam w tym rozwiązaniu to problematyczne określanie odpowiedzialności klas. Oczywiście, kłopot ten występuję tylko przy bardziej złożonych implementacjach. W przykładach podobnych do poniższego raczej taka sytuacja nie będzie mieć miejsca. W najgorszym wypadku, zły podział można stosunkowo łatwo uratować. Na pocieszenie dodam, że jedna ogromna klasa z wszystkimi odpowiedzialnościami to też błędne ich określenie.

Wzorzec projektowy Bridge wspiera zasady SOLID, a szczególnie dwie pierwsze, czyli pojedynczą odpowiedzialność (single responsibility) i otwarte-zamknięte (open-closed). Zamiast zbędnego dziedziczenia pojawia się wspomniana już wcześniej kompozycja. Rozwiązanie to gwarantuje także niezależność między abstrakcją, a implementacją. Przejawia się ona właśnie przy rozbudowie jednej lub drugiej.

Co ciekawe, Most to jeden z nielicznych wzorców, który zmniejsza złożoność. Patrząc na wcześniejszy diagram klas (błędny), a poniższą implementację – na pierwszy rzut oka widać różnicę. Zazwyczaj przy patternach zaobserwować można rosnącą liczbę klas lub skomplikowane powiązania między nimi. Tutaj sytuacja jest korzystna.

Przykładowa implementacja w PHP

Przykład kodu prezentuje system benefitów w organizacji. Współczynnik autorytetu wyliczany jest na podstawie doświadczenia i ma swoje odzwierciedlenie w wysokości budżetu na dany dodatek. Interfejs JobLevelInterface jest implementacją, a klasa AbstractBenefit abstrakcją. Taki podział sprawia, że odpowiedzialnością klasy konkretnego benefitu jest tylko i wyłącznie określenie wysokości dofinansowania. W oparciu o autorytet pracownika. Samo wyliczenie tego współczynnika jest delegowane do interfejsu JobLevelInterface, który wstrzykiwany jest przez konstruktor.

Można spotkać też implementację pozwalającą na dynamiczną podmianę interfejsu w trakcie cyklu życia obiektu. Podobnie zresztą jak ma to miejsce we wzorcu Strategia. W tym konkretnym przykładzie byłaby to funkcja setLevel().

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge;

interface JobLevelInterface
{
    public const int MAX_AUTHORITY = 10;

    public function getAuthorityFactor(): float;
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge;

final class Junior implements JobLevelInterface
{
    private const int AUTHORITY = 3;

    public function getAuthorityFactor(): float
    {
        return self::AUTHORITY / self::MAX_AUTHORITY;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge;

final class Senior implements JobLevelInterface
{
    private const int AUTHORITY = 7;

    public function getAuthorityFactor(): float
    {
        return self::AUTHORITY / self::MAX_AUTHORITY;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge;

abstract class AbstractBenefit
{
    public function __construct(
        protected JobLevelInterface $level
    ) {}

    abstract public function calculateGrant(): float;
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge;

final class HealthCare extends AbstractBenefit
{
    private const int MAX_GRANT = 1200;

    public function calculateGrant(): float
    {
        return $this->level->getAuthorityFactor() * self::MAX_GRANT;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge;

final class TrainingBudget extends AbstractBenefit
{
    private const int MAX_GRANT = 1000;
    private const int BONUS_RATIO = 3;

    public function calculateGrant(): float
    {
        $authorityFactor = $this->level->getAuthorityFactor();

        $grant = $this->level->getAuthorityFactor() * self::MAX_GRANT;
        return $this->bonus($authorityFactor, $grant);
    }

    private function bonus(float $authorityFactor, float $grant): float
    {
        $halfOfMaxAuthority = (JobLevelInterface::MAX_AUTHORITY / 2) / JobLevelInterface::MAX_AUTHORITY;
        if ($authorityFactor < $halfOfMaxAuthority) {
            return $grant * self::BONUS_RATIO;
        }

        return $grant;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge\Test;

use DesignPatterns\Structural\Bridge\HealthCare;
use DesignPatterns\Structural\Bridge\Junior;
use DesignPatterns\Structural\Bridge\Senior;
use PHPUnit\Framework\TestCase;

final class HealthCareTest extends TestCase
{
    public function testCalculatingGrantForJunior(): void
    {
        $healthCareJuniorGrant = 360.00;

        $healthCare = new HealthCare(new Junior());

        self::assertSame($healthCareJuniorGrant, $healthCare->calculateGrant());
    }

    public function testCalculatingGrantForSenior(): void
    {
        $healthCareSeniorGrant = 840.00;

        $healthCare = new HealthCare(new Senior());

        self::assertSame($healthCareSeniorGrant, $healthCare->calculateGrant());
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge\Test;

use DesignPatterns\Structural\Bridge\Junior;
use DesignPatterns\Structural\Bridge\Senior;
use DesignPatterns\Structural\Bridge\TrainingBudget;
use PHPUnit\Framework\TestCase;

final class TrainingBudgetTest extends TestCase
{
    public function testCalculatingGrantForJunior(): void
    {
        $trainingBudgetJuniorGrant = 900.00;

        $trainingBudget = new TrainingBudget(new Junior());

        self::assertSame($trainingBudgetJuniorGrant, $trainingBudget->calculateGrant());
    }

    public function testCalculatingGrantForSenior(): void
    {
        $trainingBudgetSeniorGrant = 700.00;

        $trainingBudget = new TrainingBudget(new Senior());

        self::assertSame($trainingBudgetSeniorGrant, $trainingBudget->calculateGrant());
    }
}

Bridge – podsumowanie

Wzorzec projektowy Most może z powodzeniem zagościć w dobrze napisanym kodzie. Jeżeli struktura klas może się rozrastać ze względu na dwa lub więcej typów to należy rozważyć jego użycie. Szczególnie tam, gdzie zamiast posługiwać się mechanizmami obiektowymi jak polimorfizm, sięga się po prostsze (w teorii) rozwiązania. Przy większych projektach, gdzie nieustannie powstają nowe funkcjonalności, droga na skróty nie popłaci (za to wykorzystanie tego patternu już tak). A nawet jeśli hierarchia klas jaką zapewnia Bridge nie będzie rozszerzana to do końca zostanie w takiej samej, właściwej postaci.

Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan dobrego humoru i podcastów.