Jak uratowałem projekt

za pomocą wzorców projektowych

Dawid Mazur / @dwdmzr
"Experienced" Backend Developer @ Clearcode

Krótko o mnie

  • Programuję
  • Pracowałem jako PM / team lead
  • Lubię dzielić się moją wiedzą
  • I ogólnie się udzielam
  • Uczę też dzieci IT

Po co mi wzorce projektowe?

Znajomość OOP nie czyni Cię jeszcze dobrym architektem

Mnie też nie :F

Zalety wzorców projektowych

  • Czynią twój kod SOLIDnym
  • Są sprawdzone i wielokrotnie udowodnione
  • Tworzą wspólny słownik wśród programistów

Ok, przekonałeś mnie :D

Ale jak nauczyć się tych wzorców?

Historia projektu dla Pani Grażynki

bo przykłady z książek są nudne

Dawid Mazur / @dwdmzr
Pizza QA @ Clearcode


class Invoice {
    public function setStatus(string $status) : void {}
    public function getFullPrice() : float {}
}

class Contract {
    public function calculateInvoice() : Invoice {}
    public function printPdf() : void {}
}
                    

class Contract {
    private function isPaidMonthly() : bool {}
    private function getMonthlyInvoice() : Invoice {}
    private function getFullInvoice() : Invoice {}

    public function calculateInvoice() : Invoice {
        if ($this->isPaidMonthly()) {
            return $this->getMonthlyInvoice();
        }

        return $this->getFullInvoice();
    }

    public function printPdf() : void {}
}
					

Strategia (Strategy)

Wzorzec do enkapsulacji algorytmów

...czyli jak podmieniać algorytmy robiące tę samą rzecz, ale w różny sposób


interface InvoiceGeneratingStrategy {
    public function generateInvoice() : Invoice;
}
					

class MonthlyInvoiceGeneratingStrategy implements InvoiceGeneratingStrategy {
    public function generateInvoice(): Invoice {}
}

class QuarterlyInvoiceGeneratingStrategy implements InvoiceGeneratingStrategy {
    public function generateInvoice(): Invoice {}
}

class FullInvoiceGeneratingStrategy implements InvoiceGeneratingStrategy {
    public function generateInvoice(): Invoice {}
}
					

class Contract {
    private function isPaidMonthly() : bool {}
    private function getInvoiceGeneratingStrategy() : InvoiceGeneratingStrategy {}

    public function calculateInvoice() : Invoice {
        return $this->getInvoiceGeneratingStrategy()->generateInvoice();
    }

    public function printPdf() : void {}
}
					

class Contract {
    private function isPaidMonthly() : bool {}
    private function getInvoiceGeneratingStrategy() : InvoiceGeneratingStrategy {}

    public function calculateInvoice() : Invoice {
        return $this->getInvoiceGeneratingStrategy()->generateInvoice();
    }

    public function printPdf() : void {}
}
					

Metoda Wytwórcza (Factory Method)

Enkapsulacja tworzenia obiektu

...czyli jak dobrać strategię do typu kontraktu


abstract class Contract {
    abstract protected function getInvoiceGeneratingStrategy() : InvoiceGeneratingStrategy;

    public function calculateInvoice() : Invoice {
        return $this->getInvoiceGeneratingStrategy()->generateInvoice();
    }

    public function printPdf() : void {}
}
					

class MonthlyPaidContract extends Contract {
    protected function getInvoiceGeneratingStrategy(): InvoiceGeneratingStrategy
    {
        return new MonthlyInvoiceGeneratingStrategy();
    }
}

class FullyPaidContract extends Contract {
    protected function getInvoiceGeneratingStrategy(): InvoiceGeneratingStrategy
    {
        return new FullInvoiceGeneratingStrategy();
    }
}
					

abstract class Contract {
    abstract protected function getInvoiceGeneratingStrategy() : InvoiceGeneratingStrategy;
    abstract protected function getPdfPrintingTemplate() : ContractTemplate;

    public function calculateInvoice() : Invoice {
        return $this->getInvoiceGeneratingStrategy()->generateInvoice();
    }

    public function printPdf() : void {
        $template = $this->getPdfPrintingTemplate();
        // do some printing magic
        // (ノ°∀°)ノ⌒・*:.。. .。.:*・゜゚・*☆
    }
}
					

abstract class Contract {
    abstract protected function getInvoiceGeneratingStrategy() : InvoiceGeneratingStrategy;
    abstract protected function getPdfPrintingTemplate() : ContractTemplate;

