I v SOLIDe

Milan Herda, 03 / 2018

Príklady sú vlastné alebo prebrané z knihy

Principles of Package Design

Kto som

programátor (PHP, JavaScript), hráč spoločenských hier, fanúšik sci-fi, wannabe autor browser hier (feudarium.com)

Profesia, FatChilli, Porada, BlueOrange, NOV

profesia.sk, domelia.sk, rtvs.sk, reality.sme.sk, living.sk...

O čom budeme hovoriť

  • Čo je SOLID
  • Interface Segregation Principle
  • Výhody ISP
  • Ako rozpoznať porušenia princípu
  • Ako refaktorovať, aby sme dodržali princíp

SOLID

  • Single Responsibility Principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Skratka predstavujúca 5 základných princípov dobrého softvérového návrhu

Interface Segregation Principle

Princíp oddelenia rozhraní

Interfejsy by mali byť jemne granulované a špecifické pre klienta

Interface Segregation Principle

Jemne granulované

  • malé množstvo metód
  • najmenšie možné, ktoré dáva zmysel
  • áno, častokrát iba jedna metóda
  • a áno, dokonca aj nula

Špecifické pre klienta

  • klient by nemal závisieť na metódach, ktoré nepoužíva
  • do interfacu sa dajú iba metódy, ktoré potrebuje
  • ak viacerí klienti pracujú s triedou rôzne, tak vytvoríme viacero interfejsov
  • interface definuje skupinu metód, ktoré "chodia spolu"

Klient je kód používajúci iný kód

Výhody dodržiavania

Interface Segregation Principle

  • časti kódu sú navzájom ľahko vymeniteľné
  • klienti závisia iba na tom, čo potrebujú

Symptómy porušenia princípu

  • interface má priveľa metód
  • deravé abstrakcie (leaky abstractions)
  • žiaden kód nepoužíva všetky metódy interfacu
  • interface obsahuje metódy, ktoré "nechodia spolu"
  • triedy bez interfacu
interface FileInterface
{
    public function rename(string $name);
    public function changeOwner(string $user, string $group);
}

// požitie
public function importFile(FileInterface $file)
{
    $file->rename($this->location);
}

// ...

public function transferOwner(FileInterface $file)
{
    $file->changeOwner($this->user, $this->group);
}
class DosFile implements FileInterface
{
    public function rename(string $name)
    {
        // ...
    }

    public function changeOwner(string $user, string $group)
    {
        throw new BadMethodCallException(
            'Not implemented for DOS files'
        );
    }
}
class LocalFile implements FileInterface
{
    public function rename(string $name)
    {
        rename($this->filepath, $name);

        $this->filepath = $name;
    }

    public function changeOwner(string $user, string $group)
    {
        chown($this->filepath, $user);
        chgrp($this->filepath, $group);
    }
}

Časté riešenie je pridanie has/can metódy

interface FileInterface {
    public function rename(string $name);
    public function canChangeOwner(): bool;
    public function changeOwner(string $user, string $group);
}

class DosFile implements FileInterface
{
    public function canChangeOwner(): bool
    {
        return false;
    }

    public function rename(string $name) { /* ... */ }

    public function changeOwner(string $user, string $group) { /* ... */ }
}

// ...
public function transferOwner(FileInterface $file)
{
    if ($file->canChangeOwner()) {
        $file->changeOwner($this->user, $this->group);
    }
}

Časté riešenie je pridanie has/can metódy

  • každý potomok FileInterface musí implementovať canChangeOwner
  • DosFile teraz namiesto jednej zbytočnej metódy imlementuje dve
  • canChangeOwner a changeOwner sa musia volať spolu = veľa duplikácií v kóde (+ temporal coupling)

Takéto riešenie nie je dobré, lebo:

Refactoring time!

Upravte kód tak, aby metóda transferOwner nespadla na volaní changeOwner

Výsledok

interface FileInterface {
    public function rename(string $name);
}

interface FileWithOwnerInterface {
    public function changeOwner(string $user, string $group);
}

