Mediator design pattern

Идеята на този design pattern е да създаде нещо като „контролна кула“ или централизиран начин отделни обекти да комуникират и работят заедно.

Един прост пример би бил чат програма с много участници, всеки от които ще приемем, че са отделни обекти, наследяващи или имплементиращи общ абсклас или интерфейс.
Те поотделно не се знаят един друг. Самият чатруум ще е нашият Медиатор, който ще разпределя съобщенията от, и за всеки участник.

Друг пример би бил различните графични среди, или например една HTML форма с различни контроли, които са отделни обекти но не се знаят един друг. При клик на радио бутон например, искаме неговият лейбъл да се покаже в текстово поле. Клас Dialog например може да организира взаимодействието между елементите, действайки като медиатор.

Mediator design pattern силно напомня на Observer design pattern, в този смисъл, че с Observer можем да реализираме програмно идеята на Mediator.

Друг възможен начин да реализираме програмно Mediator е като например имаме двата „участника“, между които искаме да посредничим, и единият вика метод на другият, като му предава например съобщението си и също и себе си (this), за да знае получателят кой го „безпокои“.
Ясно е, че при този начин трябва с абсклас/интерфейс да зададем обща структура на отделните участници.

Mediator design pattern също може да използва и Adapter design pattern за да „напасне“ комуникацията между „участниците“.

Advantages

Comprehension – The mediator encapsulate the logic of mediation between the colleagues. From this reason it’s more easier to understand this logic since it is kept in only one class.

Decoupled Colleagues – The colleague classes are totally decoupled. Adding a new colleague class is very easy due to this decoupling level.
Mediator design patter намалява coupling (зависимостта) между класовете на участниците, защото няма да има нужда те да „знаят“ един за друг и да „се съобразяват“ един с друг, за да могат да комуникират.

Simplified object protocols – The colleague objects need to communicate only with the mediator objects. Practically the mediator pattern reduce the required communication channels(protocols) from many to many to one to many and many to one.

Limits Subclassing – Because the entire communication logic is encapsulated by the mediator class, when this logic need to be extended only the mediator class need to be extended.

Disadvantages

Complexity – in practice the mediators tends to become more complex and complex. A good practice is to take care to make the mediator classes responsible only for the communication part. For example when implementing different screens the the screen class should not contain code which is not a part of the screen operations. It should be put in some other classes.

The mediator pattern is used to takes the role of a hub or router and facilitates the communication between many classes. A similarity can be made with the database systems. The mediator transform a hard to implement relation of many to many, where each calls has to communicate with each other class, in 2 relations, easy to implement, of many to one and one to many, where the communication is handled by the mediator class.

An inflexible way is to define a set of interacting objects ( Colleague1, Colleague2 ,…) by referring to (and update) each other directly, which results in many interconnections between them.

The Mediator design pattern provides a solution:
Define a separate Mediator object that encapsulates how a set of objects interact.
Objects interact with a Mediator object instead of interacting with each other directly.

Демек, „Участник 1“ иска да каже нещо на „Участник 2“. Не му го казва директно, а казва на Медиаторът да го стори.

Литература:

https://www.oodesign.com/mediator-pattern.html

Visitor design pattern

Идеята на този design pattern e да може ако искаме да добавяме още функционалност към даден обект (Component), да не го дописваме него (обектът), а да имаме друг клас (Visitor), на който да подаваме този обект, и там, използвайки методите му (на предаденият Component) да създадем допълнителната логика.

Или даже по-скоро изцяло да разделим даден клас на части, например оставяйки единият да отговаря само за данните, а в други (visitors) да имаме различните логики за работа с тези данни.
In object-oriented programming and software engineering, the Visitor Design pattern is a way of separating an algorithm from an object data structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying the structures. It is one way to follow the open/closed principle.

Казах „подаваме“, но имах предвид, че по-скоро приемаме „посетителят“ и му подаваме сами себе си с $this с метод, обикновено наречен accept()

public function accept (Visitor $visitor): void
{
    $visitor->visitConcreteComponent($this);
}

Ето как Visitor обектът бива допуснат в нашият Component и той сам си взема дадената инстанция ($this) с помощта на свой метод visitConcreteComponent() и го използва за да създаде допълнителна функционалност в себе си (в класът Visitor).

Този design pattern е едно типично превъплъщение на Open Close SOLID principle и много напомня на Adapter или Decorator и други, на които подаваме даден обект и те го „доразвиват“.

…the visitor design pattern is a way of separating an algorithm from an object data on which it operates…

По този начин основният клас Component може спокойно да остане максимално прост, а за него да създадем N на брой Visitor класа, които да му добавят нужната допълнителна логика. Особено когато това се налага често.

Най-малкото, което печелим е, че няма да претоварим Component класа и да нарушим Single responsibility SOLID принципа, както и няма да му пишем наследници, което значи, че ще използваме composition over inheritance.

Ето я в основни линии идеята на Visitor design pattern – да можем да „разхвърляме“ логиката на даден клас по други класове (Visitors), а не да го претоварваме като пишем цялата логика само в него.

Отделно, представете си че имаме даден алгоритъм, който трябва да реализираме в повече от един клас. Във всеки ли да го реализираме?
Не е ли по-елегантно да го реализираме в един Visitor клас,
и с accept()-овете на тези отделните, повече от един Component-и,
да му ги подаваме, и той (Visitor-ът) да ги „посещава“ и да реализира тази логика, използвайки ги.

