O v SOLIDe
Milan Herda, 09 / 2017
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
- Open-Closed Principle
- Výhody Open-Closed Principle
- 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
Open-Closed Principle
Open-Closed Principle
Trieda by mala byť otvorená pre rozširovanie a zároveň uzatvorená pre zmeny
Open-Closed Principle
Mali by ste byť schopní rozšíriť správanie triedy aj bez potreby modifikácie jej kódu
Výhody dodržiavania
Open-Closed Principle
- možnosť zmeniť správanie triedy aj bez jej úpravy
- bezpečnejšie úpravy (lebo nemeníme kód)
Symptómy porušenia princípu
- trieda má podmienky pre určenie stratégie (typicky napr. switch)
- podobné podmienky sa opakujú na viacerých miestach v kóde
- trieda obsahuje natvrdo nakódované názvy tried (v poli, podmienkach, stringoch)
- vo vnútri triedy sa vytvárajú objekty pomocou new
- trieda obsahuje hardkódovaný názov súboru, emailovú adresu alebo inú skalárnu hodnotu
Ako refaktorovať
- zabezpečiť spĺňanie Single Responsibility Principle
- identifikovať všeobecné a špecifické časti úlohy
- oddeliť špecifické a všeobecné úlohy
- špecifické časti preniesť von (do samostatných tried)
- prepojiť všeobecnú a špecifickú časť pomocou "konfigurácie"
class MessageLogger
{
public static function logMessage(string $message)
{
file_put_contents(
'/var/log/my-app/action.log',
$message . "\n",
FILE_APPEND
);
}
}
Zmeňte súbor, do ktorého sa loguje.
- Nie je to možné bez modifikácie kódu triedy
- Názov súboru je špecifická časť, ktorú treba vyňať
Refactoring time!
Skúsime triedu zrefaktorovať
Stiahnite si zdrojáky
Krok 1: Zabezpečiť spĺňanie Single-Responsibility princípu
Trieda už spĺňa SRP
Krok 2: Identifikovať všeobecné a špecifické časti úlohy
Názov súboru je špecifická časť
Krok 3: Oddeliť špecifické a všeobecné úlohy
Metóda logMessage už nebude statická.
Názov súboru bude závislosť triedy vkladaná cez konštruktor.
Krok 4: Špecifické časti preniesť von (do samostatných tried)
Názov súboru príde do triedy zvonka v čase jej konštrukcie. Nie je potrebné vytvárať novú triedu.
Krok 5: Prepojiť všeobecnú a špecifickú časť pomocou "konfigurácie"
Názov môže byť definovaný v nejakom konfiguračnom súbore alebo databáze.
Výsledok
class MessageLogger
{
private string $logFileName;
public function __construct(string $logFileName)
{
$this->logFileName = $logFileName;
}
public function logMessage(string $message)
{
file_put_contents(
$this->logFileName,
$message . "\n",
FILE_APPEND
);
}
}
Príklad 2
class GenericEncoder
{
public function encodeToFormat(array $data, string $format): string
{
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, string $format): string
{
switch ($format) {
case 'json':
$data = $this->forceArray($data);
$data = $this->fixKeys($data);
break;
case 'xml':
$data = $this->fixAttributes($data);
break;
}
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
Krok 1: 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 zodpovednosti podľa Single Responsibility Principle
Krok 1: Oddelenie zodpovedností
Vytvoríme továrničku pre encodery
class EncoderFactory
{
public function createForFormat(string $format): EncoderInterface
{
if ($format == 'json') {
return new JsonEncoder();
} else if ($format == 'xml') {
return new XmlEncoder();
}
throw new InvalidArgumentException(sprintf('Unknown format %s', $format));
}
}
Encodery by mali mať jeden interface
interface EncoderInterface
{
public function encode(array $data): string;
}
class JsonEncoder implements EncoderInterface {}
class XmlEncoder implements EncoderInterface {}
Krok 2: Urobiť GenericEncoder otvorený pre rozširovanie
GenericEncoder bude závisieť na EncoderFactory
class GenericEncoder
{
private $encoderFactory;
public function __construct(EncoderFactory $encoderFactory)
{
$this->encoderFactory = $encoderFactory;
}
}
Nepresunuli sme len problém na iné miesto?
Presunuli.
Krok 2: Urobiť GenericEncoder otvorený pre rozširovanie
GenericEncoder bude závisieť na EncoderFactoryInterface
interface EncoderFactoryInterface
{
public function createForFormat(string $format): EncoderInterface
}
class EncoderFactory implements EncoderFactoryInterface {}
class GenericEncoder
{
private $encoderFactory;
public function __construct(EncoderFactoryInterface $encoderFactory)
{
$this->encoderFactory = $encoderFactory;
}
}
GenericEncoder teraz spĺňa OCP.
Čo ale EncoderFactory?
Krok 3: Urobiť továrničku otvorenú pre rozširovanie
class EncoderFactory implements EncoderFactoryInterface
{
private array $factories = [];
public function registerFactory(string $format, callable $factory)
{
$this->factories[$format] = $factory;
return $this;
}
public function createForFormat(string $format): EncoderInterface
{
if (!array_key_exists($format, $this->factories)) {
throw new InvalidArgumentException(sprintf('Unknown format %s', $format));
}
$factory = $this->factories[$format];
$encoder = $factory();
return $encoder;
}
}
Vytváranie jednotlivých encoderov vytiahneme mimo továrničky do svojich vlastných factory
Krok 3: Upratať zodpovednosti
class JsonEncoder implements EncoderInterface
{
public function encode(array $data): string
{
$this->prepareData($data);
return json_encode($data);
}
// ...
}
Prípravu dát presunieme na správne miesto
Finálne riešenie
$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');
Príklad 3
class BuildingFactory
{
/** @var array<int,string> */
private $classes = [
BuildingType::LUMBERJACK => 'Lumberjack',
BuildingType::QUARRY => 'Quarry',
BuildingType::IRON_MINE => 'IronMine',
BuildingType::FARMS => 'Farms',
BuildingType::STOREHOUSE => 'Storehouse',
];
public function create(int $type, int $level): BuildingInterface
{
if (!isset($this->classes[$type])) {
throw new \Exception('Takýto typ budovy neexistuje');
}
$building = new $this->classes[$type]($level);
return $building;
}
}
Upravte kód tak, aby sklad (Storehouse) dostal automaticky do konštruktoru aj druhý parameter, ktorým je jeho úvodná kapacita.
Hodnota úvodnej kapacity je 650.
Dodržte pritom Open-Closed Principle.
Riešenie: Storehouse
class Storehouse extends ABuilding
{
private int $initialCapacity;
public function __construct(int $level, int $initialCapacity)
{
parent::__construct($level);
$this->initialCapacity = $initialCapacity;
}
// ...
}
Riešenie: Malé továrničky pre budovy
class FarmsBuildingFactory implements BuildingFactoryInterface
{
public function createBuilding(int $level): BuildingInterface
{
return new Farms($level);
}
}
class StorehouseBuildingFactory implements BuildingFactoryInterface
{
private int $initialCapacity;
public function __construct(int $initialCapacity)
{
$this->initialCapacity = $initialCapacity;
}
public function createBuilding(int $level): BuildingInterface
{
return new Storehouse($level, $this->initialCapacity);
}
}
interface BuildingFactoryInterface
{
public function createBuilding(int $level): BuildingInterface;
}
Riešenie: Všeobecná továrnička
class BuildingFactory
{
private array $factories = [];
public function registerFactory(int $type, BuildingFactoryInterface $factory)
{
$this->factories[$type] = $factory;
return $this;
}
public function create(int $type, int $level): BuildingInterface
{
if (!isset($this->factories[$type])) {
throw new \Exception('Takýto typ budovy neexistuje');
}
$factory = $this->factories[$type];
$building = $factory->create($level);
return $building;
}
}
Riešenie: Použitie
$genericFactory = new BuildingFactory();
$genericFactory->registerFactory(
BuildingType::FARMS, new FarmsBuildingFactory()
);
$genericFactory->registerFactory(
BuildingType::IRON_MINE, new IronMineBuildingFactory()
);
$genericFactory->registerFactory(
BuildingType::LUMBERJACK, new LumberjackBuildingFactory()
);
$genericFactory->registerFactory(
BuildingType::QUARRY, new QuarryBuildingFactory()
);
$genericFactory->registerFactory(
BuildingType::STOREHOUSE, new StorehouseBuildingFactory(650)
);
$storehouse = $genericFactory->create(BuildingType::STOREHOUSE, 1);
echo $storehouse->getCapacity();
Opakovanie
Trieda by mala byť otvorená pre rozširovanie a zároveň uzatvorená pre zmeny
Mali by sme byť schopní rozšíriť správanie triedy aj bez potreby modifikácie jej kódu
Výhody dodržiavania
Open-Closed Principle
- možnosť zmeniť správanie triedy aj bez jej úpravy
- bezpečnejšie úpravy (lebo nemeníme kód)
Symptómy porušenia princípu
- trieda má podmienky pre určenie stratégie (typicky napr. switch)
- podobné podmienky sa opakujú na viacerých miestach v kóde
- trieda obsahuje natvrdo nakódované názvy tried (v poli, podmienkach, stringoch)
- vo vnútri triedy sa vytvárajú objekty pomocou new
- trieda obsahuje hardkódovaný názov súboru, emailovú adresu alebo inú skalárnu hodnotu
Viac informácií:
Matthias Noback - Principles of Package Design
Ďakujem za pozornosť
O v SOLIDe
By Milan Herda
O v SOLIDe
- 643