I v SOLIDe
Milan Herda, 03 / 2018
Príklady sú vlastné alebo prebrané z knihy
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
- Interface Segregation Principle
- Výhody ISP
- 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
Interface Segregation Principle
Princíp oddelenia rozhraní
Interfejsy by mali byť jemne granulované a špecifické pre klienta
Interface Segregation Principle
Jemne granulované
- malé množstvo metód
- najmenšie možné, ktoré dáva zmysel
- áno, častokrát iba jedna metóda
- a áno, dokonca aj nula
Špecifické pre klienta
- klient by nemal závisieť na metódach, ktoré nepoužíva
- do interfacu sa dajú iba metódy, ktoré potrebuje
- ak viacerí klienti pracujú s triedou rôzne, tak vytvoríme viacero interfejsov
- interface definuje skupinu metód, ktoré "chodia spolu"
Klient je kód používajúci iný kód
Výhody dodržiavania
Interface Segregation Principle
- časti kódu sú navzájom ľahko vymeniteľné
- klienti závisia iba na tom, čo potrebujú
Symptómy porušenia princípu
- interface má priveľa metód
- deravé abstrakcie (leaky abstractions)
- žiaden kód nepoužíva všetky metódy interfacu
- interface obsahuje metódy, ktoré "nechodia spolu"
- triedy bez interfacu
interface FileInterface
{
public function rename(string $name);
public function changeOwner(string $user, string $group);
}
// požitie
public function importFile(FileInterface $file)
{
$file->rename($this->location);
}
// ...
public function transferOwner(FileInterface $file)
{
$file->changeOwner($this->user, $this->group);
}
class DosFile implements FileInterface
{
public function rename(string $name)
{
// ...
}
public function changeOwner(string $user, string $group)
{
throw new BadMethodCallException(
'Not implemented for DOS files'
);
}
}
class LocalFile implements FileInterface
{
public function rename(string $name)
{
rename($this->filepath, $name);
$this->filepath = $name;
}
public function changeOwner(string $user, string $group)
{
chown($this->filepath, $user);
chgrp($this->filepath, $group);
}
}
Časté riešenie je pridanie has/can metódy
interface FileInterface {
public function rename(string $name);
public function canChangeOwner(): bool;
public function changeOwner(string $user, string $group);
}
class DosFile implements FileInterface
{
public function canChangeOwner(): bool
{
return false;
}
public function rename(string $name) { /* ... */ }
public function changeOwner(string $user, string $group) { /* ... */ }
}
// ...
public function transferOwner(FileInterface $file)
{
if ($file->canChangeOwner()) {
$file->changeOwner($this->user, $this->group);
}
}
Časté riešenie je pridanie has/can metódy
- každý potomok FileInterface musí implementovať canChangeOwner
- DosFile teraz namiesto jednej zbytočnej metódy imlementuje dve
- canChangeOwner a changeOwner sa musia volať spolu = veľa duplikácií v kóde (+ temporal coupling)
Takéto riešenie nie je dobré, lebo:
Refactoring time!
Upravte kód tak, aby metóda transferOwner nespadla na volaní changeOwner
Zdrojové súbory:
Výsledok
interface FileInterface {
public function rename(string $name);
}
interface FileWithOwnerInterface {
public function changeOwner(string $user, string $group);
}
class DosFile implements FileInterface {
public function rename(string $name) {
// ...
}
}
class LocalFile implements FileInterface, FileWithOwnerInterface {
public function rename(string $name) {
// ...
}
public function changeOwner(string $user, string $group) {
//...
}
}
public function importFile(FileInterface $file)
{
$file->rename($this->location);
}
// ...
public function transferOwner(FileWithOwnerInterface $file)
{
$file->changeOwner($this->user, $this->group);
}
Príklad 2
interface ServiceContainerInterface {
public function get(string $name);
public function set(string $name, callable $factory): ServiceContainerInterface;
}
// service provider
public function defineServices(ServiceContainerInterface $serviceContainer) {
$serviceContainer->set('mailer', function () use ($serviceContainer) {
return new Mailer(
$serviceContainer->get('mailer.transport')
);
});
$serviceContainer->set('mailer.transport', function () use ($serviceContainer) {
return new MailerSmtpTransport();
});
}
// controller
public function detailAction(ServiceContainerInterface $serviceContainer) {
$repository = $serviceContainer->get('repository.user');
$templating = $serviceContainer->get('templating');
}
Problém: klienti požadujú funkcionalitu, s ktorou nepracujú
- Service provider používa najmä metódu set
- V controlleri sa používa výhradne metóda get
Upravte kód tak, aby každý klient vyžadoval iba to, čo potrebuje
interface MutableServiceContainerInterface {
public function set(string $name, callable $factory): MutableServiceContainerInterface;
}
interface ServiceLocatorInterface {
public function get(string $name);
}
interface ServiceContainerInterface extends ServiceLocatorInterface, MutableServiceContainerInterface {}
// service provider
public function defineServices(ServiceContainerInterface $serviceContainer) {
$serviceContainer->set('mailer', function () use ($serviceContainer) {
return new Mailer(
$serviceContainer->get('mailer.transport')
);
});
$serviceContainer->set('mailer.transport', function () use ($serviceContainer) {
return new MailerSmtpTransport();
});
}
// controller
public function detailAction(ServiceLocatorInterface $serviceLocator) {
$repository = $serviceLocator->get('repository.user');
$templating = $serviceLocator->get('templating');
}
Riešenie
Príklad 3
Vytvárame malú knižnicu s utilitami pre jednoduché konverzie (napr. kľúče v poli na camelCase formát)
Knižnica sa bude do projektov inštalovať cez composer
Ferko Mrkvička z druhého konca sveta našu knižnicu používa takto:
class UserFacade {
private ArrayConverter $arrayConverter;
public function __construct(ArrayConverter $arrayConverter, /* ... */) {
$this->arrayConverter = $arrayConverter;
// ...
}
public function getActiveUsers(): array
{
$usersData = $this->userRepository->getActiveUsers();
return $this->arrayConverter->convert($usersData);
}
}
Kód knižnice v januári:
class ArrayConverter
{
public function convert(array $input): array
{
$result = [];
foreach ($input as $key => $data) {
if (is_array($data)) {
$data = $this->convert($data);
}
$result[$this->convertToCamelCase($key)] = $data;
}
return $result;
}
private function convertToCamelCase(string $str): string
{
/* ... */
}
}
Február: rozhodneme sa pridať triedu XmlUtil s užitočnými metódami pre XML
Napríklad formátovanie názvov XML tagov do camelCase formátu.
Nebudeme ale formátovanie do camelCase robiť odznova, keď ho už máme v triede ArrayConverter
XmlUtil si preto nainjectuje ArrayConverter cez konštruktor a len zavolá metódu convertToCamelCase
Metóda je ale privátna, tak ju najskôr urobíme public
Marec: uvedomili sme si, že je blbosť, aby XmlUtil závisel na celom ArrayConverter, keď potrebuje iba jednu metódu
Preto metódu extrahujeme do novej, samostatnej triedy.
XmlUtil a aj ArrayConverter budú závisieť na novej triede
Pôvodnú metódu z ArrayConverter tak môžeme zrušiť
Alebo nie?
ArrayConverter nemá interface, takže jeho interfacom sú všetky public metódy.
Keďže convertToCamelCase sa vo februári stalo public metódou, tak na nej medzičasom môže závisieť kód aj mimo našej knižnice
Odstrániť túto metódu znamená urobiť breaking change.
Riešenie:
Nerobiť implicitné interfejsy z public metód a už od začiatku používať vhodne vytvorené explicitné interface-y
Január
interface ArrayConverterInterface
{
public function convert(array $input): array;
}
class ArrayConverter implements ArrayConverterInterface
{
public function convert(array $input): array { /* ... */ }
private function convertUndescoreToCamelCase(string $str): string { /* ... */ }
}
Február
interface ArrayConverterInterface
{
public function convert(array $input): array;
}
interface StringConverterInterface
{
public function convertUndescoreToCamelCase(string $str): string;
}
class ArrayConverter implements ArrayConverterInterface, StringConverterInterface
{
public function convert(array $input): array { /* ... */ }
public function convertUndescoreToCamelCase(string $str): string { /* ... */ }
}
class XmlUtil
{
private StringConverterInterface $stringConverter; // v skutočnosti ArrayConverter
public function convertTagNameToCamelCase(string $str): string { /* ... */ }
}
Marec
interface ArrayConverterInterface
{
public function convert(array $input): array;
}
interface StringConverterInterface
{
public function convertUndescoreToCamelCase(string $str): string;
}
class StringConverter implements StringConverterInterface
{
public function convertUndescoreToCamelCase(string $str): string { /* ... */ }
}
class ArrayConverter implements ArrayConverterInterface
{
private StringConverterInterface $stringConverter;
public function convert(array $input): array { /* ... */ }
}
class XmlUtil
{
private StringConverterInterface $stringConverter;
public function convertTagNameToCamelCase(string $str): string { /* ... */ }
}
Ferko Mrkvička z druhého konca sveta našu knižnicu používa takto:
class UserFacade {
private ArrayConverterInterface $arrayConverter;
public function __construct(ArrayConverterInterface $arrayConverter, /* ... */) {
$this->arrayConverter = $arrayConverter;
// ...
}
public function getActiveUsers(): array
{
$usersData = $this->userRepository->getActiveUsers();
return $this->arrayConverter->convert($usersData);
}
}
Vaše verejné rozhrania sú vašimi "sľubmi" o spôsobe práce s vašimi triedami.
Ponúknutím menších interfejsov dávate menšie sľuby a uvoľňujete si ruky pre budúce zmeny.
Porušiť malý sľub je menej bolestivé ako porušiť veľký.
Trieda by mala mať interface, aj keď by bola jeho jedinou implementáciou.
Ak knižnica ponúka interface, tak vždy programujeme proti metódam interfacu a nikdy nie proti metódam triedy.
Opakovanie
Interfejsy by mali byť jemne granulované a špecifické pre klienta
Interface Segregation Principle
Jemne granulované
- malé množstvo metód
- najmenšie možné, ktoré dáva zmysel
- áno, častokrát iba jedna metóda
- a áno, dokonca aj nula
Špecifické pre klienta
- klient by nemal závisieť na metódach, ktoré nepoužíva
- do interfacu sa dajú iba metódy, ktoré potrebuje
- ak viacerí klienti pracujú s triedou rôzne, tak vytvoríme viacero interfejsov
- interface definuje skupinu metód, ktoré "chodia spolu"
Klient je kód používajúci iný kód
Výhody dodržiavania
Interface Segregation Principle
- časti kódu sú navzájom ľahko vymeniteľné
- klienti závisia iba na tom, čo potrebujú
Viac informácií:
Matthias Noback - Principles of Package Design
Ďakujem za pozornosť
I v SOLIDe
By Milan Herda
I v SOLIDe
- 623