Един Component разбира се може да има много Visitor-и. Даже не „да има“, а „да използва“, защото това са два различни класови структури (Component и Visitor), тоест, няма пряка връзка между тях, като наследяване например.

  1. Visitor makes adding new operations easy. You can define a new operation over an object structure simply by adding a new visitor.
  2. A visitor gathers related operations and separates unrelated ones. Демек, във отделни Visitors можеш да си капсулираш сходни по смисъл функционалности.
  3. Visitor can visit objects that don’t have a common parent class, as long as it can use the Component functionality.
  4. Accumulating state. Visitors can accumulate state as they visit each element in the object structure. Without a visitor, this state would be passed as extra arguments to the operations that perform the traversal, or they might appear as global variables. Visitor-ите могат да акумулират информация от отделните обекти, които му се подават.
  5. Breaking encapsulation. Visitor’s approach assumes that the ConcreteElement interface is powerful enough to let visitors do their job. As a result, the pattern often forces you to provide public operations that access an element’s internal state, which may compromise its encapsulation.
    Като недостатък, понеже Visitor-ите трябва да могат да използват public функционалността на подаваните им обекти, може да се наложи те (използваните обекти) да имат повече public методи и пропъртита, което може да намали data hiding.

Литература:

https://refactoring.guru/design-patterns/visitor/php/example

Null object design pattern

Има мнение, че с ООП можем да заменим условията като IF с полиморфизъм. Затова и използваме обекти. Обектът си идва с цял свой набор от функционалности и ако имаме група обекти от даден по-общ и по-глобален тип, и спазващи обща структура, можем да му ги извикваме (функционалностите) без да има нужда да проверяваме предварително от кой тип е обектът.

Искаш да лае – използваш обект Куче от тип Животно и му викаш метод doSound(), който метод лае.
Искаш да мяука – използваш обект Котка от обект Животно и му викаш метод doSound(), който метод мяука.

А не да проверяваш „ако си куче, изпълни doBauBau(), ако си котка изпълни doMiauMiau()…“

Tell, don’t ask.

Но със сигурност има и случаи, в които се налага все пак да подадем обект, който обаче да не прави нищо, демек – в нашият случай – да не издава звуци. Нещо като default случай.

Например имаме електронна игра с 4 копчета за команди – Up, Down, Left и Right, с които местим нещо по екрана. И искаме за всеки случай, ако например натиснем 2 копчета и т.н… да подадем команда за… невалидна команда, демек „не прави нищо“.

Тук идва идеята на Null object design pattern.

Подаваме обект, който е със същата структура като останалите 4 но методите му не вършат нищо. По този начин няма нужда да имаме разни проверки дали командата е валидна, дали е Up, Down…. и тогава да свършим нещо.

Просто като подадем пак НЕЩО, което обаче не върши НИЩО, постигаме същият ефект и то много по-елегантно и без проверки и т.н…

Литература:

https://docs.php.earth/php/ref/oop/design-patterns/null-object

Iterator design pattern

Като кажем Итератор какво всъщност имаме предвид?
Това е обект, имплементиращ IteratorInterface, който интерфейс декларира какви методи да има даденият Итератор. Те разбира се трябва да са едни и същи откъм signature, но различни откъм имплементация.

И идеята е на този Итератор обект e да можем да му подаваме различни даннови структури, с цел – те да бъдат изциклени по унифициран начин.

И поради това, че отделните даннови структури могат да се итерират по различен начин, не е лоша идея всяка от тях да си върви и със свой Итератор. Примерно, ако имаме клас, който представлява дадена даннова структура, може с интерфейс да го задължим да има и setter, който да му задава Итератор…

Идеята на този design pattern е да имаме свой и също така стандартизиран начин да итерираме дадени data структури като масиви (едномерни или двумерни), обекти и т.н…

Ако например имаме едномерен масив, многомерен масив и масив с обекти, за да ги изциклим трябва да имаме три отделни foreach цикъла.
Ако имаме и трети вид структура – четири отделни foreach цикъла… и т.н…

Отделно, трябва да знаем и структурата на отделните data structures…

Един начин да вкараме нещата в някакъв общ стандарт за да можем да изцикляме по един и същ начин е разбира се да изберем едната като меродавна, и да препишем останалите да се равняват по нея.

Другият вариант е точно идеята на Iterator design pattern.

Да имаме обект (Iterator), на когото подаваме структурата, която ще циклим.

И понеже можем да имаме повече различни итератори – ще имаме интерфейс (IteratorInterface), и всеки Iterator клас ще го имплементира, като ще има методи като например: hasNextElement(), getNextElement() и т.н…
Полиморфизъм.

Демек, итераторът е длъжен да може и да връща един по един елементите на циклената data структура, и да следи индексът (броячът) на текущият връщан елемент.

Ето един прост пример:

Ще създаваме N на брой обекта Book и ще ги пълним например в масив.

$books = new BookList();
$books->addBook(new Book('Voina i Mir', 'Lev Tolstoy'));
$books->addBook(new Book('Pod Igoto', 'Ivan Vazov'));
$books->addBook(new Book('Mamino detentse', 'Luben Karavelov'));

Подаваме масивът с обекти $books на даден итератор.

$booksIterator = new BookListStraightIterator($books);

Циклим.

while ($booksIterator->hasNextBook()) {
$book = $booksIterator->getNextBook();
echo "getting next book with iterator : \n";
echo $book->getAuthorAndTitle();
}

hasNextBook() ще прави проверка дали има още елементи и ще връща true/false.

Aко има, getNextBook() ще взема следващият елемент и ще го връща, с помощта на някакъв вътрешен брояч, който ще се инкрементира всеки път.

Докато има още – цикли и вземай следващият, следващият… като си ги следиш дали не свършват. Отделно, получаваш и брояч, който да следи до къде си стигнал с цикленето.