    public function calculateInvoice() : Invoice {
        return $this->getInvoiceGeneratingStrategy()->generateInvoice();
    }

    public function printPdf() : void {
        $template = $this->getPdfPrintingTemplate();
        // do some printing magic
        // (ノ°∀°)ノ⌒・*:.。. .。.:*・゜゚・*☆
    }
}
					

class MonthlyPaidContract extends Contract {
    protected function getInvoiceGeneratingStrategy(): InvoiceGeneratingStrategy
    {
        return new MonthlyInvoiceGeneratingStrategy();
    }

    protected function getPdfPrintingTemplate(): ContractTemplate
    {
        return new MonthlyPaidContractTemplate();
    }
}
					

Fabryka abstrakcyjna
(Abstract Factory)

Enkapsulacja tworzenia rodziny obiektów

...czyli jak dobrać rodzinę powiązanych strategii do typu kontraktu


abstract class ContractComponentFactory {
    abstract public function getInvoiceGeneratingStrategy() : InvoiceGeneratingStrategy;
    abstract public function getPdfPrintingTemplate() : ContractTemplate;
}
					

class MonthlyPaidContractComponentFactory extends ContractComponentFactory {
    public function getInvoiceGeneratingStrategy(): InvoiceGeneratingStrategy
    {
        return new MonthlyInvoiceGeneratingStrategy();
    }

    public function getPdfPrintingTemplate(): ContractTemplate
    {
        return new MonthlyPaidContractTemplate();
    }
}
					

class Contract {
    public function __construct(
        ContractComponentFactory $componentFactory
    ) {
        $this->factory = $componentFactory;
    }

    public function calculateInvoice() : Invoice {
        return $this->factory->getInvoiceGeneratingStrategy()->generateInvoice();
    }

    public function printPdf() : void {
        $template = $this->factory->getPdfPrintingTemplate();
        //	(/ ̄ー ̄)/~~☆’.・.・:★’.・.・:☆
    }
}
					

$contract = new Contract(
	new MonthlyPaidContractComponentFactory()
);
					

class OrderContractComponentFactory extends ContractComponentFactory {
    public function getInvoiceGeneratingStrategy(): InvoiceGeneratingStrategy
    {
        return new FullInvoiceGeneratingStrategy();
    }

    public function getPdfPrintingTemplate(): ?ContractTemplate
    {
        return null;
    }
}
					

class Contract {
    (...)

    public function printPdf() : void {
        $template = $this->factory->getPdfPrintingTemplate();

        if ($template) {
            // do some printing magic
            // 	(ノ>ω<)ノ :。・:*:・゚’★,。・:*:・゚’☆
        }
    }
}
					

Pusty obiekt
(Null Object)

Zastępuje null, ale nadal implementuje interfejs

...czyli tak naprawdę nic nie robi, ale też nic nie popsuje :D


class OrderContractComponentFactory extends ContractComponentFactory {
    public function getInvoiceGeneratingStrategy(): InvoiceGeneratingStrategy
    {
        return new FullInvoiceGeneratingStrategy();
    }

    public function getPdfPrintingTemplate(): ContractTemplate
    {
        return new NullContractTemplate();
    }
}
					

abstract class ContractTemplate {
    abstract public function getPagesToPrint() : array;
}

class NullContractTemplate extends ContractTemplate {
    public function getPagesToPrint() : array {
        return [];
    }
}
					

class Invoice {
    (...)
    public function setStatus(string $status) : void {
        switch ($status) {
            case InvoiceStatus::PAID:
                $this->yayMoreMoneySms($this->getFullPrice());
                break;
            case InvoiceStatus::OVERDUE:
                $this->dispatchMareczek(
                    $this->getFullPrice() * 1.25,
                    $this->getFullAddress()
                );
                break;
        }

        $this->status = $status;
    }
}
					

Obserwator (Observer)

Wzorzec do powiadamiania o zmianie stanu


interface Observer {
    public function update(Subject $subject) : void;
}

interface Subject {
    public function attach(Observer $observer) : void;
    public function detach(Observer $observer) : void;
    public function notify(Observer $observer) : void;
    public function getStatus() : string;
    public function getFullPrice() : float;
    public function getFullAddress() : string;
}
					

class Invoice implements Subject {
    public function attach(Observer $observer) : void {}
    public function detach(Observer $observer) : void {}

    public function notify(Observer $observer) : void {
        $observer->update($this);
    }

