#2 SOLID – Open/closed principle
Projektując poważny system musimy mieć na uwadze jego przyszłą ewolucję. Kolejną z zasad SOLID, która pozwoli nam w spokoju rozwijać nasz kod jest Open/closed principle. Mówimy nam ona o tym, że kod powinien być otwarty na rozbudowę oraz zamknięty na modyfikację.
Co to znaczy?
Najprostszym sposobem na ocenienie czy nasz kod jest zgodny z tą zasadą jest odpowiedzenie sobie na następujące pytanie:
- Czy jesteśmy wstanie dodać nową funkcjonalność poprzez dodanie nowych klas nie zmieniając istniejącego już kodu?
Przykład złamania zasady
<?php /** * Class Triangle */ class Triangle { /** * @var float */ private $a; /** * @var float */ private $h; /** * @return float */ public function getA(): float { return $this->a; } /** * @param float $a * @return Triangle */ public function setA(float $a): Triangle { $this->a = $a; return $this; } /** * @return float */ public function getH(): float { return $this->h; } /** * @param float $h * @return Triangle */ public function setH(float $h): Triangle { $this->h = $h; return $this; } }
<?php /** * Class Circle */ class Circle { /** * @var float */ private $radius; /** * @return float */ public function getRadius(): float { return $this->radius; } /** * @param float $radius * @return Circle */ public function setRadius(float $radius): Circle { $this->radius = $radius; return $this; } }
<?php /** * Class AreaCalculator */ class AreaCalculator { /** * @param array ...$shapes * @return float */ public function sum(...$shapes): float { $sum = 0; foreach ($shapes as $shape) { $sum += $this->calcArea($shape); } return $sum; } /** * @param $shape * @return float */ private function calcArea($shape): float { switch(get_class($shape)) { case 'Triangle': return 0.5 * $shape->getA() * $shape->getH(); case 'Circle': return M_PI * $shape->getRadius() * $shape->getRadius(); } } }
Powyżej znajduje się napisany na szybko przykład złamania zasady Open/closed. Mamy tutaj trzy klasy: dwie reprezentujące odpowiednio trójkąt oraz okrąg, natomiast trzecia z nich odpowiada za wykonywanie operacji na polach figur. Wszystko wydaje się tutaj ok, jednak zastanówmy się, co jeśli będziemy chcieli rozszerzyć funkcjonalność i dodać prostokąt. Oprócz napisania odpowiedniej klasy reprezentującej nową figurę, będziemy również musieli zmodyfikować metodę calcArea w klasie AreaCalculator, czyli nasz kod nie jest zgodny z zasadą OCP.
Przykład poprawnego kodu
<?php /** * Interface AreaCalculableInterface */ interface AreaCalculableInterface { public function calcArea(): float; }
<?php /** * Class Circle */ class Circle implements AreaCalculableInterface { /** * @var float */ private $radius; /** * @return float */ public function getRadius(): float { return $this->radius; } /** * @param float $radius * @return Circle */ public function setRadius(float $radius): Circle { $this->radius = $radius; return $this; } /** * {@inheritdoc} */ public function calcArea(): float { return M_PI * $this->radius * $this->radius; } }
<?php /** * Class Triangle */ class Triangle implements AreaCalculableInterface { /** * @var float */ private $a; /** * @var float */ private $h; /** * @return float */ public function getA(): float { return $this->a; } /** * @param float $a * @return Triangle */ public function setA(float $a): Triangle { $this->a = $a; return $this; } /** * @return float */ public function getH(): float { return $this->h; } /** * @param float $h * @return Triangle */ public function setH(float $h): Triangle { $this->h = $h; return $this; } /** * {@inheritdoc} */ public function calcArea(): float { return 0.5 * $this->a * $this->h; } }
<?php /** * Class AreaCalculator */ class AreaCalculator { /** * @param AreaCalculableInterface[] ...$shapes * @return float */ public function sum(AreaCalculabeInterface ...$shapes): float { $sum = 0; foreach ($shapes as $shape) { if (!$shape instanceof AreaCalculableInterface) { throw new \RuntimeException('Shape have to be instance of AreaCalculabeInterface.'); } $sum += $shape->calcArea(); } return $sum; } }
W powyższym przykładzie dodany został interfejs, który następnie jest implementowany w klasach reprezentujących figury, co wymusza zaimplementowanie w nich metody calcArea(). Dzięki takiej zmianie dodanie nowej figury wymaga od nas utworzenia jedynie odpowiedniej klasy reprezentującej tę figurę.
Podsumowanie
Oczywiście powyższy przykład jest trywialny i wiadomo jakie elementy mogą się zmieniać. Jednak jeśli projektujemy złożony system to często spotykamy się z problemem ustalenia, które elementy w przyszłości mogą ulec zmianie, tak aby je odpowiednio zaprojektować i przygotować się na wdrażanie nowych funkcjonalności w prosty sposób.
Nie powinniśmy też popadać w paranoje próbując idealnie zaprojektować każdą część aplikacji. Ważną kwestią jest odpowiednia refaktoryzacja kodu, do którego nie przewidzieliśmy przyszłych zmian, wtedy gdy jakieś zmiany w tym miejscu wystąpią. Wszystkiego idealnie nie przewidzimy, więc trzeba czasem reagować na bieżąco i pamiętać o zostawianiu kodu w lepszym stanie niż go zastaliśmy.
Pozostałe wpisy o SOLID
- SRP – Single responsibility principle (Zasada pojedynczej odpowiedzialności)
- OCP – Open/closed principle (Zasada otwarte/zamknięte)
- LSP – Liskov substitution principle (Zasada podstawienia Liskov)
- ISP – Interface segregation principle (Zasada segregacji interfejsów)
- DIP – Dependency inversion principle (Zasada odwrócenia zależności)
Subscribe and master unit testing with my FREE eBook (+60 pages)! 🚀
In these times, the benefits of writing unit tests are huge. I think that most of the recently started projects contain unit tests. In enterprise applications with a lot of business logic, unit tests are the most important tests, because they are fast and can us instantly assure that our implementation is correct. However, I often see a problem with good tests in projects, though these tests’ benefits are only huge when you have good unit tests. So in this ebook, I share many tips on what to do to write good unit tests.
Niepotrzebnie używasz w swoich przykładach phpdoc, skoro masz deklaracje typów.
W tym przypadku argumenty nie są tablicami ale pojedynczymi obiektami:
/**
* @param array …$shapes
* @return float
*/
public function sum(…$shapes): float
Można stworzyć interfejs Shape, klasy niech go implementują i wtedy
public function sum(Shape …$shapes): float
Faktycznie niedopatrzenie z tymi typami w phpdoc, niżej w przykładzie już jest ok. Co do stosowania phpdoc to faktycznie już miałem kilkukrotnie przestać ich używać, stosować jedynie tam gdzie się tego nie da załatwić przez type/return hint czyli np. do określania elementów tablic, ale jednak cały czas ich używam z przyzwyczajenia 😀
W przykładzie, który zawiera poprawną implementację zgodnie z OCP, użyty jest interfejs jako type hint. Co do tworzenia interfejsu Shape to należy sobie zadać pytanie co miałoby się w nim znaleźć, bo ja tutaj za bardzo nie widzę sensu dla czegoś takiego, a nawet wydaje mi się, że mógłby on się przyczynić do późniejszego łamania zasady ISP.
Jesteś pewny, że settery zwracające obiekt są dobrym wyjściem? Bo moim zdaniem pokazujesz jak pisać zgodnie z SOLIDem łamiąc przy okazji zasady CQRSa.
Pozdrawiam
Settery w ogóle nie są dobrym wyjściem w myśl CQRS. Faktycznie tutaj w ogóle nie potrzebnie ich używałem, powinienem to konstruktorem załatwić.