Pozwala oddzielić abstrakcję obiektu od jego implementacji.
Formatter.php
<?php declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
interface Formatter
{
public function format(string $text): string;
}
PlainTextFormatter.php
<?php declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
class PlainTextFormatter implements Formatter
{
public function format(string $text): string
{
return $text;
}
}
HtmlFormatter.php
<?php declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
class HtmlFormatter implements Formatter
{
public function format(string $text): string
{
return sprintf('<p>%s</p>', $text);
}
}
Service.php
<?php declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
abstract class Service
{
protected Formatter $implementation;
public function __construct(Formatter $printer)
{
$this->implementation = $printer;
}
public function setImplementation(Formatter $printer)
{
$this->implementation = $printer;
}
abstract public function get(): string;
}
HelloWorldService.php
<?php declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
class HelloWorldService extends Service
{
public function get(): string
{
return $this->implementation->format('Hello World');
}
}
PingService.php
<?php declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
class PingService extends Service
{
public function get(): string
{
return $this->implementation->format('pong');
}
}
Testy
Tests/BridgeTest.php
<?php declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge\Tests;
use DesignPatterns\Structural\Bridge\HelloWorldService;
use DesignPatterns\Structural\Bridge\HtmlFormatter;
use DesignPatterns\Structural\Bridge\PlainTextFormatter;
use PHPUnit\Framework\TestCase;
class BridgeTest extends TestCase
{
public function testCanPrintUsingThePlainTextFormatter()
{
$service = new HelloWorldService(new PlainTextFormatter());
$this->assertSame('Hello World', $service->get());
}
public function testCanPrintUsingTheHtmlFormatter()
{
$service = new HelloWorldService(new HtmlFormatter());
$this->assertSame('<p>Hello World</p>', $service->get());
}
}
Wzorca projektowego pyłek (ang. flyweight) można użyć, jeśli mamy naprawdę dużo małych obiektów (małych jak muchy), które różnią się tylko podanym stanem lub wieloma danymi, które działają na podobnych danych wejściowych (lub część tego stanu jest powtarzalna).
Wyobraź sobie, że importujesz ogromny plik CSV z kilkoma tysiącami wierszy informacji o telewizorach rozmieszczonych po całej Europie. Twoim zadaniem jest sprawdzanie kondycji każdego z nich za pomocą pojedynczego połączenia z jego adresem IP i zalogowanie informacji o tym urządzeniu.
Kilka tysięcy nowych obiektów. To naprawdę sporo danych do przechowywania w pamięci RAM. Na szczęście duża część danych w wierszach jest powtarzalna. Możemy ponownie wykorzystywać obiekty, które już stworzyliśmy i pracować na nich. To esencja wzorca pyłek.
Zacznijmy od utworzenia przykładowego raportu. Mamy wiele różnych telewizorów umieszczonych w wielu lokalizacjach. Każdy z nich ma unikalny identyfikator użytkownika i kilka innych informacji. W tym przykładzie utworzymy 5 tys. wierszy danych.
Nasz raport jest już gotowy. Teraz musimy dowiedzieć się, jak stworzyć aplikację, która będzie odpytywać każde urządzenie. Zauważyliśmy, że każdy ma unikalny identyfikator użytkownika i adres IP. Inne pola są dość powtarzalne i możemy je przechowywać w osobnym obiekcie. Nazwijmy to DeviceType.
<?php
namespace structural\flyweight;
class DeviceType {
protected $location;
protected $resolution;
protected $producer;
protected $operatingSystem;
public function __construct (
string $location,
string $resolution,
string $producer,
string $operatingSystem
) {
$this->location = $location;
$this->resolution = $resolution;
$this->producer = $producer;
$this->operatingSystem = $operatingSystem;
}
public function reportType () {
return "Working on device in {$this->location} with resolution {$this->resolution} crated by {$this->producer} and running {$this->operatingSystem}";
}
}
Ponieważ możemy ponownie wykorzystywać obiekty, stwórzmy fabrykę, która będzie odpowiedzialna za cache'owanie ich i tworzenie nowych w razie potrzeby.
Rozpoznajemy każdy typ urządzenia, tworząc hash z wszystkich jego parametrów. Teraz stwórzmy nasze urządzenie.
<?php
namespace structural\flyweight;
class Device {
protected $uid;
protected $ip;
protected $type;
public function __construct (
string $uid,
string $ip,
DeviceType $type
) {
$this->uid = $uid;
$this->ip = $ip;
$this->type = $type;
}
public function ping () {
echo "Checking if device {$this->uid} is active" . PHP_EOL;
$this->type->reportType() . PHP_EOL;
echo "Calling it on ip {$this->ip}" . PHP_EOL;
}
}
Ten kod odpowiada za sprawdzenie stanu każdego urządzenia. Urządzenia te tworzone są za pomocą DeviceStorage, który również śledzi wszystkie utworzone obiekty.
<?php
namespace structural\flyweight;
class DeviceStorage {
public $devices = [];
public $deviceFactory;
public function __construct () {
$this->deviceFactory = new DeviceTypeFactory();
}
public function addDevice (
string $uuid,
string $location,
string $resolution,
string $producer,
string $operatingSystem,
string $ip
) {
$type = $this->deviceFactory->getType(
$location,
$resolution,
$producer,
$operatingSystem
);
$this->devices[] = new Device($uuid, $ip, $type);
}
public function checkDevicesHealth () {
foreach ($this->devices as $device) {
$device->ping();
}
}
}
Aplikacja z wykorzystaniem Flyweight zużyła około 2,1 MB na zestawie danych 5 tys. I 13,6 MB na zestawie danych 50 tys.
Bez użycia współużytkowanego DeviceType było to 3,3 MB dla 5k i 25,7 MB dla 50k. To ogromna oszczędność.
Pyłek jest rzadko używany w aplikacjach webowych PHP, ponieważ każde żądanie w PHP jest całkowicie niezależne. Często nie przechowujemy danych bezpośrednio w pamięci RAM, a raczej w niektórych trwałych bazach danych lub pamięci podręcznej. Niemniej jednak ten wzorzec może być całkiem przydatny w konkretnych przypadkach użycia lub aplikacjach wiersza poleceń.
Oryginał tekstu w języku angielskim przeczytasz tutaj.
Jak widać, wszystkie jednostki naszego modelu rozszerzają klasę Unit. Użytkownik może więc być pewien, że każdy z obiektów hierarchii Unit będzie obsługiwał metodę bombardStrength(). Klasa Unit może być traktowana identycznie jak Archer. Klasy Army i TroopCarrier są kompozytami (ang. composite) — obiektami składającymi się z innych obiektów. Klasy Archer i LaserCannonUnit to liście bądź końcówki (ang. leaves), klasy reprezentujące końcowe węzły struktury drzewiastej przystosowane do obsługi podstawowych operacji hierarchii, ale nienadające się do przechowywania innych jej obiektów. Pojawia się wątpliwość, czy liście powinny implementować identyczny interfejs, co kompozyty (jak na rysunku 10.1). Na diagramie widać, że TroopCarrier i Army agregują inne jednostki, ale klasy liści również implementują wywołanie addUnit() — wrócimy do tego wkrótce. Na razie przyjrzyjmy się abstrakcyjnej klasie Unit:
<?php
abstract class Unit
{
abstract function addUnit(Unit $unit);
abstract function removeUnit(Unit $unit);
abstract function bombardStrength();
}
//Mamy tu zarys podstawowej funkcjonalności wszystkich obiektów hierarchii Unit. Spójrzmy teraz,
//jak wymienione metody abstrakcyjne mogłyby być implementowane w obiektach-kompozytach:
class Army extends Unit
{
private $units = array();
function addUnit(Unit $unit)
{
if (in_array($unit, $this->units, true))
{
return;
}
$this->units[] = $unit;
}
function removeUnit(Unit $unit)
{
$this->units = array_udiff($this->units, array(
$unit
) , function ($a, $b)
{
return ($a === $b) ? 0 : 1;
});
}
function bombardStrength()
{
$ret = 0;
foreach ($this->units as $unit)
{
$ret += $unit->bombardStrength();
}
return $ret;
}
}
Metoda addUnit() klasy Army przed włączeniem do oddziału przekazywanej w wywołaniu jednostki sprawdza, czy nie posiada jej już w prywatnej tablicy jednostek. Metoda removeUnit() sprawdza w pętli (podobnie jak metoda addUnit()), czy usunąć dany obiekt Unit.
Obiekty klasy Army mogą przechowywać dowolnego rodzaju obiekty hierarchii Unit, w tym inne obiekty klasy Army lub końcówki takie jak Archer czy LaserCannonUnit. Ponieważ wszystkie jednostki mają implementować metodę bombardStrength(), implementacja tej metody w klasie Army sprowadza się do przejrzenia wszystkich obiektów zawieranych, przechowywanych w składowej $units, i sumowania wartości zwracanych z inicjowanych na ich rzecz wywołań bombardStrength(). Problematycznym aspektem wzorca pozostaje implementacja operacji wcielania i usuwania jednostek. Klasyczny wzorzec zakłada definicję metod add...() i remove...() w abstrakcyjnej klasie bazowej. Dzięki temu wszystkie klasy objęte szablonem udostępniają wspólny interfejs. Ale przez to implementacje tych m
<?php
class UnitException extends Exception
{
}
class Archer extends Unit
{
function addUnit(Unit $unit)
{
throw new UnitException(get_class($this) . " to liść");
}
function removeUnit(Unit $unit)
{
throw new UnitException(get_class($this) . " to liść");
}
function bombardStrength()
{
return 4;
}
}
Z definicji klasa Archer nie jest przewidziana do przechowywania obiektów hierarchii Unit, więc na wywołanie na rzecz obiektu Archer metody addUnit() albo removeUnit() reagujemy zgłoszeniem wyjątku. Ponieważ musielibyśmy podobną implementację przewidzieć dla wszystkich klas końcówek (liści), możemy zdecydować się na jej przeniesienie do abstrakcyjnej klasy bazowej
<?php
abstract class Unit
{
abstract function bombardStrength();
function addUnit(Unit $unit)
{
throw new UnitException(get_class($this) . " to liść");
}
function removeUnit(Unit $unit)
{
throw new UnitException(get_class($this) . " to liść");
}
}
class Archer extends Unit
{
function bombardStrength()
{
return 4;
}
}
// utworzenie armii
$main_army = new Army();
// włączenie do niej paru jednostek
$main_army->addUnit(new Archer());
$main_army->addUnit(new LaserCannonUnit() );
// utworzenie nowej armii
$sub_army = new Army();
// zaciąg do nowej armii
$sub_army->addUnit(new Archer());
$sub_army->addUnit(new Archer());
$sub_army->addUnit(new Archer());
// wcielenie drugiej armii do pierwszej
$main_army->addUnit($sub_army);
// obliczenia siły ataku wykonywane automatycznie w tle
print "Atak z siłą: {$main_army->bombardStrength()}\n";
Do utworzonego oddziału głównego dodajemy kilka jednostek podstawowych. Proces ten powtarzamy dla drugiego utworzonego oddziału, który następnie wcielamy do pierwszego. Przy obliczaniu siły rażenia (Unit::bombardStrength()) wynikowego oddziału złożoność struktury hierarchii obiektów jest dla wywołującego zupełnie niewidoczna. Konsekwencje Jeśli Czytelnik myśli podobnie jak ja, powinien na widok kodu klasy Archer nabrać podejrzeń. Po co bowiem do klas końcówek włączamy metody addUnit() i removeUnit(), jeśli nie ma potrzeby obsługiwania operacji wcielania i usuwania jednostek? Odpowiedź tkwi w przezroczystości typu Unit. Jeśli użytkownik otrzymuje obiekt typu Unit, ma pewność, że obiekt ten implementuje metody addUnit() i removeUnit(). Uwidacznia się tu przyjęta we wzorcu Composite zasada, że klasy obiektów niepodzielnych (liści) mają interfejs identyczny z klasami kompozytów. Taka odpowiedź jest jednak mało satysfakcjonująca, ponieważ honorowanie interfejsu nie oznacza w tym przypadku bezpieczeństwa wywołania metod addUnit() czy removeUnit() na rzecz każdego z obiektów hierarchii Unit. Gdybyśmy owe metody przesunęli tak, aby były dostępne jedynie dla klas kompozytów, wtedy z kolei powstałby problem niepewności co do tego, czy otrzymany obiekt hierarchii Unit obsługuje czy nie obsługuje daną metodę. Mimo wszystko pozostawienie metod-pułapek w klasach liści to dla mnie sytuacja mało komfortowa. Nie ma tu wartości dodanej, a jedynie zamieszanie w projekcie systemu, ponieważ interfejs w zasadzie okłamuje użytkowników co do swojej własnej funkcjonalności. Moglibyśmy w prosty sposób wyeliminować tę niedogodność, wydzielając dla kompozytów ich własny podtyp CompositeUnit. Polegałoby to przede wszystkim na usunięciu metod addUnit() i removeUnit() z klasy Unit:
<?php
abstract class Unit
{
function getComposite()
{
return null;
}
abstract function bombardStrength();
}
Zwróćmy uwagę na metodę getComposite(). Wrócimy do niej za moment. Teraz potrzebujemy abstrakcyjnej klasy definiującej metody usunięte z klasy Unit. Możemy w niej nawet przewidzieć ich implementacje domyślne:
abstract class CompositeUnit extends Unit
{
private $units = array();
function getComposite()
{
return $this;
}
protected function units()
{
return $this->units;
}
function removeUnit(Unit $unit)
{
$this->units = array_udiff($this->units, array(
$unit
) , function ($a, $b)
{
return ($a === $b) ? 0 : 1;
});
function addUnit(Unit $unit)
{
if (in_array($unit, $this->units, true))
{
return;
}
$this->units[] = $unit;
}
}
}
Klasa CompositeUnit (kompozyt jednostek) choć sama w sobie nie zawiera żadnych metod abstrakcyjnych, jest deklarowana jako abstrakcyjna. Równocześnie rozszerza klasę Unit, nie definiując jej abstrakcyjnej metody bombardStrength(). Klasa Army (i wszystkie inne klasy kompozytów) może teraz rozszerzać klasę CompositeUnit. Organizację klas po tej modyfikacji ilustruje rysunek 10.2
Wyeliminowaliśmy irytujące i bezużyteczne implementacje metod dodawania i usuwania jednostek z klas liści, ale teraz klient musi przed wywołaniem tych metod sprawdzać, czy obiekt, na rzecz którego chce zainicjować wywołanie, jest obiektem klasy CompositeUnit. Tutaj do akcji wkracza metoda getComposite(). Domyślnie zwraca ona bowiem wartość pustą. Jedynie w klasach dziedziczących po CompositeUnit wartość zwracana to obiekt klasy CompositeUnit. Jeśli więc wywołanie tej metody zwróci obiekt, można na jego rzecz wywołać metodę addUnit(). Oto zastosowanie tej techniki z punktu widzenia użytkownika:
<?php
class UnitScript
{
static function joinExisting(Unit $newUnit, Unit $occupyingUnit)
{
$comp;
if (!isnull($comp = $occupyingUnit->getComposite()))
{
$comp->addUnit($newUnit);
}
else
{
$comp = new Army();
$comp->addUnit($occupyingUnit);
$comp->addUnit($newUnit);
}
return $comp;
}
}
Metoda joinExisting() (połącz siły) przyjmuje dwa obiekty hierarchii Unit. Pierwszy z nich reprezentuje jednostkę nowo przybyłą na dane pole, drugi — jednostkę już na tym polu przebywającą (okupującą pole planszy). Jeśli druga z tych jednostek jest kompozytem (obiektem klasy CompositeUnit), wtedy pierwszy z obiektów jest do niej dodawany. W innym przypadku tworzony jest nowy obiekt klasy Army, do którego wcielane są obie jednostki. Określanie przynależności do hierarchii klas kompozytowych odbywa się za pośrednictwem metody getComposite(). Jeśli zwróci ona obiekt, możemy wprost do niego dodawać nowe obiekty klasy Unit. Jeśli wynikiem wywołania getComposite() będzie wartość pusta, musimy utworzyć obiekt kompozytu na własną rękę, tworząc egzemplarz klasy Army i wcielając do niego obie jednostki. Model można uprościć jeszcze bardziej, wymuszając w metodzie Unit::getComposite() zwrócenie obiektu Army wypełnionego początkowo bieżącą jednostką Unit. Moglibyśmy też wrócić do poprzedniego modelu (w którym nie rozróżnialiśmy pomiędzy obiektami kompozytów a liśćmi) i zrealizować to samo w metodzie Unit::addUnit(): możemy tam utworzyć obiekt Army i dodać do niego oba obiekty Unit. To eleganckie rozwiązanie,Metoda joinExisting() (połącz siły) przyjmuje dwa obiekty hierarchii Unit. Pierwszy z nich reprezentuje jednostkę nowo przybyłą na dane pole, drugi — jednostkę już na tym polu przebywającą (okupującą pole planszy). Jeśli druga z tych jednostek jest kompozytem (obiektem klasy CompositeUnit), wtedy pierwszy z obiektów jest do niej dodawany. W innym przypadku tworzony jest nowy obiekt klasy Army, do którego wcielane są obie jednostki. Określanie przynależności do hierarchii klas kompozytowych odbywa się za pośrednictwem metody getComposite(). Jeśli zwróci ona obiekt, możemy wprost do niego dodawać nowe obiekty klasy Unit. Jeśli wynikiem wywołania getComposite() będzie wartość pusta, musimy utworzyć obiekt kompozytu na własną rękę, tworząc egzemplarz klasy Army i wcielając do niego obie jednostki. Model można uprościć jeszcze bardziej, wymuszając w metodzie Unit::getComposite() zwrócenie obiektu Army wypełnionego początkowo bieżącą jednostką Unit. Moglibyśmy też wrócić do poprzedniego modelu (w którym nie rozróżnialiśmy pomiędzy obiektami kompozytów a liśćmi) i zrealizować to samo w metodzie Unit::addUnit(): możemy tam utworzyć obiekt Army i dodać do niego oba obiekty Unit. To eleganckie rozwiązanie,
<?php
class TroopCarrier
{
function addUnit(Unit $unit)
{
if ($unit instanceof Cavalry)
{
throw new UnitException("Transporter nie może przewozić koni");
}
parent::addUnit($unit);
}
function bombardStrength()
{
return 0;
}
}
Jesteśmy tu zmuszeni do testowania typu obiektu przekazanego w wywołaniu metody addUnit() za pośrednictwem operatora instanceof. Im więcej takich jak ten przypadków specjalnych, tym wady wzorca będą dokuczliwsze. Wzorzec Composite działa najlepiej wtedy, kiedy większość komponentów to obiekty wymienialne, o zbliżonej semantyce. Kolejną kwestią jest koszt niektórych operacji w ramach wzorca. Typowym przykładem jest wywołanie Army::bombardStrength(), prowokujące kaskadę wywołań propagowanych w dół drzewa struktury jednostek zawieranych w oddziale. Przy mocno rozbudowanych drzewach z wieloma pododdziałami owo jedno wywołanie może sprowokować „w tle” istną lawinę wywołań. Co prawda koszt wykonania metody bombardStrength() nie jest obecnie wysoki, łatwo jednak sobie wyobrazić efekty skomplikowania obliczania siły ataku niektórych jednostek. Jednym ze sposobów eliminacji nawału wywołań i delegowania jest buforowanie wyników poprzednich wywołań metod obiektów zawieranych w obiektach-kompozytach, tak aby w przyszłych odwołaniach do tej wartości można było pominąć narzut wywołań. Ale wtedy trzeba pilnować aktualizacji buforowanych wartości, wdrażając strategię opróżniania buforów po operacjach na drzewie obiektów. Może to wymagać wyposażenia obiektów zawieranych w referencje do obiektów kompozytów. Wreszcie słowo o trwałości. Wzorzec Composite jest co prawda wyjątkowo elegancki, ale nie bardzo nadaje się do utrwalania zbudowanej struktury obiektów w bazie danych, a to dlatego, że całe struktury traktowane są jako pojedyncze obiekty. Aby więc skonstruować taką strukturę na podstawie informacji odczytywanych z bazy danych, trzeba posłużyć się serią kosztownych zapytań. Problem można wyeliminować, przypisując do całego drzewa identyfikator, tak aby można było jednym zapytaniem wyodrębnić z bazy danych wszystkie komponenty drzewa. Po wyodrębnieniu wszystkich obiektów trzeba będzie jednak i tak odtworzyć budowę drzewa, z zachowaniem układu obiektów podrzędnych i nadrzędnych, który również trzeba odzwierciedlić w schemacie bazy danych. Nie jest to zadanie bardzo trudne, ale mimo wszystko nieco skomplikowane. Przystosowanie wzorca Composite do baz danych jest wątpliwe, zupełnie inaczej ma się sprawa z językiem XML, a to dlatego, że w XML-u bardzo łatwo tworzyć drzewiaste struktury elementów
<?php
interface Subscriber
{
public function send(): void;
}
class Client implements Subscriber
{
private $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function send(): void
{
echo 'i got it: ' . $this->name . '<br>';
}
}
class Subject
{
private $subscribers = [];
public function subscribe(Subscriber $subscriber): void
{
$this->subscribers[] = $subscriber;
}
public function startWork(): void
{
sleep(1);
//code
foreach ($this->subscribers as $subscriber) {
$subscriber->send();
}
}
}
$client1 = new Client('first client');
$client2 = new Client('second client');
$client3 = new Client('third client');
$subject = new Subject();
$subject->subscribe($client1);
$subject->subscribe($client2);
$subject->subscribe($client3);
$subject->startWork();
2)Implementacja Sedno wzorca Observer (obserwator) polega na rozdzieleniu elementów użytkujących (obserwatorów) od klasy centralnej (podmiotu obserwacji). Obserwatory muszą być informowane o zdarzeniach zachodzących w podmiocie obserwacji. Równocześnie nie chcemy wprowadzać trwałych i sztywnych zależności pomiędzy podmiotem obserwacji a klasami obserwatorów. Możemy więc umożliwić obserwatorom rejestrowanie się w klasie podmiotu. W tym celu powinniśmy uzupełnić klasę Login o trzy nowe metody: rejestracji (attach()), rezygnacji (detach()) i powiadomienia (notify()), przystosowując klasę do wymogów wyróżniających podmioty obserwacji interfejsu (tutaj ma on nazwę Observable):
<?php
interface Observable
{
function attach(Observer $observer);
function detach(Observer $observer);
function notify();
}
<?php
// Klasa Login…
class Login implements Observable
{
private $observers = array();
private $storage;
const LOGIN_USER_UNKNOWN = 1;
const LOGIN_WRONG_PASS = 2;
const LOGIN_ACCESS = 3;
function attach(Observer $observer)
{
$this->observers[] = $observer;
}
function detach(Observer $observer)
{
$this->observers = array_filter($this->observers, function ($a) use ($observer)
{
return (!($a === $observer));
});
}
function notify()
{
foreach ($this->observers as $obs)
{
$obs->update($this);
}
}
//
}
Mamy więc klasę podmiotu utrzymującą listę obiektów obserwatorów. Obiekty te są dodawane do listy z zewnątrz poprzez wywołanie metody attach(). Rezygnacja z obserwacji i usunięcie z listy następuje w wyniku wywołania metody detach(). Z kolei wywołanie metody notify() służy jako powiadomienie obiektów obserwatorów o potencjalnie interesujących ich zdarzeniach. Implementacja tej metody sprowadza się do przejrzenia tablicy obiektów obserwatorów i wywołania na rzecz każdego z nich metody update(). Wywołanie metody rozsyłającej powiadomienia następuje we wnętrzu klasy Login, w ciele metody handleLogin():
interface Observer {
function update(Observable $observable);
}
Do listy obserwatorów można dodawać (za pośrednictwem metody attach() klasy podmiotu obserwacji) dowolne obiekty, które implementują interfejs Observable. Tak wygląda tworzenie konkretnego egzemplarza:
<?php
class SecurityMonitor implements Observer
{
function update(Observable $observable)
{
$status = $observable->getStatus();
if ($status[0] == Login::LOGIN_WRONG_PASS)
{
// wyślij wiadomość do administratora…
print __CLASS__ . "\twysyłam wiadomość do administratora\n";
}
}
}
$login = new Login();
$login->attach(new SecurityMonitor());
Zwróćmy uwagę, jak obiekt obserwatora odwołuje się do egzemplarza klasy Observable (podmiotu obserwacji) w celu pozyskania dodatkowych informacji o zdarzeniu. Metody, za pośrednictwem których obiekty obserwatorów mogłyby dowiadywać się o stanie, powinny zostać udostępnione właśnie w klasie podmiotu obserwacji. W tym przypadku klasa podmiotu ma zdefiniowaną metodę getStatus(), dzięki której obiekty obserwatorów mogą dowiadywać się o bieżącym stanie obiektu obserwowanego. Pojawia się tutaj pewien problem. Otóż w wywołaniu metody Login::getStatus() klasa SecurityMonitor bazuje na wiedzy o klasie Login, na której nie powinna polegać. Przecież w wywołaniu otrzymuje obiekt Observable, ale nie ma żadnej gwarancji, że będzie to właśnie obiekt Login. Mamy tu kilka możliwości: możemy rozszerzyć interfejs Observable tak, aby zawierał w sobie deklarację metody getStatus(), i możemy od razu przemianować interfejs na ObservableLogin, sygnalizując, że ma związek z klasami Login. Możemy też utrzymać ogólny interfejs Observable i obarczyć klasy Observable odpowiedzialnością za to, aby podmioty obserwacji były odpowiedniego typu. Możemy wtedy złożyć na nie również zadanie kojarzenia się z podmiotami obserwacji. Ponieważ będziemy mieć więcej niż jeden typ Observer, a zamierzamy zaimplementować przy okazji czynności porządkowe wspólne dla wszystkich podtypów, możemy od razu udostępnić abstrakcyjną klasę bazową:
<?php
abstract class LoginObserver implements Observer
{
private $login;
function __construct(Login $login)
{
$this->login = $login;
$login->attach($this);
}
function update(Observable $observable)
{
if ($observable === $this->login)
{
$this->doUpdate($observable);
}
}
abstract function doUpdate(Login $login);
}
Klasa LoginObserver wymaga do konstrukcji obiektu typu Login. W konstruktorze zachowuje sobie referencję obiektu i wywołuje własną metodę Login::attach(). W wywołaniu update() następuje sprawdzenie, czy przekazany obiekt Observable jest w istocie referencją obserwowanego podmiotu, po czym dochodzi do wywołania metody szablonowej doUpdate(). Teraz możemy utworzyć cały zestaw obiektów LoginObserver, z których każdy będzie operował na obiekcie Login, a nie na dowolnym obiekcie implementującym nasz stary interfejs Observable:
<?php
class SecurityMonitor extends LoginObserver
{
function doUpdate(Login $login)
{
$status = $login->getStatus();
if ($status[0] == Login::LOGIN_WRONG_PASS)
{
// wysłanie wiadomości do administratora
print __CLASS__ . ":\twysyłam wiadomość do administratora\n";
}
}
}
class GeneralLogger extends LoginObserver
{
function doUpdate(Login $login)
{
$status = $login->getStatus();
// dodanie danych do rejestru
print __CLASS__ . ":\tdodaję dane logowania do rejestru\n";
}
}
class PartnershipTool extends LoginObserver
{
function doUpdate(Login $login)
{
$status = $login->getStatus();
// sprawdzenie adresu IP
// ustawienie ciasteczka dla dopuszczonego IP
print __CLASS__ . ":\tustawiam ciasteczko dla dopuszczonego IP\n";
}
}
//Tworzenie i podłączanie obserwatorów LoginObserver jest teraz wykonywane w czasie konkretyzacji obiektów:
$login = new Login();
new SecurityMonitor($login);
new GeneralLogger($login);
new PartnershipTool($login);
<?php
interface CarInterface {
public function calcualtePrice(): int;
}
abstract class CarDecorator implements CarInterface {
protected $car;
public function __construct(CarInterface $car) {
$this->car = $car;
}
}
Class Car implements CarInterface {
public function calcualtePrice(): int {
return 5000;
}
}
Class CarWithAirConditioning extends CarDecorator {
public function calcualtePrice(): int {
return $this->car->calcualtePrice() + 101;
}
}
Class CarWithSunRoof extends CarDecorator {
public function calcualtePrice(): int {
return $this->car->calcualtePrice() + 70;
}
}
Class CarWith5Skin extends CarDecorator {
public function calcualtePrice(): int {
return $this->car->calcualtePrice() + 111;
}
}
Class CarWithCamera extends CarDecorator {
public function calcualtePrice(): int {
return $this->car->calcualtePrice() + 52;
}
}
$car1 = new CarWithCamera(new CarWithAirConditioning(new Car()));
var_dump($car1->calcualtePrice());
$car2 = new CarWithAirConditioning(new CarWith5Skin(new Car()));
var_dump($car2->calcualtePrice());
$car3 = new Car();
$car3 = new CarWithAirConditioning($car3);
$car3 = new CarWithSunRoof($car3);
$car3 = new CarWith5Skin($car3);
$car3 = new CarWithCamera($car3);
var_dump($car3->calcualtePrice());
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
interface ImageResizeInterface {
public function setImage(string $fileName);
public function resizeTo(string $width, string $height);
public function resizeHeightByWidth(string $width);
public function save();
}
class ResizeImage implements ImageResizeInterface {
public function setImage(string $fileName) {
//code
}
public function resizeTo(string $width, string $height) {
//code
}
public function resizeHeightByWidth(string $width) {
//code
}
public function save() {
//code
}
}
interface ImageInterface {
public function setImageToResize(string $fileName);
public function resizeImageTo(string $imageWidth, string $imageHeight);
public function resizeImageHeightByWidth(string $width);
public function saveImage();
}
class ImageResize implements ImageInterface {
public function setImageToResize(string $fileName) {
//code
}
public function resizeImageTo(string $imageWidth, string $imageHeight) {
//code
}
public function resizeImageHeightByWidth(string $width) {
//code
}
public function saveImage() {
//code
}
}
class ImageAdapter implements ImageResizeInterface {
private $image;
public function __construct(ImageResize $image) {
$this->image = $image;
}
public function setImage(string $fileName) {
$this->image->setImageToResize($fileName);
}
public function resizeTo(string $width, string $height) {
$this->image->resizeImageTo($width, $height);
}
public function resizeHeightByWidth(string $width) {
$this->image->resizeImageHeightByWidth($width);
}
public function save() {
$this->image = saveImage();
}
}
$imageAdapter = new ImageAdapter(new ImageResize());
var_dump($imageAdapter);
2//
<?php
//Book
class Book {
private $author;
private $title;
function __construct(string $author, string $title) {
$this->author = $author;
$this->title = $title;
}
function getAuthor(): string {
return $this->author;
}
function getTitle(): string {
return $this->title;
}
}
//book adapter
class BookAdapter {
private $book;
function __construct(Book $book) {
$this->book = $book;
}
function getAuthorAndTitle(): string {
return $this->book->getTitle() . ' by ' . $this->book->getAuthor();
}
}
// client
$book = new Book("J.R.R. Tolkien", "Władca Pierścieni:");
$bookAdapter = new BookAdapter($book);
echo ('Author and Title: ' . $bookAdapter->getAuthorAndTitle());
Wydzielanie złożonych funkcji np do jednej klasy co ukrywa szczegóły i pozwala w łatwy sposób uruchamiać złozone funkcjonalności.
Wyobraźmy sobie, że struktura i treść kodu są jeszcze bardziej złożone i że przez to nie ma szans na ich przepisanie na własną rękę — pozostaje nam jedynie korzystanie z rozwiązania kalekiego, ale gotowego. Aby skonwertować plik o zawartości: 234-bluza_damska 55 532-kapelusz_męski 55 na postać tablicy obiektów, musimy wywołać wszystkie z prezentowanych funkcji (dla uproszczenia pomijamy przy tym wyodrębnianie z pliku liczby kończącej wiersz, reprezentującej cenę artykułu):
Jeśli będziemy w swoim projekcie stosować powyższą procedurę odczytu plików, zwiążemy ściśle kod projektu z kodem podsystemu wczytywania plików. Jeśli potem ów podsystem ulegnie zmianom albo zdecydujemy się na skorzystanie z innego podsystemu w jego miejsce, staniemy w obliczu problemu rozległych modyfikacji kodu projektu. Trzeba by więc powołać do życia bramkę pomiędzy tym podsystemem a resztą projektu.
Implementacja Oto prosta klasa udostępniająca interfejs do kodu proceduralnego, z którym borykaliśmy się w poprzednim punkcie:
<?php
class ProductFacade
{
private $products = array();
function __construct($file)
{
$this->file = $file;
$this->compile();
}
private function compile()
{
$lines = getProductFileLines($this->file);
foreach ($lines as $line)
{
$id = getIDFromLine($line);
$name = getNameFromLine($line);
$this->products[$id] = getProductObjectFromID($id, $name);
}
}
function getProducts()
{
return $this->products;
}
function getProduct($id)
{
if (isset($this->products[$id]))
{
return $this->products[$id];
}
return null;
}
}
Z punktu widzenia użytkownika tego kodu dostęp do obiektów Product generowanych na podstawie pliku rejestru produktów jest znacznie uproszczony:
$facade = new ProductFacade('test.txt');
$facade->getProduct(234);
<?php
//Programowanie obiektowe w języku PHP5 / Moduł 5. SOLID / Cz. 2
require_once 'lib/logic/Aplication.php';
require_once 'lib/logic/AplicationModel.php';
interface Command {
public function execute($parameters);
public function printHelp();
}
class Action {
public function execute() {}
}
class HellowAction extends Action {
public function __construct($app) {
$this->app = $app;
}
function execute() {
$this->app->hellow();
}
}
class LogoutAction extends Action {
public function __construct($app) {
$this->app = $app;
}
function execute() {
$this->app->logout();
}
}
//wzorzec komendy
class RequestProcess {
public function process() {
session_start();
$app = new Aplication;
$aplicationmodel = new AplicationModel;
$actions['hellow'] = new HellowAction($app);
$actions['logout'] = new LogoutAction($app);
include 'template/header.php';
include 'template/menu.php';
$actionName = $_GET['action'];
$action = $actions[$actionName];
$action->execute();
include 'template/footer.php';
}
}
?>
To samo bez zastosowania wzoraca:
<?php
require_once 'lib/logic/Aplication.php';
require_once 'lib/logic/AplicationModel.php';
class RequestProcess {
function __construct() {
}
public function process() {
session_start();
$app = new Aplication;
$aplicationmodel = new AplicationModel;
include 'template/header.php';
include 'template/menu.php';
if (isset($_GET['action']) && $_GET['action'] == 'hellow') {
$app->Hellow();
} elseif (isset($_GET['action']) && $_GET['action'] == 'logout') {
$app->logout();
}
//z
include 'template/footer.php';
}
}
?>