„Provide a way to access the elements of an aggregate object sequentially
without exposing its underlying representation.“
[GoF]
Защо „without exposing its underlying representation“, демек без да знаем и да работим със самата структура на data структурата?
Защото както се вижда, използваме допълнителни методи, които да „свършат работата“ просто казано. И тези допълнителни методи са специфични за всяка data структура, и са „заповядани“ с интерфейс. И те просто казват: „има ли следващ? Да. Дай ми го… Има ли следващ? Да. Дай ми го…“ без да те интересува самата му структура както и какво точно се има предвид под „следващият“.

Вече като вземеш даденият елемент, тогава може би да, но не и просто да го изциклиш.

Отделно, така „скриваме“ data структурата от използващите я „клиенти“. „Клиентът“ е този който цикли, заради него е цялата дандания. За да може да цикли без да знае повече за това, което цикли от „дай ми следващият, дай ми следващият…“.

Ако искаме да ги изциклим в обратен ред например, пишем още един итератор от интерфейса IteratorInterface, и горе долу всичко ще е същото, само дето вътрешният брояч няма да започва от 0, а от броя елементи, и няма да се инкрементира, а ще се декрементира.

Също и може да си напишем итератор-филтър, който да ни връща само нужните елементи, съдейки по дадена логика…

По колкото различни начини може да искаме да изциклим дадена data структура, толкова различни методи трябва да имаме в нея (data структурата). Ясно е колко много това може да я усложни и уголеми, от там – нарушавайки Single responsibility principle.

Има и друг начин да приложим идеята на Iterator design pattern.

Пак създаваме колекция от книги $book, но вместо да я подаваме на итератор като BookListStraightIterator, в нея (в класа и) си задаваме интератор.
Тоест, класът Books ще има метод:

public function createStraightIterator(): BookListStraightIterator
{
    return new BookListStraightIterator($this);
}

И циклим така:

$straightIterator = $booksCollection->createStraightIterator();

while ($straightIterator->hasNext()) {
    echo $straightIterator->next() . "\n";
}

Тоест, разликата основно е, че обектът-колекция си върви в комплект с итератора, като разбира се може да има повече от един.

Но не нарушаваме ли Single Responsibility SOLID принципът…

Три ползи от използването на Iterator design pattern

Този design pattern е фокусиран изцяло и само върху изциклянето на дадените data structures. И по-точно – да капсулира това изцикляне чрез създаване на допълнителна функционалност и разбира се – чрез използването на тази допълнителна функционалност.

Като допълнение към горното, можем отделно да имаме и функционалност за това какво да правим с всеки текущ елемент. В смисъл, че може да имаме функционалност например за изтриване на елемент – remove()…

Също, важно е да се знае, че Iterator design pattern няма отношение с реда на елементите в колекцията, нито трябва да дава функционалност за сортиране.

  1. Скрива структурата на това, което циклим от външният свят, от клиента, когато искаме да го циклим. Демек, клиентът (циклещият) няма нужда да знае как точно да изцикли структурата, няма нужда да знае масив ли е и т.н…. и отделните и елементи… Просто казва „докато не свършат, давай ми елементите един по един…“
  2. Lazy evaluation – „не ми ги стоварвай всички наведнъж, давай ми ги един по един. Дай първият, аз ще ти кажа кога искам вторият, третият…“
    Така теоретично можем да циклим дори и безкрайни структури.
  3. Имаш по-добър контрол върху самото циклене – можеш да знаеш къде си, да паузираш, да продължиш… не че с обикновено циклене не можеш също, но тук можеш по-гъвкаво да зададеш например някаква логика за това „кой да е следващият например“ и т.н…
    При обикновеното циклене просто ти ги подава така както са сортирани, това е единствената логика, и ако искаш допълнителна логика – може разбира се но трябва да е вътре в цикъла и трябва да знаеш структурата на data структурата… връщаме се на точка 1.

Видове Iterator design pattern

  1. На база това кой контролира цикленето, биват internal и external iterator. При internal самият итератор контролира кой е следващият, при external – използващият клиент. Демек, при external iterator ние като клиент трябва да задаваме броячът и по този начин да казваме кой е следващият елемент, който ни трябва.
    Демек, по-горе разгледаните примери са internal iterator, защото вътре в итератора контролираме кой ще е следващият върнат елемент.
  2. Кой задава и определя самата логика за изцикляне – итераторът или клиентът?
    Когато итераторът само следи за това кой е текущият елемент съхранявайки само броячът, a клиентът цикли, такъв итератор се нарича cursor.
    We call this kind of iterator a cursor, since it merely points to the current position in the aggregate.
    Клиентът сам си вика методи като getNext() например когато иска да си вземе текущият за дадената итерация елемент. И getNext() ще зададе новата стойност на брояча.
  3. Колко надежден е един итератор? Какво се има предвид под „надежден“? Има се предвид ако например по време на циклене внасяме различни промени в data структурата (добавяне на елементи, триене…) дали това няма да се отрази негативно на цикленето, например в реда, в това кой е следващият…
    Итератор, който е надежден в това отношение, и то без заобиколни мерки като копиране на елементи например, се нарича „robust iterator“.
    Например като актуализираме броячът по начин такъв, че например да не върнем два пъти даден елемент, или да не върнем null, ако елементът е бил изтрит и т.н…
    On insertion or removal, the aggregate either adjusts the internal state of iterators it has produced, or it maintains information internally to ensure proper traversal.
  4. Minimal iterator – по принцип минималният итератор трябва да съдържа такива методи: first(), next(), isDone() и currentElement().
    Но може да имаме и повече методи като например previous() и т.н…

Литература:

https://en.wikipedia.org/wiki/Iterator_pattern

Template method design pattern

