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ť
L v SOLIDe
By Milan Herda
L v SOLIDe
- 619