    public function setStatus(string $status) : void {
        $this->status = $status;

        foreach ($this->observers as $observer) {
            $this->notify($observer);
        }
    }
}
					

class MareczekObserver implements Observer {
    private function dispatchMareczek(float $howMuch, string $address) {}

    public function update(Subject $subject): void
    {
        if ($subject->getStatus() === InvoiceStatus::OVERDUE) {
            $this->dispatchMareczek(
                $subject->getFullPrice() * 1.25,
                $subject->getFullAddress()
            );
        }
    }
}
					

class MareczekObserver implements Observer {
    private function dispatchMareczek(float $howMuch, string $currency, string $address) {}

    public function update(Subject $subject): void
    {
        if ($subject->getStatus() === InvoiceStatus::OVERDUE) {
            $this->dispatchMareczek(
                $subject->getFullPrice() * 1.25,
                $subject->getCurrency(),
                $subject->getFullAddress()
            );
        }
    }
}
					

Lista dla Mareczka

  • 1000 x 1,25 = 1250 PLN
  • 500 x 1,25 = 625 EUR
  • 2000 x 1,25 = 2500 DDDd
  • 999,99 x 1,25 = 1249,9875 z\u0142

Value Object
(Value Object)

Enkapsuluje kilka wartości jako jedność

...bo liczy się nie tylko ilość, ale też waluta $$$


class Money {
    private $amount;
    private $currency;

    private function validateCurrency(string $currency) : bool {}
    public function multiply(float $multiplier) : Money {}
    public function changeCurrency(string $currency) : Money {}

    public function __construct(int $amount, string $currency)
    {
        $this->validateCurrency($currency);

        $this->amount = $amount;
        $this->currency = $currency;
    }
}
					

class Money {
    private $amount;
    private $currency;

    private function validateCurrency(string $currency) : bool {}
    public function multiply(float $multiplier) : Money {
        return new Money(
            ceil($this->amount * $multiplier),
            $currency
        );
    }
}
					

class MareczekObserver implements Observer {
    private function dispatchMareczek(Money $howMuch, string $address) {}

    public function update(Subject $subject): void
    {
        if ($subject->getStatus() === InvoiceStatus::OVERDUE) {
            $this->dispatchMareczek(
                $subject->getFullPrice()->multiply(1.25),
                $subject->getFullAddress()
            );
        }
    }
}
					

Podsumowując

  1. Strategia - do enkapsulacji algorytmów
  2. Metoda Wytwórcza - do enkapsulacji tworzenia obiektów
  3. Fabryka abstrakcyjna - do enkapsulacji tworzenia rodziny obiektów
  4. Pusty obiekt - zastępuje null, ale nadal implementuje interfejs
  5. Obserwator - do powiadamiania o zmianie stanu
  6. Value Object - do enkapsulacji złożonych wartości

Inaczej

  1. Strategia - bo rzeczy się zmieniają i warto mieć możliwość ich łatwej zmiany
  2. Metoda Wytwórcza - bo trzeba jakoś dobrać te rzeczy do istniejącego systemu
  3. Fabryka abstrakcyjna - bo jeśli coś od siebie bezpośrednio zależy, to warto to odzwierciedlić w kodzie
  4. Pusty obiekt - bo nic tak nie psuje dnia, jak niezłapany null ;)
  5. Obserwator - żeby nie komplikować istniejących już obiektów
  6. Value Object - bo czasami proste zmienne nie wystarczą

Wady?

Pattern fever

Jaki jest morał tej historii?

  • Wymagania zmieniają się, nawet w trakcie projektu i dobrze być na to gotowym
  • np napisać kod, który będzię łatwiej się rozszerzał
  • bo można napisać kod, który robi to samo w różny sposób i niektóre sposoby są lepsze od innych
  • dlatego warto znać dobre praktyki, zwłaszcza, że są one uniwersalne
  • ...chociaż na początku warto też popełnić parę błędów ;)
  • Warto starać się pisać dobrze od razu, żeby potem tempo prac nie zwolniło do zera (legacy code)
  • Bonus: "Nigdy nie ufaj użytkownikowi, bo Ci wbije nóż w plecy"

Dziękuję za uwagę!

Pytania?
@dwdmzr | dwdmzr | dwd.mazur


Slajdy znajdziesz na
dawidmazur.eu/design-patterns


Obczajcie Coding Dojo Silesia!
codingdojosilesia


oraz Clearcode (szukamy ludzi :3)!
clearcode.pl