Много лесен и приятен design pattern. И наистина си заслужава името.

Цялата му идея се състои в това в един абсклас (но може и не абстрактен), ще го наричаме „base class“, да имаме един или повече final методи (важно е да не могат да се овъррайдват), който/които ще са т.н. „темплейти“, тоест, подобно на HTML темплейт, те ще викат други методи и ще сглобяват нещо цялостно.

В абскласа също трябва да има един или повече абстрактни методи, който да се дефинират в класовете наследници. Ще ги наричаме „hook methods“, защото на тях ще „закачаме“ подобно на куки, какво трябва да правят по-късно.

Но също и те (тези, абстрактните методи) да бъдат викани и в „темплейт метода“. В който темплейт метод може да има и друга логика разбира се, могат да се викат и други методи… Може и да не се викат всички от тези абстрактните (the hooks), може да се викат в различен ред, по колкото пъти трябва и т.н…

Идеята е те (hooks) да бъдат дефинирани в класовете наследници, за да може като инстанцираме някой от наследниците и извикаме темплейт метода от абскласа (родителя), той да може в себе си да ги извика тях (hooks) и така подобно на темплейт, да сглоби цялата логика, използвайки ги така да се каже, за да „попълни“ себе си с тях.

Отделните „парчета“ ще варират според как са дедфинирани в класа/класовете наследници, но схемата по-която се викат е една и е зададена в метода-темплейт.

Ето един крайно опростен пример:

<?php
abstract class PatapanAbstract
{
    final public function templateMeto1(): void
    {
        // some other logic or methods calls...

        $this->someSimpleMeto1();

        // some other logic or methods calls...

        $this->absMeto1();

        // some other logic or methods calls...

        $this->someSimpleMeto2();

        $this->absMeto2();

        // some other logic or methods calls...

        $this->someSimpleMeto2();
    }

    private function someSimpleMeto1(): void
    {
        echo 'Hello, this is private simple meto 1';
    }

    private function someSimpleMeto2(): void
    {
        echo 'Hello, this is private simple meto 2';
    }

    abstract protected function absMeto1();
    abstract protected function absMeto2();
}

class PatapanConcrete1 extends PatapanAbstract
{
    public function absMeto1(): void
    {
        echo 'This is meto 1';
    }

    public function absMeto2(): void
    {
        echo 'This is meto 2';
    }
}

$p1 = new PatapanConcrete1;
$p1->templateMeto1();

Ще имаме резултат:

Hello, this is private simple meto 1
Hello, this is meto 1
Hello, this is private simple meto 2
Hello, this is meto 2
Hello, this is private simple meto 2

Вижда се ясно как в „PatapanAbstract“ е създаден нещо като темплейт, а какво да има в отделните „парчета“, се дефинира в класа наследник/наследници.

The template method is a method in a superclass, usually an abstract superclass, and defines the skeleton of an operation in terms of a number of high-level steps. These steps are themselves implemented by additional helper methods in the same class as the template method.

Отделно, може и отделните hook методи да не са абстрактни, тоест да не е задължително да ги дефинираме в класовете-наследници. Може да са обикновени празни методи.

The helper methods may be either abstract methods, in which case subclasses are required to provide concrete implementations, or hook methods, which have empty bodies in the superclass. Subclasses can (but are not required to) customize the operation by overriding the hook methods. The intent of the template method is to define the overall structure of the operation, while allowing subclasses to refine, or redefine, certain steps.

И друго много съществено за този design pattern e, че логиката в темплейт метода е твърдо зададена и не може да се променя в наследниците. Затова и темплейт метода трябва да е задължително final.
Динамична е логиката по отделните „парчета“ – the hooks.

Също, понеже главният клас (base class) се явява „high-level module“ за наследяващите го „low-lewel“ модули, и не зависи от тях както и ги оставя те да задават своята логика, можем да кажем, че Template method design pattern е пример за „Inversion of control principle“.

Memento design pattern

Memento – обект, който служи само за да съхранява ЕДНА „моментна снимка“ (shapshot) на обект, който ще наричаме Originator.

И отделните Memento обекти („mementos“ на Originator обектът) могат от своя страна да бъдат организирани като масив например, където да се трупат и достъпват.

A memento is an object that stores a snapshot of the internal state of another object—the memento’s originator.

Много важно е Memento класът да не допуска друг клас освен Originator да има достъп до отделните запазени състояния (snaphots), демек Originator-ът сам да си управлява така да се каже „миналите състояния“.

Също, класът Memento трябва да има поне една двойка публични сетър/гетър методи, които Orginator да вика, за да може да вземе текущото състояние на данните на Memento, както и да може да възстанови някои от предишните такива. Това ще са така да се каже „прозорци“ на Memento класа.
Да не забравяме, че принципите data hiding и capsulation трябва да се спазват.

Originator – това ще е обектът, на който ще запазваме отделните състояния.
Only the originator can store and retrieve information from the memento, the memento is „opaque“ to other objects.

Оригинаторът ще има един метод (напр. „saveToMemento()“), в който ще се създава обект Memento, на който пък се подава дадената информация (моментна снимка), която ще искаме да съхраним.
Ще има съответно и метод (напр. „restoreFromMemento()“), който ще приема даденото „мементо“, което искаме да възстановим.

CareTaker – това е класът, който ще използваме като „шкафче“ и където ще пазим отделните mementos, например като масив. Originator ще използва този клас за да добавя/извилича отделните mementos.

CareTaker не се интересува и не работи по никакъв начин с информацията, съхранена в него, само я съхранява, добавя и изважда от „шкафчето“ за да я върне на Originator.

Прост пример:

<?php
/**
 * Това ще ни е класът, на когото ще пазим отделните "моментни снимки" 
 * с оглед на това да можем да възстановяваме
 * когато ни трябва. Тоест, заради него ще прилагаме самият Memento Design Pattern.
 * Съществените методи са "saveToMemento()" и "restoreFromMemento()"
 */
class Originator
{
    private string $state;

    // The class could also contain additional data that is not part 
    // of the Memento Design Pattern

    public function set(string $state) : void
    {
        $this->state = $state;
        printf("%s\n\n", 'Originator: Setting state to ' . $state);
    }

    public function saveToMemento() : Memento
    {
        printf("%s\n\n", 'Originator: Saving to Memento.');
        return new Memento($this->state);
    }

    public function restoreFromMemento(Memento $memento) : void
    {
        $this->state = $memento->getSnapshot();
        printf("%s\n\n", 'Originator: State after restoring from Memento: ' . $this->state);
    }
}


/**
 * Този клас ще играе ролята на една прост data container, всеки обект 
 * от който ще е просто една "моментна снимка",
 * по този начин информацията ("моментната снимка") ще е "скрита" от 
 * околният свят (data hidding).
 */
class Memento
{
    private string $snapshot;

    public function __construct(string $snapshotToSave)
    {
        $this->snapshot = $snapshotToSave;
    }

    public function getSnapshot() : string
    {
        return $this->snapshot;
    }
}


/**
 * Това ще е така да се каже "диригентът" на това да създаваме моментни снимки,
 * и да можем да ги възстановяваме.
 */
class Caretaker
{
    private array $savedStates = array();  // This will be the array with the mementos

    private Originator $originator;

    public function __construct(Originator $originator)
    {
        $this->originator = $originator;
    }

    public function takeCare() : void
    {
        $this->originator->set("State 1");

        $this->originator->set("State 2");

        // Push a memento, или още "Запази се себе си в едно Memento и го дай да го пушнем в $savedStates"
        array_push($this->savedStates, $this->originator->saveToMemento()); 

        $this->originator->set("State 3");

        $this->originator->set("State 4"); 

        // Push a memento, или още "Направи си селфи и ми го дай да го пушна в $savedStates" 
        array_push($this->savedStates, $this->originator->saveToMemento()); 

        $this->originator->set("State 5");

        $this->originator->restoreFromMemento($this->savedStates[1]);
    }
}

(new Caretaker(new Originator))->takeCare();

Command design pattern

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
GoF

Представяме си, че имаме една проста програма, например текстов редактор. И той нека има меню с няколко бутончета – New, Open, Save, Save all и т.н…, чиято работа ще е не директно да свършат това, за което за били кликнати, а само да извикат това, което трябва да стане, когато са кликнати, което си е в текст редактора (нека го наречем „бизнес моделът„).

Тоест, идеята е да организираме викането на функционалността в основният обект, не разпределянето и в подкласове.

Единият начин да организираме работата на тези бутончета (нека ги наречем „викачи“) е например да имаме абсклас/интерфейс Button и всяко бутонче да екстендва/имплементира със свой клас. И всеки от тях да вика съответната функционалност в главният обект – „бизнес моделът„.

Това не е фатално въпреки, че може да доведе до експлозия от класове когато имаме нужда от повече и повече бутончета. И стига да внимаваме да не счупим всичко щом променим родителят Button.

Отделно, да не забравяме, че дадена функционалност може да трябва да се вика не само при клик на бутонче, а и например при събития като Ctrl+N, Ctrl+S…
И за всяко от тях ли ще правим клас-викач?

Command pattern казва, всяка от тези отделни команди, като New, Open, Save… трябва да бъдат капсулирани в класове – клас NewCommand, OpenCommand, SaveCommand…
И отделните бутончета или събития (Ctrl+N…), тоест „викачи“, да викат тях, а не директно да викат функционалността на „бизнес модела“.

Тоест, принципът например за Save конкретно, да НЕ Е този

а този

Което ще рече, че този design pattern е подходящ когато трябва да можем по-унифицирано и гъвкаво да можем да викаме функционалност (методи) от даден обект, от повече от един „викач“. Демек, не отделните „викачи“ сами да си викат това, което трябва да викат, а да викат един общ викач, който да вика това, което трябва.

В крайна сметка, дали ще кликна Save дискетката, ще натисна Ctrl+S или ще дам с десен бутон Save контекстното меню, не става ли все едно и също?

Отделно, като добавим такъв „медиатор“, първо евентуални промени по начина на викане на „бизнес модела“ ще стават на едно място (капсулация).

Също и отделните „викачи“ (бутончета, клавишни комбинации, контекстни менюта…) нямат задължението и нуждата да знаят нещо за „бизнес модела“ (изолация).

Eдно възможно предимство на този design pattern е, че например, щом сме капсулирали отделните команди в общ абсклас, можем да пазим например отделните състояния, да имаме например history, което например би ни помогнало да направим нещо като undo функционалност.

Литература:

https://refactoring.guru/design-patterns/command

https://sourcemaking.com/design_patterns/command

https://sourcemaking.com/design_patterns/command/php

Chain of responsibility design pattern

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request long the chain until an object handles it.

„The pattern frees an object from knowing which other object handles a request.“ Демек, кой точно ще свърши необходимата работа е въпрос на някаква допълнителна логика.

„…and an object in the chain doesn’t have to know about the chain’s structure.“

„Instead of objects maintaining references to all candidate receivers, they keep a
single reference to their successor.“

„Since a request has no explicit receiver, there’s no
guarantee it’ll be handled – the request can fall off the end of the chain without
ever being handled. A request can also go unhandled when the chain is not
configured properly.“

