O v SOLIDe

Milan Herda, 09 / 2017

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
  • Open-Closed Principle
  • Výhody Open-Closed Principle
  • 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

Open-Closed Principle

Open-Closed Principle

Trieda by mala byť otvorená pre rozširovanie a zároveň uzatvorená pre zmeny

Open-Closed Principle

Mali by ste byť schopní rozšíriť správanie triedy aj bez potreby modifikácie jej kódu

Výhody dodržiavania

Open-Closed Principle

  • možnosť zmeniť správanie triedy aj bez jej úpravy
  • bezpečnejšie úpravy (lebo nemeníme kód)

Symptómy porušenia princípu

  • trieda má podmienky pre určenie stratégie (typicky napr. switch)
  • podobné podmienky sa opakujú na viacerých miestach v kóde
  • trieda obsahuje natvrdo nakódované názvy tried (v poli, podmienkach, stringoch)
  • vo vnútri triedy sa vytvárajú objekty pomocou new
  • trieda obsahuje hardkódovaný názov súboru, emailovú adresu alebo inú skalárnu hodnotu

Ako refaktorovať

  • zabezpečiť spĺňanie Single Responsibility Principle
  • identifikovať všeobecné a špecifické časti úlohy
  • oddeliť špecifické a všeobecné úlohy
  • špecifické časti preniesť von (do samostatných tried)
  • prepojiť všeobecnú a špecifickú časť pomocou "konfigurácie"
class MessageLogger
{
    public static function logMessage(string $message)
    {
        file_put_contents(
            '/var/log/my-app/action.log',
            $message . "\n",
            FILE_APPEND
        );
    }
}

Zmeňte súbor, do ktorého sa loguje.

  • Nie je to možné bez modifikácie kódu triedy
  • Názov súboru je špecifická časť, ktorú treba vyňať

Refactoring time!

Skúsime triedu zrefaktorovať

Stiahnite si zdrojáky

Krok 1: Zabezpečiť spĺňanie Single-Responsibility princípu

Trieda už spĺňa SRP

Krok 2: Identifikovať všeobecné a špecifické časti úlohy

Názov súboru je špecifická časť

Krok 3: Oddeliť špecifické a všeobecné úlohy

Metóda logMessage už nebude statická.

Názov súboru bude závislosť triedy vkladaná cez konštruktor.

Krok 4: Špecifické časti preniesť von (do samostatných tried)

Názov súboru príde do triedy zvonka v čase jej konštrukcie. Nie je potrebné vytvárať novú triedu.

Krok 5: Prepojiť všeobecnú a špecifickú časť pomocou "konfigurácie"

Názov môže byť definovaný v nejakom konfiguračnom súbore alebo databáze.

Výsledok

class MessageLogger
{
    private string $logFileName;

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

    public function logMessage(string $message)
    {
        file_put_contents(
            $this->logFileName,
            $message . "\n",
            FILE_APPEND
        );
    }
}

Príklad 2

class GenericEncoder
{
    public function encodeToFormat(array $data, string $format): string
    {
        if ($format == 'json') {
            $encoder = new JsonEncoder();
        ) else if ($format == 'xml') {
            $encoder = new XmlEncoder();
        } else {
            throw new InvalidArgumentException(sprintf('Unknown format %s', $format));
        }

        $data = $this->prepareData($data, $format);

        return $encoder->encode($data);
    }

    private function prepareData(array $data, string $format): string
    {
        switch ($format) {
            case 'json':
                $data = $this->forceArray($data);
                $data = $this->fixKeys($data);
                break;

            case 'xml':
                $data = $this->fixAttributes($data);
                break;
        }

        return $data;
    }
}

Pridajte do triedy podporu pre kódovanie do YAML

  • nedá sa to urobiť bez modifikácie kódu
  • modifikáciu treba urobiť na viacerých miestach

Krok 1: Oddelenie zodpovedností

GenericEncoder robí priveľa vecí

  • rozhoduje sa o type encoderu a vytvára ho
  • pripravuje dáta na kódovanie pre konkrétny encoder
  • pomocou encoderu kóduje dáta

Rozdelíme zodpovednosti podľa Single Responsibility Principle

Krok 1: Oddelenie zodpovedností

Vytvoríme továrničku pre encodery

class EncoderFactory
{
    public function createForFormat(string $format): EncoderInterface
    {
        if ($format == 'json') {
            return new JsonEncoder();
        } else if ($format == 'xml') {
            return new XmlEncoder();
        }

        throw new InvalidArgumentException(sprintf('Unknown format %s', $format));
    }
}

Encodery by mali mať jeden interface

interface EncoderInterface
{
    public function encode(array $data): string;
}

class JsonEncoder implements EncoderInterface {}

class XmlEncoder implements EncoderInterface {}

Krok 2: Urobiť GenericEncoder otvorený pre rozširovanie

GenericEncoder bude závisieť na EncoderFactory

class GenericEncoder
{
    private $encoderFactory;

    public function __construct(EncoderFactory $encoderFactory)
    {
        $this->encoderFactory = $encoderFactory;
    }
}

Nepresunuli sme len problém na iné miesto?

Presunuli.

Krok 2: Urobiť GenericEncoder otvorený pre rozširovanie

GenericEncoder bude závisieť na EncoderFactoryInterface

interface EncoderFactoryInterface
{
    public function createForFormat(string $format): EncoderInterface
}

class EncoderFactory implements EncoderFactoryInterface {}

class GenericEncoder
{
    private $encoderFactory;

    public function __construct(EncoderFactoryInterface $encoderFactory)
    {
        $this->encoderFactory = $encoderFactory;
    }
}