class DosFile implements FileInterface {
    public function rename(string $name) {
        // ...
    }
}

class LocalFile implements FileInterface, FileWithOwnerInterface {
    public function rename(string $name) {
        // ...
    }

    public function changeOwner(string $user, string $group) {
        //...
    }
}
public function importFile(FileInterface $file)
{
    $file->rename($this->location);
}

// ...

public function transferOwner(FileWithOwnerInterface $file)
{
    $file->changeOwner($this->user, $this->group);
}

Príklad 2

interface ServiceContainerInterface {
    public function get(string $name);
    public function set(string $name, callable $factory): ServiceContainerInterface;
}

// service provider
public function defineServices(ServiceContainerInterface $serviceContainer) {
    $serviceContainer->set('mailer', function () use ($serviceContainer) {
        return new Mailer(
            $serviceContainer->get('mailer.transport')
        );
    });

    $serviceContainer->set('mailer.transport', function () use ($serviceContainer) {
        return new MailerSmtpTransport();
    });
}

// controller
public function detailAction(ServiceContainerInterface $serviceContainer) {
    $repository = $serviceContainer->get('repository.user');
    $templating = $serviceContainer->get('templating');
}

Problém: klienti požadujú funkcionalitu, s ktorou nepracujú

  • Service provider používa najmä metódu set
  • V controlleri sa používa výhradne metóda get

Upravte kód tak, aby každý klient vyžadoval iba to, čo potrebuje

interface MutableServiceContainerInterface {
    public function set(string $name, callable $factory): MutableServiceContainerInterface;
}

interface ServiceLocatorInterface {
    public function get(string $name);
}

interface ServiceContainerInterface extends ServiceLocatorInterface, MutableServiceContainerInterface {}

// service provider
public function defineServices(ServiceContainerInterface $serviceContainer) {
    $serviceContainer->set('mailer', function () use ($serviceContainer) {
        return new Mailer(
            $serviceContainer->get('mailer.transport')
        );
    });

    $serviceContainer->set('mailer.transport', function () use ($serviceContainer) {
        return new MailerSmtpTransport();
    });
}

// controller
public function detailAction(ServiceLocatorInterface $serviceLocator) {
    $repository = $serviceLocator->get('repository.user');
    $templating = $serviceLocator->get('templating');
}

Riešenie

Príklad 3

Vytvárame malú knižnicu s utilitami pre jednoduché konverzie (napr. kľúče v poli na camelCase formát)

 

Knižnica sa bude do projektov inštalovať cez composer

Ferko Mrkvička z druhého konca sveta našu knižnicu používa takto:

class UserFacade {
    private ArrayConverter $arrayConverter;

    public function __construct(ArrayConverter $arrayConverter, /* ... */) {
        $this->arrayConverter = $arrayConverter;
        // ...
    }

    public function getActiveUsers(): array
    {
        $usersData = $this->userRepository->getActiveUsers();

        return $this->arrayConverter->convert($usersData);
    }
}

Kód knižnice v januári:

class ArrayConverter
{
    public function convert(array $input): array
    {
        $result = [];

        foreach ($input as $key => $data) {
            if (is_array($data)) {
                $data = $this->convert($data);
            }

            $result[$this->convertToCamelCase($key)] = $data;
        }

        return $result;
    }

    private function convertToCamelCase(string $str): string
    { 
        /* ... */
    }
}

Február: rozhodneme sa pridať triedu XmlUtil s užitočnými metódami pre XML

Napríklad formátovanie názvov XML tagov do camelCase formátu.

Nebudeme ale formátovanie do camelCase robiť odznova, keď ho už máme v triede ArrayConverter

XmlUtil si preto nainjectuje ArrayConverter cez konštruktor a len zavolá metódu convertToCamelCase

Metóda je ale privátna, tak ju najskôr urobíme public

Marec: uvedomili sme si, že je blbosť, aby XmlUtil závisel na celom ArrayConverter, keď potrebuje iba jednu metódu

Preto metódu extrahujeme do novej, samostatnej triedy.

XmlUtil a aj ArrayConverter budú závisieť na novej triede