Имаме група обекти, които работят в комбина, т.е. всеки прави своята си работа, но си предават действието, което трябва да се извърши един на друг,
докато не се намери кой да го изпълни. т.е. работата, която трябва да се свърши е една, но не се знае кой точно обект да я свърши.

Като в Държавната администрация – всеки те прехвърля на другото гише. Или за дадено нещо трябва да минеш през 5 гишета.

Или като една стара игра, където пускаш отгоре едно топче и то минава през различни възможни пътища, през различни разклонения, но предварително си задал според някаква конкретна логика на къде да поеме.

Затова си правим набор от класове и ръчно задаваме кой след кого е във въпросната „верига на отговорности“.

И подаваме задачата на първия. В него първо проверяваме дали той може да я свърши и ако не – предаваме задачата на следващия…. и така нататък докато попаднем на клас, който може да я свърши. Ако не попаднем – трябва да имаме по default клас, който например да покаже, че не сме обработили задачата.

Примерна програмна реализация

Първо, имаме един интерфейс ChainInterface където имаме декларирани две метода – setNextChainLink() и doTheJob(), личи си от имента им кой какво прави.

В setNextChainLink() ще се задава следващият елемент от веригата, а в doTheJob() ще проверяваш дали е за него работата и ако да – свърши я,
ако не – предай нататък.

Пак повтатям – задаването на „веригата от отговорности“ става предварително и ръчно. Примерно, правим си 5 класа, които трябва да имплементират
интерфейса ChainInterface, и всеки ще има едно private пропърти $nextChainLink където в конструктора, като създаваме този обект, ще му подаваме обект, създаден преди.

Този design pattern е много удобен например за избягване от заплетена логика, вложени IF-ове и т.н… Просто трябва логиката да се организира поточно.

Пример:

interface ChainInterface
{
    public function setNextChainLink(ChainInterface $oChainLink);
    public function doTheJob($jobToBeDone);
}

class Responsible1 implements ChainInterface
{
    private ChainInterface $nextChainLink;

    public function setNextChainLink(ChainInterface $oChainLink = NULL)
    {
        if (empty($oChainLink)) {
            // do smth...
        } else {
            $this->nextChainLink = $oChainLink;
        }
    }

    public function doTheJob($jobToBeDone)
    {
        if ($jobToBeDone['job'] == 'add') {
            return $jobToBeDone['tova'] + $jobToBeDone['onova'];
        }

        if (empty($this->nextChainLink)) {
            return 'Stignahme do kraq i ne namerih ChainInterface, kojto da obraboti tazi zada4a.';     
        }

        return $this->nextChainLink->doTheJob($jobToBeDone); 
    }
}

class Responsible2 implements ChainInterface
{
    private ChainInterface $nextChainLink;

    public function setNextChainLink(ChainInterface $oChainLink = NULL)
    {   
        if (empty($oChainLink)) {
            // do smth...
        } else {
            $this->nextChainLink = $oChainLink;
        }
    }

    public function doTheJob($jobToBeDone)
    {
        if ($jobToBeDone['job'] == 'sub') {
             return $jobToBeDone['tova'] - $jobToBeDone['onova'];
        }

        if (empty($this->nextChainLink)) {
             return 'Stignahme do kraq i ne namerihme ChainInterface, kojto da obraboti tazi zada4a.';
        }

        return $this->nextChainLink->doTheJob($jobToBeDone);
    }
}

class Responsible3 implements ChainInterface
{
    private ChainInterface $nextChainLink;

    public function setNextChainLink(ChainInterface $oChainLink = NULL)
    {
        if (empty($oChainLink)) {
            // do smth...
        } else {
            $this->nextChainLink = $oChainLink;
        }
    }

    public function doTheJob($jobToBeDone)
    {
        if ($jobToBeDone['job'] == 'mul') {
            return $jobToBeDone['tova'] * $jobToBeDone['onova'];
        }

        if (empty($this->nextChainLink)) {
             return 'Stignahme do kraq i ne namerihme ChainInterface, kojto da obraboti tazi zada4a.';
        }

        return $this->nextChainLink->doTheJob($jobToBeDone); 
    }
}

$resp1 = new Responsible1();
$resp2 = new Responsible2();
$resp3 = new Responsible3();

$resp1->setNextChainLink($resp2);
$resp2->setNextChainLink($resp3);

$res = $resp1->doTheJob(array('tova' => 4, 'onova' => 3, 'job' => 'add'));
var_dump($res);

Литература:

https://refactoring.guru/design-patterns/chain-of-responsibility

Strategy design pattern

Идеята на този шаблон е да осигури гъвкав начин за задаване и използване на различни алгоритми (ще ги наречем „стратегии“) с огледа на лесната им взаимозаменяемост.

Този шаблон пресъздава SOLID принципът Open-Close Principle.

Характерно за този шаблон е, че се използва принципът Composition, а не Inheritance, тоест отделните „стратегии“ не трябва да бъдат наследявани, а да бъдат добавяни като пропърти на даден централен клас, който ще наричаме Контекстен.

По-просто казано, дефинираме си различни класове, имплементиращи общ интерфейс с оглед на това да бъдат взаимозаменяеми, като в даденият случай, всеки клас е една отделна стратегия.

Пример:

interface IStrategy
{
    public function showTitle(Book $oBook) : string;
}

class StrategyCaps implements IStrategy
{
    public function showTitle(Book $oBook) : string
    {
        $title = $oBook->getTitle();
        return strtoupper($title);
    }
}

class StrategyExclaim implements IStrategy
{
    public function showTitle(Book $oBook) : string
    {
        $title = $oBook->getTitle();
        return str_replace(' ', '!', $title);
    }
}