GenericEncoder teraz spĺňa OCP.

Čo ale EncoderFactory?

Krok 3: Urobiť továrničku otvorenú pre rozširovanie

class EncoderFactory implements EncoderFactoryInterface
{
    private array $factories = [];

    public function registerFactory(string $format, callable $factory)
    {
        $this->factories[$format] = $factory;

        return $this;
    }

    public function createForFormat(string $format): EncoderInterface
    {
        if (!array_key_exists($format, $this->factories)) {
            throw new InvalidArgumentException(sprintf('Unknown format %s', $format));
        }

        $factory = $this->factories[$format];

        $encoder = $factory();

        return $encoder;
    }
}

Vytváranie jednotlivých encoderov vytiahneme mimo továrničky do svojich vlastných factory

Krok 3: Upratať zodpovednosti

class JsonEncoder implements EncoderInterface
{
    public function encode(array $data): string
    {
        $this->prepareData($data);

        return json_encode($data);
    }

    // ...
}

Prípravu dát presunieme na správne miesto

Finálne riešenie

$data = [/* ... */];

$encoderFactory = new EncoderFactory();

$encoderFactory->addEncoderFactory('json', function () {
    return new JsonEncoder();
});

$encoderFactory->addEncoderFactory('json', function () {
    return new XmlEncoder();
});

$genericEncoder = new GenericEncoder($encoderFactory);
$genericEncoder->encodeToFormat($data, 'json');

Príklad 3

class BuildingFactory
{
    /** @var array<int,string> */
    private $classes = [
        BuildingType::LUMBERJACK   => 'Lumberjack',
        BuildingType::QUARRY       => 'Quarry',
        BuildingType::IRON_MINE    => 'IronMine',
        BuildingType::FARMS        => 'Farms',
        BuildingType::STOREHOUSE   => 'Storehouse',
    ];

    public function create(int $type, int $level): BuildingInterface
    {
        if (!isset($this->classes[$type])) {
            throw new \Exception('Takýto typ budovy neexistuje');
        }

        $building = new $this->classes[$type]($level);

        return $building;
    }
}

Upravte kód tak, aby sklad (Storehouse) dostal automaticky do konštruktoru aj druhý parameter, ktorým je jeho úvodná kapacita.

Hodnota úvodnej kapacity je 650.

Dodržte pritom Open-Closed Principle.

Riešenie: Storehouse

class Storehouse extends ABuilding
{
    private int $initialCapacity;

    public function __construct(int $level, int $initialCapacity)
    {
        parent::__construct($level);

        $this->initialCapacity = $initialCapacity;
    }

    // ...
}

Riešenie: Malé továrničky pre budovy

class FarmsBuildingFactory implements BuildingFactoryInterface
{
    public function createBuilding(int $level): BuildingInterface
    {
        return new Farms($level);
    }
}
class StorehouseBuildingFactory implements BuildingFactoryInterface
{
    private int $initialCapacity;

    public function __construct(int $initialCapacity)
    {
        $this->initialCapacity = $initialCapacity;
    }

    public function createBuilding(int $level): BuildingInterface
    {
        return new Storehouse($level, $this->initialCapacity);
    }
}
interface BuildingFactoryInterface
{
    public function createBuilding(int $level): BuildingInterface;
}

Riešenie: Všeobecná továrnička

class BuildingFactory
{
    private array $factories = [];

    public function registerFactory(int $type, BuildingFactoryInterface $factory)
    {
        $this->factories[$type] = $factory;

        return $this;
    }

    public function create(int $type, int $level): BuildingInterface
    {
        if (!isset($this->factories[$type])) {
            throw new \Exception('Takýto typ budovy neexistuje');
        }

        $factory = $this->factories[$type];

        $building = $factory->create($level);

        return $building;
    }
}

Riešenie: Použitie

$genericFactory = new BuildingFactory();

$genericFactory->registerFactory(
    BuildingType::FARMS, new FarmsBuildingFactory()
);
$genericFactory->registerFactory(
    BuildingType::IRON_MINE, new IronMineBuildingFactory()
);
$genericFactory->registerFactory(
    BuildingType::LUMBERJACK, new LumberjackBuildingFactory()
);
$genericFactory->registerFactory(
    BuildingType::QUARRY, new QuarryBuildingFactory()
);
$genericFactory->registerFactory(
    BuildingType::STOREHOUSE, new StorehouseBuildingFactory(650)
);

$storehouse = $genericFactory->create(BuildingType::STOREHOUSE, 1);

echo $storehouse->getCapacity();

Opakovanie

Trieda by mala byť otvorená pre rozširovanie a zároveň uzatvorená pre zmeny

Mali by sme byť schopní rozšíriť správanie triedy aj bez potreby modifikácie jej kódu

Výhody dodržiavania

Open-Closed Principle

  • možnosť zmeniť správanie triedy aj bez jej úpravy
  • bezpečnejšie úpravy (lebo nemeníme kód)

Symptómy porušenia princípu

  • trieda má podmienky pre určenie stratégie (typicky napr. switch)
  • podobné podmienky sa opakujú na viacerých miestach v kóde
  • trieda obsahuje natvrdo nakódované názvy tried (v poli, podmienkach, stringoch)
  • vo vnútri triedy sa vytvárajú objekty pomocou new
  • trieda obsahuje hardkódovaný názov súboru, emailovú adresu alebo inú skalárnu hodnotu

Viac informácií:

Matthias Noback - Principles of Package Design

Ďakujem za pozornosť

O v SOLIDe

By Milan Herda

O v SOLIDe

  • 643