Pôvodnú metódu z ArrayConverter tak môžeme zrušiť

Alebo nie?

ArrayConverter nemá interface, takže jeho interfacom sú všetky public metódy.

Keďže convertToCamelCase sa vo februári stalo public metódou, tak na nej medzičasom môže závisieť kód aj mimo našej knižnice

Odstrániť túto metódu znamená urobiť breaking change.

Riešenie:

Nerobiť implicitné interfejsy z public metód a už od začiatku používať vhodne vytvorené explicitné interface-y

Január

interface ArrayConverterInterface
{
    public function convert(array $input): array;
}

class ArrayConverter implements ArrayConverterInterface
{
    public function convert(array $input): array { /* ... */ }
    private function convertUndescoreToCamelCase(string $str): string { /* ... */ }
}

Február

interface ArrayConverterInterface
{
    public function convert(array $input): array;
}

interface StringConverterInterface
{
    public function convertUndescoreToCamelCase(string $str): string;
}

class ArrayConverter implements ArrayConverterInterface, StringConverterInterface
{
    public function convert(array $input): array { /* ... */ }
    public function convertUndescoreToCamelCase(string $str): string { /* ... */ }
}

class XmlUtil
{
    private StringConverterInterface $stringConverter; // v skutočnosti ArrayConverter
    
    public function convertTagNameToCamelCase(string $str): string { /* ... */ }
}

Marec

interface ArrayConverterInterface
{
    public function convert(array $input): array;
}

interface StringConverterInterface
{
    public function convertUndescoreToCamelCase(string $str): string;
}

class StringConverter implements StringConverterInterface
{
    public function convertUndescoreToCamelCase(string $str): string { /* ... */ }
}

class ArrayConverter implements ArrayConverterInterface
{
    private StringConverterInterface $stringConverter;
    
    public function convert(array $input): array { /* ... */ }
}

class XmlUtil
{
    private StringConverterInterface $stringConverter;
    
    public function convertTagNameToCamelCase(string $str): string { /* ... */ }
}

Ferko Mrkvička z druhého konca sveta našu knižnicu používa takto:

class UserFacade {
    private ArrayConverterInterface $arrayConverter;

    public function __construct(ArrayConverterInterface $arrayConverter, /* ... */) {
        $this->arrayConverter = $arrayConverter;
        // ...
    }

    public function getActiveUsers(): array
    {
        $usersData = $this->userRepository->getActiveUsers();

        return $this->arrayConverter->convert($usersData);
    }
}

Vaše verejné rozhrania sú vašimi "sľubmi" o spôsobe práce s vašimi triedami.

 

Ponúknutím menších interfejsov dávate menšie sľuby a uvoľňujete si ruky pre budúce zmeny.

 

Porušiť malý sľub je menej bolestivé ako porušiť veľký.

 

Trieda by mala mať interface, aj keď by bola jeho jedinou implementáciou.

 

Ak knižnica ponúka interface, tak vždy programujeme proti metódam interfacu a nikdy nie proti metódam triedy.

Opakovanie

Interfejsy by mali byť jemne granulované a špecifické pre klienta

Interface Segregation Principle

Jemne granulované

  • malé množstvo metód
  • najmenšie možné, ktoré dáva zmysel
  • áno, častokrát iba jedna metóda
  • a áno, dokonca aj nula

Špecifické pre klienta

  • klient by nemal závisieť na metódach, ktoré nepoužíva
  • do interfacu sa dajú iba metódy, ktoré potrebuje
  • ak viacerí klienti pracujú s triedou rôzne, tak vytvoríme viacero interfejsov
  • interface definuje skupinu metód, ktoré "chodia spolu"

Klient je kód používajúci iný kód

Výhody dodržiavania

Interface Segregation Principle

  • časti kódu sú navzájom ľahko vymeniteľné
  • klienti závisia iba na tom, čo potrebujú

Viac informácií:

Matthias Noback - Principles of Package Design

Ďakujem za pozornosť

I v SOLIDe

By Milan Herda

I v SOLIDe

  • 520