L 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
  • Liskov Substitution Principle
  • Výhody Liskov Substitution Principle
  • Ako rozpoznať porušenia princípu

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

Liskov Substitution Principle

Liskovovej princíp zameniteľnosti

Liskov Substitution Principle

Odvodená trieda musí byť náhradou svojej základnej triedy

Byť dobrou náhradou svojej základnej triedy:

  • poskytovať implementáciu pre všetky metódy
  • mať rovnaké typy návratových hodnôt
  • nesprísňovať požiadavky na argumenty
  • nesprísňovať kontrakt
  • neobchádzať kontrakt v kóde

Výhody Liskov Substitution Principle

  • časti kódu sú navzájom bezpečne vymeniteľné
  • eliminácia chýb spôsobených nedodržaním kontraktu

Symptómy porušenia princípu

  • nie sú poriadne implementované všetky metódy
  • potomok má inú návratovú hodnotu ako rodič
  • prísnejšie požiadavky na argumenty
  • prísnejší kontrakt
  • obchádzanie kontraktu v kódu

Príklad 1


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

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

    public function changeOwner($user, $group)
    {
        throw new BadMethodCallException(
            'Not implemented for Dropbox files'
        );
    }
}
                    

Refactoring Time!

Upravte kód tak, aby DropboxFile nemal prázdnu implementáciu metódy

Zdrojové súbory tu

Nesprávny rodič

Prečo má FileInterface metódu changeOwner, keď ju nevedia implementovať všetci potomkovia?

  • rozdelíme interface
  • každý *File bude dediť od správneho interfacu

Výsledok


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

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

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

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

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

Príklad 2


interface RouterInterface {
    /**
     * @return Route[]
     */
    public function getRoutes();
}

class SimpleRouter implements RouterInterface {
    public function getRoutes() {
        $routes = [];
        // ...
        return $routes;
    }
}

class AdvancedRouter implements RouterInterface {
    public function getRoutes() {
        $routeCollection = new RouteCollection();
        // ...
        return $routeCollection;
    }
}

class RouteCollection implements Iterator { /* ... */}
                    

Rozdielny typ návratovej hodnoty

Keď by sme si dali návratovú hodnotu spočítať funkciou count, tak nám jedna implementácia zhavaruje.

  • rodič musí mať presnejšiu špecifikáciu návratovej hodnoty
  • upravíme potomkov, aby spĺňali špecifikáciu

Výsledok


interface RouterInterface {
    /**
     * @return RouteCollectionInterface
     */
    public function getRoutes();
}

interface RouteCollectionInterface extends Iterator, Countable {
}
                    

Príklad 3


interface MassMailerInterface {
    public function sendMail(
        TransportInterface $transport,
        MessageInterface $message,
        RecipientsInterface $recipients
    );
}

class SmtpMassMailer implements MassMailerInterface {
    public function sendMail(
        TransportInterface $transport,
        MessageInterface $message,
        RecipientsInterface $recipients
    ) {
        if (!($transport instanceof SmtpTransport)) {
            throw new InvalidArgumentException(
                'SmtpMassMailer only works with SMTP'
            );
        }
        // ...
    }
}
                    

Prísnejšie požiadavky na argumenty, ako má rodič

Hoci trieda o sebe tvrdí, že akceptuje inštancie TransportInterface, tak v skutočnosti akceptuje iba SmtpTransport a inak zhavaruje.

  • zistíme, čím je SmtpTransport výnimočný a rozdielny oproti TransportInterface
  • upravíme/vytvoríme patričné interfacy a typehinty

Pracovný scenár:

  • Povedzme, že nie všetky implementácia TransportInterface vedia rozposielať emaily hromadne
  • SmtpTransport to vie
  • MassMailerInterface chce rozposielať emaily hromadne

Výsledok (pre pracovný scenár)


class SmtpTransport implements TransportWithMassMailSupportInterface {
    // ...
}

interface MassMailerInterface {
    public function sendMail(
        TransportWithMassMailSupportInterface $transport,
        MessageInterface $message,
        RecipientsInterface $recipients
    );
}

class SmtpMassMailer implements MassMailerInterface {
    public function sendMail(
        TransportWithMassMailSupportInterface $transport,
        MessageInterface $message,
        RecipientsInterface $recipients
    ) {
        // ...
    }
}
                    

Príklad 4


interface HttpKernelInterface {
    public function handle(Request $request);
}

class HttpKernel implements HttpKernelInterface {
    public function handle(Request $request) {
        // ...
    }

    public function getEnvironment() {
        // ...
    }
}

class CachedHttpKernel implements HttpKernelInterface {
    public function __construct(HttpKernelInterface $kernel) {
        if ($kernel->getEnvironment() === 'dev') {
            // ...
        }
    }

    public function handle(Request $request) {
        //...
    }
}
                    

Metóda getEnvironment nepatrí HttpKernelInterface

Keď do konštruktoru vložíme inšťanciu HttpKernelInterface, ktorá nemá metódu getEnvironment, tak kód zhavaruje

  • programujeme len voči deklarovanému rozhraniu
  • nedostatočné rozhranie vymeníme

Výsledok


interface HttpKernelWithEnvironmentInterface extends HttpKernelInterface {
    public function getEnvironment();
}

class CachedHttpKernel implements HttpKernelInterface
{
    public function __construct(
        HttpKernelWithEnvironmentInterface $kernel
    ) {
        // ...
    }
}
                    

Ďakujem za pozornosť

Otázky?