Проблема
class Order {
public $id;
}
class Order_Processing {
public function create_new_order(): void {
/* Логика выполнения заказа */
$this->log('Order created!');
}
private function log( string $message ): void
{
echo "Save log with message: {$message}" . PHP_EOL;
}
}
$order_processing = new Order_Processing();
$order_processing->create_new_order();
В классе Order_Processing принцип Single Responsibility т.к. кроме обработки заказа у нас происходит еще и логирование. Так же код логирования нельзя переиспользовать. Мы немного подумали и решили, что нужно создать класс Logger, тогда его можно будет переиспользовать в других классах.
class Logger {
public function log( string $message ) {
echo "Save log with message: {$message}" . PHP_EOL;
}
}
class Order_Processing {
public function create_new_order() {
/* Логика выполнения заказа */
$logger = new Logger();
$logger->log('Order created!');
}
}
$order_processing = new Order_Processing();
$order_processing->create_new_order();
Класс Logger теперь можно переиспользовать в других классах. В классе Order_Processing у нас появляется жесткая зависимость (Hard Dependency). Чтобы понять, от чего зависит класс Order_Processing нужно прочитать весь код. Объект класса Logger будет создавать каждый раз при создании заказа.
В силу входит Dependency Injection
class Order_Processing {
private $logger;
public function __construct( Logger $loger ) {
$this->logger = $logger;
}
public function create_new_order() {
/* Логика выполнения заказа */
$this->logger->log('Order created!');
}
}
$logger = new Logger();
$order_processing = new Order_Processing( $logger );
$order_processing->create_new_order();
В целом уже все хорошо, уже применяется принцип Dependency Injection. Но нарушен принцип Dependency Inversion и мы можем вместо класса Logger в конструкторе использовать интерфейс, но мы опустим эту деталь.
Зависимостей становится больше
class Order_Processing {
private $logger;
public function __construct( Logger $loger, Order_Repository $repository, SMS_Notifier $sms_notifier ) {
$this->logger = $logger;
$this->repository = $repository;
$this->sms_notifier = $sms_notifier;
}
public function create_new_order() {
/* Логика выполнения заказа */
$this->logger->log('Order created!');
}
}
$repository = new Order_Repository();
$sms_notifier = new SMS_Notifier();
$logger = new Logger();
$order_processing = new Order_Processing( $logger, $repository, $sms_notifier );
$order_processing->create_new_order();
Конструктор разрастается и теперь в любом месте, где мы используем Order_Processing нам приходится тащить за этим классом все его зависимости. При добавлении и изменении зависимостей необходимо менять код везде(можно решить с помощью паттерна Factory, но это решит проблему не на долго).
Применение Inversion Of Control
Inversion Of Control — говорит о том, что зависимостями должен заниматься фреймворк, а не разработчики.
Есть несколько подходов для реализиции Ioc. Рассмотрим самые известные, это:
- Service Locator — плохой вариант
- Dependency Injection Container — хороший вариант
Service Locator
Подключим библиотеку Pimple
и заменим весь конструктор на ServiceLocator. Создаем конфиг для Service Locator
require __DIR__ . '/../vendor/autoload.php';
$container = new PimpleContainer();
$container['logger'] = function ( $container ) {
return new Logger();
};
$service_locator = new PimplePsr11ServiceLocator( $container, ['logger'] );
и изменяем наш класс Order_Processing:
class Order_Processing {
private $service_locator;
public function __construct(PimplePsr11ServiceLocator $service_locator) {
$this->service_locator = $service_locator;
}
public function create_new_order() {
// Здесь логика создания заказа
$this->service_locator->get('logger')->log('Order created!');
}
}
Но почему же Сервис Локатор считается антипаттерном?
Из минусов:
- Зависимости указаны неявно. Смотря на код Order_Processing мы не понимаем какие у него есть зависимости.
- Зависимости настроены неявно. Мы так же не понимаем что находится за идентификатором ‘logger’
- Сложно рефакторить. Попробуйте найти в большом проекте все вызовы метода log которые относятся к классу Logger.
- Если используется он вот так
$this->serviceLocator->get('logger')->log('Order created!')
. Здесь разве что поиск в помощь. Но представте что вам нужно отрефакторить метод который называется например «save» и он у множества классов которые настроенны в Локаторе Служб. - Здесь нарушен принцип Dependency Inversion. Зависимости создаются внтури самого класса.
Что делать, если в проекте уже есть Service Locator и избавится от него не получится
Поправим немного конфиг:
require __DIR__ . '/../vendor/autoload.php';
$container = new PimpleContainer();
$container['logger'] = function ( $container ) {
return new Logger();
};
$container['order.processing'] = function ( $container ) {
return new Order_Processing( $container['logger'] );
};
$service_locator = new PimplePsr11ServiceLocator( $container, ['logger', 'order.processing'] );
И так же поправим наш класс Order_Processing и его вызов.
class Order_Processing {
private $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function create_new_order() {
// Здесь логика создания заказа
$this->logger->log('Order created!');
}
}
$order_processing = $serviceLocator->get('order.processing');
$order_processing->create_new_order();
Таким образом мы решаем ряд следующих проблем:
- Теперь в классе OrderProcessing зависимости указаны явно. Мы видим от каких копоненов зависит наш класс.
- Стало полегче рефакторить. Мы слегкость сможем найти все использования метода log конкретного класса.
- Dependecy Inversion не нарушен (за исключением того что зависимость построена на реализацию, а не на абстракцию) Из минусов:
- При разработке веб-приложения нам всеравно прийдется использовать локатор в контроллерах, консольных команда. И зависимости будут по прежнему указаны не явно.
- При добавлении или удалении зависимости, нужно править конфиги.
Dependency Injection Container
Лучший вариант на данный момент это использование Dependency Injection Container(далее DIC). Для этого нужно поставить http://php-di.org/ и создать DIC:
require __DIR__ . '/../vendor/autoload.php';
$container = new DIContainer();
и теперь класс Order_Processing и его вызов выглядят так:
class Order_Processing {
private $logger;
public function __construct( Logger $logger ) {
$this->logger = $logger;
}
public function create_new_order() {
// Здесь логика создания заказа
$this->logger->log( 'Order created!' );
}
}
$order_processing = $container->get(Order_Processing::class);
$order_processing->create_new_order();
Таким образом первый раз будет создан класс Order_Processing и Logger т.к. он требуется в конструкторе Order_Processing и оба класса будут записаны в DIC. При каждом вызове сначала проверяется наличие объекта и всех его зависистей в DIC и при их отстутствии они создаются.