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.