1. S.O.L.I.D Расшифруем: S - Single responsibility principle (SRP) O - Open/closed principle (OCP) L - Liskov substitution principle (LSP) I - Interface segregation principle (ISP) D - Dependency inversion principle (DIP)
2. Зачем эти правила? Помогают построить архитектуру приложения, которое со временем возможно будет проще (дешевле) поддерживать и развивать. Помогают писать повторно используемый код.
3. Принцип единственности ответственности (SRP) Не должно быть больше одной причины для изменения класса. Почему? Потому что это ведет к хрупкости дизайна (пишем один функционал - ”отваливается” другой).
6. Принцип открытости/закрытости (OCP) Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для изменения. Почему? Потому что это позволяет быстро и безболезненно реагировать на изменение бизнес-требований.
7. class Logger { public function log($text) { // Сохраняем текст в лог (лог у нас будет храниться в файлах) } } class Product { private $_logger; public function __construct() { $this->_logger = new Logger(); } /* Продать товар */ public function sale() { // … продаем товар // Записываем дату продажи в лог $this->_logger->log('Sale time: '. time()); } } Чем плох этот код?
8. Лог продаж в файлах?! Это же отстой! Изменение требований: лог надо хранить в БД class DBLogger { public function log($text) { // Сохраняем что-то в лог (лог у нас будет храниться в БД) } } class Product { private $_logger; public function __construct() { // Меняем класс Product, чтоб поменять логер (помните про SRP?) $this->_logger = new DBLogger(); } /* Продать товар*/ public function sale() { // Продаем товар // ... // Записываем дату продажи в лог $this->_logger->log('Sale time: '. time()); } }
9. Готовимся к борьбе с изменениями требований (а не к борьбе с менеджерами) interface ILogger { public function log($text); } class Logger implements ILogger { public function log($text) { // Сохраняем что-то в лог (лог у нас будет храниться в файлах) } } class DBLogger implements ILogger { public function log($text) { // Сохраняем что-то в лог (лог у нас будет храниться в БД) } } class Product { private $_logger; public function __construct(ILogger $logger) { $this->_logger = $logger; } /* Продать товар */ public function sale() { // Продаем товар // ... // Записываем дату продажи в лог $this->_logger->log('Sale time: '. time()); } }
10. Принцип подстановки Барбары Лисков (LSP) Поведение наследуемых классов не должно противоречить поведению, заданному базовым классом. Почему? Потому что клиентский код начинает считать производный класс разновидностью базового, и возможно появление кода, явно использующего этот факт.
12. Ударим кодом по уткам (базовый класс) /* Определим базовую утку */ abstract class Duck { private $_batteryStatus = 100; // Стстус заряда батареек утки (%) private $_steps = 0; // Количество шагов пройденных уткой (шт.) public function getBatteryStatus() { return $this->_batteryStatus; } public function setBatteryStatus($value) { $this->_batteryStatus = $value; } public function getSteps() { return $this->_steps; } public function setSteps($value) { $this->_steps = $value; } // Эти методы абстрактные (разные утки крякают и двигаются по-своему) abstract public function move(); abstract public function quack(); }
13. Утки на батарейках (Америка, Китай) /* Американская утка на батарейках */ class AmericanDuck extends Duck { public function move() { $this->setSteps($this->getSteps() + 1); // Утка шагает $this->setBatteryStatus($this->getBatteryStatus() - 20); // Батарейки садятся echo "Я прошагала {$this->getSteps()} шагов (заряд батарейки: {$this->getBatteryStatus()}%) "; } public function quack() { echo "Здравствуйте, я Американская утка! "; } } /* Китайская утка на батарейках */ class ChinaDuck extends Duck { public function move() { $this->setSteps($this->getSteps() + 1); // Утка шагает $this->setBatteryStatus($this->getBatteryStatus() - 10); // Батарейки садятся (но не так быстро) echo "Я прошагала {$this->getSteps()} шагов (заряд батарейки: {$this->getBatteryStatus()}%) "; } public function quack() { echo "Здравствуйте, я Китайский утка, я плохо говорить по-русски, но я уметь много шагать! "; } }
14. Клиентский код использования уток // Имеем список уток $duckList = array(new AmericanDuck(), new ChinaDuck()); // Просим всех уток по очереди представиться и походить foreach($duckList as $duck) { $duck->quack(); // Представимся while ($duck->getBatteryStatus() > 0) { // Утка должна шагать, пока не сядут батарейки $duck->move(); sleep(1); } echo ""; }
15. Появилась возожность юзать живую утку! /* Живая утка. Её особенность в том, что у нее нет батареек и она шагает сколько хочет */ class BrainyDuck extends Duck { public function move() { $this->setSteps($this->getSteps() + 1); // Утка шагает echo "Я прошагала {$this->getSteps()} шагов "; } public function quack() { echo "Здравствуйте, я живая утка с мозгом! "; } public function getBatteryStatus() { throw new Exception('Сума сошел? Какие батарейки, я живая!'); } public function setBatteryStatus() { throw new Exception('Сума сошел? Какие батарейки, я живая!'); } }
16. Клиентский код и Fail исползования уток // Имеем список уток $duckList = array(new AmericanDuck(), new ChinaDuck(), new BrainyDuck()); // Просим всех уток по очереди представиться и походить foreach($duckList as $duck) { $duck->quack(); // Представимся while ($duck->getBatteryStatus() > 0) { // Утка должна шагать, пока не сядут батарейки $duck->move(); sleep(1); } echo ""; }
17. Принцип разделения интерфейса (ISP) Клиенты не должны зависеть от методов, которые они не используют. Почему? Если мы определим большой универсальный интерфейс, тогда в наследниках возможно появление множества заглушек, а соответственно, много лишнего кода, который неудобно поддерживать.
18. Делаем трансформера (пока что все хорошо) interface IMegaTrsansformer { public function transformToCar(); public function transformToShip(); public function transformToPlane(); } class MegaTrsansformer implements IMegaTrsansformer { public function transformToCar() { echo 'Я преобразовался и стал спортивной машиной!'; } public function transformToShip() { echo 'Я преобразовался и стал сверхбыстрым катером!'; } public function transformToPlane() { echo 'Я преобразовался и стал истребителем!'; } }
19. А теперь нужно сделать менее крутых трансформеров (в каждом из них теперь костыли) class TaxiTrsansformer implements IMegaTrsansformer { public function transformToCar() { echo 'Я преобразовался и стал такси!'; } public function transformToShip() { throw new Exception('Данная трансформация не поддерживается'); } public function transformToPlane() { throw new Exception('Данная трансформация не поддерживается'); } } class StelthTrsansformer implements IMegaTrsansformer { public function transformToCar() { throw new Exception('Данная трансформация не поддерживается'); } public function transformToShip() { throw new Exception('Данная трансформация не поддерживается'); } public function transformToPlane() { echo 'Я преобразовался и стал стелсом!'; } } class IcebreakerTrsansformer implements IMegaTrsansformer { public function transformToCar() { throw new Exception('Данная трансформация не поддерживается'); } public function transformToShip() { echo 'Я преобразовался и стал ледоколом!'; } public function transformToPlane() { throw new Exception('Данная трансформация не поддерживается'); } }
20. Лучше иметь такой набор интерфейсов interface ICarTrsansformer { public function transformToCar(); } interface IShipTrsansformer { public function transformToShip(); } interface IPlaneTrsansformer { public function transformToPlane(); }
21. И такой набор классов class MegaTrsansformer implements ICarTrsansformer, IShipTrsansformer, IPlaneTrsansformer { public function transformToCar() { echo 'Я преобразовался и стал спортивной машиной!'; } public function transformToShip() { echo 'Я преобразовался и стал сверхбыстрым катером!'; } public function transformToPlane() { echo 'Я преобразовался и стал истребителем!'; } } class TaxiTrsansformer implements ICarTrsansformer { public function transformToCar() { echo 'Я преобразовался и стал такси!'; } } class StelthTrsansformer implements IPlaneTrsansformer { public function transformToPlane() { echo 'Я преобразовался и стал стелсом!'; } } class IcebreakerTrsansformer implements IShipTrsansformer { public function transformToShip() { echo 'Я преобразовался и стал ледоколом!'; } }
24. Анатомия зависимостей (X зависит от Y + циклическая зависимость) Зависимости бывают: - Прямые; - Транзитивные; - Циклические.
25. Зависимость модуля верхннго уровня от модулей нижнего уровня class siteMapBilder { private $_dataStorage; private $_webGrabber; public function __construct() { $this->_dataStorage = new FileDataStorage(); $this->_webGrabber = new CurlWebGrabber(); } // ... Здесь какие-то методы для построения карты сайта }
26. Освобождаем SiteMapBuilder от зависимостей (инвертируем зависимости) class siteMapBilder { private $_dataStorage; private $_webGrabber; public function __construct(WebGrabber $dataStorage, DataStorage $webGrabber) { $this->_dataStorage = $dataStorage; $this->_webGrabber = $webGrabber; } // ... Здесь какие-то методы для построения карты сайта }
27. «... любые хорошо структурированные объектно-ориентированные архитектуры имеют четко определенные слои, каждый из которых поддерживает некоторый компактный набор служб с помошью хорошо определенного и контролируемого интерфейса» Г. Буч Разделение архитектуры по слоям