L v SOLIDe

Milan Herda, 03 / 2018

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
  • Liskov Substitution Principle
  • Výhody LSP
  • 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

Liskov Substitution Principle

Liskovovej princíp zameniteľnosti

Liskov Substitution Principle

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

Liskov Substitution Principle

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 dodržiavania

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óde
class AdminUser implements UserInterface {
	private string $name;
    private array $allowedSections = [];
    
    public function setName(string $name): self
    {
        $this->name = $name;
        
        return $this;
    }
    
    public function setCapitalTown(
        TownInterface $town
    ): self {
        // ???
    }
    
    public function grantAccess(int $sectionId): bool
    {
        $this->allowedSections[] = $sectionId;
        
        return true;
    }
}
class Player implements UserInterface {
    private string $name;
    private TownInterface $capitalTown;
    
    public function setName(string $name): self
    {
        $this->name = $name;
        
        return $this;
    }
    
    public function setCapitalTown(
        TownInterface $town
    ): self {
        $this->capitalTown = $town;
        
        return $this;
    }
    
    public function grantAccess(int $sectionId): bool
    {
        // ???
    }
}
interface UserInterface {
    public function setName(string $name): self;
    
    public function setCapitalTown(
        TownInterface $town
    ): self;
    
    public function grantAccess(int $sectionId): bool;
}

Refactoring time!

Skúsime kód zrefaktorovať

Stiahnite si zdrojáky

Problém: triedy majú nastaveného nesprávneho rodiča

Prečo má UserInterface metódy setCapitalTown a grantAccess, keď ich nevedia implementovať všetci potomkovia?

  • rozdelíme interface
  • každý typ používateľa bude dediť od správneho interfacu

Výsledok:

interface UserInterface {
    public function setName(string $name): self;
}

interface PlayerInterface extends UserInterface {
    public function setCapitalTown(TownInterface $town);
}

interface AdminUserInterface extends UserInterface {
    public function grantAccess(int $sectionId);
}

class AdminUser implements AdminUserInterface {
    public function setName(string $name) { /* ... */ }
    public function grantAccess(int $sectionId) { /* ... */ }
}

class Player implements PlayerInterface {
    public function setName(string $name) { /* ... */ }
    public function setCapitalTown(TownInterface $town) { /* ... */ }
}

Príklad 2

interface BuildingsProviderInterface {
    /**
     * @return BuildingInterface[]
     */
    public function getBuildings();
}

class BuildingsProvider implements BuildingsProviderInterface {

    public function getBuildings() {
        $buildings = [];
        // ...
        return $buildings;
    }
    
}

class PremiumBuildingsProvider implements BuildingsProviderInterface {

    public function getBuildings() {
        $buildings = new BuildingCollection();
        // ...
        return $buildings;
    }
    
}

class BuildingCollection implements Iterator { /* ... */}
<?php

use Example2\PremiumBuildingsProvider;
use Example2\BuildingsProvider;
use Example2\BuildingsProviderInterface;

require_once __DIR__ . '/vendor/autoload.php';

class UsageExample2
{
    public function run(BuildingsProviderInterface $buildingsProvider)
    {
        $buildings = $buildingsProvider->getBuildings();

        echo "pocet budov: " . count($buildings) . "\n";

        foreach ($buildings as $building) {
            echo $building->getName() ."\n";
        }
    }
}

$example = new UsageExample2();

$example->run(new BuildingsProvider());
$example->run(new PremiumBuildingsProvider());

Problém: rozdielny typ návratovej hodnoty, rodič ju špecifikuje nejasne

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

  • rodič musí mať jednoznačnejšiu špecifikáciu návratovej hodnoty
  • upravíme potomkov, aby spĺňali špecifikáciu
interface BuildingsProviderInterface
{
    public function getBuildings(): BuildingCollection;
}

// alebo

interface BuildingsProviderInterface
{
    public function getBuildings(): array;
}

Výsledok:

Príklad 3

interface RangedStrengthCalculatorInterface
{
    public function calculateStrength(
    	UnitInterface $unit,
        int $howMany,
        int $weatherType
    ): int;
}

class RangedStrengthCalculator implements RangedStrengthCalculatorInterface
{
    public function calculateStrength(
    	UnitInterface $unit,
        int $howMany,
        int $weatherType
    ): int {
        if (!($unit instanceof Archer) && !($unit instanceof LongBowArcher)) {
            throw new InvalidArgumentException(
                'Invalid unit type'
            );
        }
        // ...
    }
}

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

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

  • zistíme, čím je Archer a LongBowArcher výnimočný a rozdielny oproti UnitInterface
  • upravíme/vytvoríme patričné interfacy a typehinty
  • Archer a LongBowArcher sú logicky podtypom UnitInterface
  • zavedieme nový interface RangedUnitInterface
  • upravíme RangedStrengthCalculator aj RangedStrenghtCalculatorInterface
interface RangedStrengthCalculatorInterface
{
    public function calculateStrength(
    	RangedUnitInterface $unit,
        int $howMany,
        int $weatherType
    ): int;
}

class RangedStrengthCalculator implements RangedStrengthCalculatorInterface
{
    public function calculateStrength(
    	RangedUnitInterface $unit,
        int $howMany,
        int $weatherType
    ): int {
        // už tu nie je žiadna kontrola typu inštancie
        // ...
    }
}

Príklad 4

interface MessageInterface
{
    public function setText(string $text): self;
    public function getText(): string;
}

class ArticleMessage implements MessageInterface
{
    // ...
    
    public function setText(string $text): self
    {
        // ...
    }
    
    public function getText(): string
    {
        //...
    }
    
    public function setArticleId(int $id)
    { 
        // ...
    }
}

class Article {
    public function addMessage(MessageInterface $message)
    {
        $message->setArticleId($this->id);
        // ...
    }
}

Problém: Volaná metóda setArticleId nepatrí použitému interfacu.

Keď do addMessage vložíme inštanciu MessageInterface, ktorá nemá metódu setArticleId, tak kód zhavaruje

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

Výsledok:

interface ArticleMessageInterface extends MessageInterface
{
    public function setArticleId(int $id): self { /* ... */ }
}

class ArticleMessage implements ArticleMessageInterface
{
    public function setText(string $text): self { /* ... */ }
    public function getText(): string { /* ... */ }
    public function setArticleId(int $id): self { /* ... */ }
}

class Article
{
    public function addMessage(ArticleMessageInterface $message)
    {
        $message->setArticleId($this->id);
        // ...
    }
}

Opakovanie

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

Liskov Substitution Principle

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 dodržiavania

Liskov Substitution Principle

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

Viac informácií:

Matthias Noback - Principles of Package Design

Ďakujem za pozornosť