O v SOLIDe

06/2016 / Milan "perún" Herda / @moriquend / prezentacie.perunhq.org

Príklady prebrané z knihy Principles of Package Design

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

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

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

Open-Closed Principle

Open-Closed Principle

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

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

Výhody Open-Closed Principle

  • možnosť zmeniť správanie triedy aj bez jej úpravy
  • bezpečnejšie úpravy (bez zmeny kódu)

Symptómy porušenia princípu

  • trieda má podmienky pre určenie stratégie (typický je switch)
  • podobné podmeinky sa opakujú na viac miestach v kóde
  • trieda obsahuje natvrdo nakódované názvy triedy (v poli, podmienkach, stringoch)
  • vo vnútri triedy sa vytvárajú objekty pomocou new

Ako refaktorovať

  • zabezpečiť spĺňanie Single-Responsibility princípu
  • identifikovať všeobecné a špecifické časti úlohy
  • oddeliť špecifické a všeobecné úlohy
  • špecifické časti preniesť do samostatných tried
  • prepojiť všeobecnú a špecifickú časť pomocou "konfigurácie"

class GenericEncoder
{
    public function encodeToFormat(array $data, $format)
    {
        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, $format)
    {
        switch ($format) {
            case 'json':
                $data = $this->forceArray($data);
                $data = $this->fixKeys($data);
                break;

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

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

        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

Refactoring Time!

Zdrojové súbory tu

Prvý krok: 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 rozdpovednosti podľa Single Responsibility Principle

Prvý krok: oddelenie zodpovedností

Abstract Factory pre encodery


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

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

Prvý krok: oddelenie zodpovedností

EncoderInterface


interface EncoderInterface
{
    /**
     * @param array $data
     *
     * @return string
     */
    public function encode(array $data);
}

class JsonEncoder implements EncoderInterface {}

class XmlEncoder implements EncoderInterface {}
                    

Druhý krok: urobiť továrničku otvorenú pre rozširovanie

EncoderInterface


interface EncoderFactoryInterface
{
    /**
     * @param string $format
     *
     * @return EncoderInterface
     */
    public function createForFormat(string $format);
}

class EncoderFactory implements EncoderFactoryInterface {}

class GenericEncoder
{
    private $encoderFactory;

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

Tretí krok: urobiť továrničku otvorenú pre rozširovanie

Vytváranie encoderov vytiahnuť mimo továrničku


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

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

        return $this;
    }

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

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

        $encoder = $factory();

        return $encoder;
    }
}
                    

Štvrtý krok: upratať zodpovednosti

Prípravu dát presunúť na správne miesto


class JsonEncoder implements EncoderInterface
{
    /**
     * @param array $data
     *
     * @return string
     */
    public function encode(array $data)
    {
        $this->prepareData($data);

        return json_encode($data);
    }

    // ...
}

                    

Finále


$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');
                    

Ďakujem za pozornosť

Otázky?