class StrategyStars implements IStrategy
{
    public function showTitle(Book $oBook) : string
    {
        $title = $oBook->getTitle();
        return str_replace(' ', '*', $title);
    }
}


class StrategyContext
{
    private IStrategy $strategy;  // Instance of one of the above classes
 
    // 1. Set desired strategy in a private property 
    public function __construct(IStrategy $oStrategy) 
    {
        $this->strategy = $oStrategy; 
    } 

    // 2. Perform selected strategy method 
    public function showBookTitle(Book $book): string
    {
        return $this->strategy->showTitle($book); 
    }
}


class Book
{
    private string $author, $title;

    public function __construct(string $title_in, string $author_in) 
    {
        $this->author = $author_in;
        $this->title  = $title_in;
    }

    public function getAuthor(): string 
    {
        return $this->author;
    }

    public function getTitle(): string 
    {
        return $this->title;
    }

    public function getAuthorAndTitle(): string 
    {
        return $this->getTitle() . ' by ' . $this->getAuthor();
    }
}

// Предварително създаваме набор от различни стратегии
$strategyContextC = new StrategyContext(new StrategyCaps());
$strategyContextE = new StrategyContext(new StrategyExclaim());
$strategyContextS = new StrategyContext(new StrategyStars());

// Върху този обект ще прилагаме различните стратегии
$book = new Book('Nemili Nedragi', 'Ivan Vazov');

echo "\n\ntest 1 - show name context C \n";
echo $strategyContextC->showBookTitle($book);
echo "\n\n";

echo "\n\ntest 2 - show name context E \n";
echo $strategyContextE->showBookTitle($book);
echo "\n\n";

echo "\n\ntest 3 - show name context S \n";
echo $strategyContextS->showBookTitle($book);
echo "\n\n";

// Може и без контекст
$strategyContextC1 = new StrategyCaps();
echo $strategyContextC1->showTitle($book);

Виждат се отделните „стратегии“, вижда се обектът, върху който ще ги прилагаме.

Вижда се че може директно да прилагаме желаната стратегия, като създадем обект от нейният клас и и подадем обектът, върху ще се прилага стратегията.
Може и през т.н. „контекст“, ако например искаме да капсулираме допълнителна логика, която да обработи подадения обект.

Observer design pattern

Най-просто, идеята е даден обект/обекти, които ще наречем Subjects, когато им бъде извикан някой от методите, този метод/методи допълнително да извикат някаква функционалност (методи) на други обекти, наречени Observers.

Примерно имаш обект Продукт, който има разни пропъртита (цена, наличност, описание…) и примерно трябва да се промени цената (да поскъпне). И примерно трябва освен да UPDATE-неш таблицата в базата, трябва и да изпратиш емайл на всички клиенти за да има кажеш, че е поскъпнал.

Има 2 вида обекти (от два класа) – Subject и Observer.

На всеки Subject му се задават 1 или N на брой Observer (като масив) с метода attach(), а могат и да се махат с detach().

Kaк се attach(IObserver $observer)-ват? Подаваш го на Subject->attach(), изцикляш масива с Observers-те му добавени до тук, и проверяваш дали вече го имаш този обект добавен. И естествено, ако не – push-ваш го към края на масива.

Как се detach(IObserver $observer)-ват? По подобен начин, пак изцикляш масива с Observers и проверяваш дали го имаш този обект, добавен към Observers на дадения Subject.
И ако да – unset($this->observers[$oKey])-ваш го.

Важен е Subject, него ще следим дали се променя. Въпросното „следене“ е просто ако примерно със setter се зададе стойност на някое property, и ако искаш Observer-те му (на даденият Subject) да „разберат“, ги изцикляш с foreach() и за всеки Observer му предаваш (използвайки update(ISubject $subject)) текущата инстанция на Subject, с $this (себе си).

Както казахме, това „предаване“ на Subject на Observer става с метод на Observer-a update(ISubject $subject), който приема $this (Subject-a) и вече има копие на този Subject.
Демек, когато нещо се случи в Subject, при условие че имаш масив с Observers, които така да се каже, да го „следят“, изцикляш този масив с Observers, и при всяка итерация извикваш методът update($this) на конкретният Observer.

В Subject-ът трябва да имаме задължително 3 метода, с които ще му задаваме (на Subject-ът) кои ще са му Observer-итер, които ще пълним в private масив към Subject-а.

attach(IObserver ) – подаваш му обект-наблюдател, който бива добавен към private масива с останалите наблюдатели за този субект.

detach(IObserver ) – пак подаваме обект-наблюдател но с цел да го махнем от този private масив.

notify() – извиква се ръчно когато настъпи „наблюдавано събитие“, изциклят се всичките наблюдатели, атачнати към даденият Subject, и на всеки Observer му се вика методът update(ISubject).

Oчевидно е, че всеки Observer трябва да има задължително метод update(), който ще съдържа какво трябва да се случи, когато го извикаме, тоест, когато настъпи „събитие“.
Който пък от своя страна ще бъде извикан в методът notify() на Subject-ът, когато настъпи т.н. „събитие“ и изциклим масивът с Observer-ите.

Най-просто казано, задачата на notify() на даденият Subject e да каже на атачнатите Oserver-и „действайте“.

Пример:

/**
 * Понеже може да имаме много обзървъри, нека ги задължим
 * да имат метод update() за да го викаме унифицирано
 */
interface IObserver
{
    public function update(ISubject $oSubject) : void;
}

/**
 * Понеже можем да имаме много субекти (наблюдавани обекти),
 * нека ги задължим да имат методи за:
 *    - добавяне на обзървър към даденият обект;
 *    - махане на обзървър от даденият обект;
 *    - оповестяване на обзървърите, закачени към субекта. Този метод
 *      трябва изрично да се извика при настъпване на дадено събитие.
 */
