Bridge (Most)

bridge uml

Opis

Wzorzec projektowy Bridge (Most) należy do grupy wzorców strukturalnych. Pozwala rozdzielić abstrakcję od implementacji odpowiednio delegując pewne zachowania. Struktura 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 jednym pliku. Jako, że prezentuję wzorce programowania obiektowego to pominę ten przykład. 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 ze względu na dwa typy. Oprę się na koncepcie przedstawionym poniżej za pomocą kodu, czyli systemie benefitów na podstawie stanowiska. Poniższy diagram klas najlepiej zobrazuje problem:

Błędna struktura klas UML

Teraz gdyby doszedł nowy poziom na przykład 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, czyli 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 pojedyncza odpowiedzialność i otwarte-zamknięte. Dodatkowo 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 powyższy 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 prosta i 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 w tym przypadku 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 w konstruktorze.

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 MAX_AUTHORITY = 10;

    public function getAuthorityFactor(): float;
}

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge;

class Junior implements JobLevelInterface
{
    private const AUTHORITY = 3;

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

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge;

class Senior implements JobLevelInterface
{
    private const AUTHORITY = 7;

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

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge;

abstract class AbstractBenefit
{
    protected JobLevelInterface $level;

    public function __construct(JobLevelInterface $level)
    {
        $this->level = $level;
    }

    abstract public function calculateGrant(): float;
}

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge;

class HealthCare extends AbstractBenefit
{
    private const MAX_GRANT = 1200;

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

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Bridge;

class TrainingBudget extends AbstractBenefit
{
    private const MAX_GRANT = 1000;

    private const BONUS_RATIO = 3;

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

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

        return $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());

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

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

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

        $this->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());

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

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

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

        $this->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 rozrastać się 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 postaci – właściwej postaci.

Krystian Żądło
Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan angielskiej piłki nożnej, dobrego humoru oraz podcastów.