interface ISubject
{
    public function attach(IObserver $oObserver) : void;
    public function detach(IObserver $oObserver) : void;
    public function notify();
}

class FirstExampleObserver implements IObserver
{
    /**
     * Когато в някой Субект настъпи някаква промяна, той (Субектът) 
     * ще вика този метод на всичките си обзървъри.
     * Примерно като ги изцикля ако са в масив.
     * И така образно казано ще им казва, че с нещо се е променил, 
     * да знаят. И трябра да им предава на всеки по едно $this
     *
     * В нашия случай този метод не прави нищо кой знае какво, просто 
     * показва промените.
     */
    public function update(ISubject $oSubject): void
    {
         echo "\n\n";
         echo "IN PATTERN OBSERVER - NEW GOSSIP HAS ARRIVED*\n";
         echo $oSubject->getFavorites() . "\n";
         echo "\n\n";
    }
}

class FirstExampleSubject implements ISubject
{
    private string $favorites = '';
    private array $observers = array();  // Array with observers for the given "FirstExampleSubject" object

    /**  
     * The "subject" must maintain a list (array) of "observers" (or dependents) objects
     * and notifies them when a changes in it (the subject) happens,
     * usually by calling one of their methods (in this case - update()).
     *
     * Т.е. с attach() задаваш обзървърите на даден субект.  
     */
    public function attach(IObserver $oObserver): void
    {
        $this->observers[] = $oObserver; 
    } 

    /**
     * Това е обратното на attach(), когато искаш да махнеш даден обзървър от субекта.
     * Предаваш като параметър обекта-обзървър, изцикляш на субекта всички обзъръври,
     * при всяка итерация проверяваш всеки дали не е този, който си предал (за махане), и го unset()-ваш ако да.
     */
    public function detach(IObserver $oObserver): void
    {
        foreach ($this->observers as $oKey => $oVal) {
            if ($oVal == $oObserver) {
                unset($this->observers[$oKey]);
            }
        }
    }

    /** 
     * Този метод се вика, когато искаш да "информираш" всички обзървъри на даден субект.  
     * Той ги изцикля и за всеки вика update() като предава по едно $this  
     * Всеки обзървър тряба да има метод update(), което се гарантира с интерфейс.  
     */
    public function notify(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    } 

    /**  
     * Единият начин за "нотифайване" на обзървъра е  
     * като директо се извика notify() в даденият метод  
     */ 
    public function updateFavorites(string $fav = ''): void
    {
        $this->favorites = $fav;
        $this->notify();
    }

    /**  
     * Може и така, с "чейване на влакче".  
     */
    public function setFavorites(string $fav): self
    {
        $this->favorites = $fav;

        return $this;
    } 

    /**  
     * Може и да не нотифайваме ;-P
     */ 
    public function getFavorites(): string
    {
        return $this->favorites; 
    }
}

$kljukar1 = new FirstExampleSubject();

$patternGossipFan = new FirstExampleObserver();

$kljukar1->attach($patternGossipFan);

$kljukar1->updateFavorites('Pena se razvede');
$kljukar1->updateFavorites('Ivan izneveri na jena si');
$kljukar1->updateFavorites('Pe6o si izgoni svekyrvata obratno v Kaspichan');
$kljukar1->updateFavorites('Kak Minka ne moje da gotvi');

$kljukar1->setFavorites('Gosho krade nafta')->notify();  // Може и така, с "влакче".

$kljukar1->detach($patternGossipFan);

$kljukar1->updateFavorites('Tova veche ne go nabludavame, zashtoto detach-nahme observera');

Low coupling between Subject and Observer – Субектът има списък със своите Обзървъри, които са му атачнати, но само толкова. Те са съвсем отделни обекти, от един общ интерфейс, а не част от него. Субектът задължително трябва да може да работи безпроблемно дори и без никакви Обзървъри. На Обзървърите трябва да се гледа чисто и просто като някакви придатъци, без който може и в никакъв случай да няма никава зависимост, поради която промени в едното да счупят другото.
Типичен пример за Composition over inheritance.

Също, трябва да се внимава кога Субектът вика своите Обзървъри. Трябва Субектът да е в т.н. „consistent state“, тоест когато информацията му е вече променена, например цената на продукта, заявките към БД са къмитнати… и т.н…, а не преди това. Затова може би най-добре е това викане на Обзървърите да става например в деструктора.

Също, интересно е да се спомене, че даден Обзървър може да бъде нотифайван от не само един Субект, тоест може да е Обзървър на повече от един Субект.

Тhe push and pull models

Отделно, трябва да споменем, че има два модела, или по-скоро две разновидности на Observer design pattern – push model и pull model.

При push model, Субектът сам изпраща цялата информация към Обзървърите си независимо дали те я искат или не.

При pull model Субектът изпраща само сигнал или флаг към Обзървърите си, и те решават дали сами да си изтеглят информацията. Но на каква база да решат? Демек, ти им казваш: промених се, ъпдейтнете се и вие, но те ще кажат: ОК но кажи какво точно си се променил за да можем да решим дали тряба да се променяме… Демек, не ни казвай само да се променим и ние, кажи ни защо, как, какво при теб се промени…

Implementations of the Observer pattern often have the subject broadcast additional information about the change. The subject passes this information as an argument to Update. The amount of information may vary widely.

At one extreme, which we call the push model, the subject sends observers detailed information about the change, whether they want it or not. At the other extreme is the pull model; the subject sends nothing but the most minimal notification, and observers ask for details explicitly thereafter.

Вижда се, че при pull model Обзървърите трябва да знаят повече за Субекта си отколкото при push model.