- Сначала в документе описаны задачи
- Далее - решения, имеющие те или иные недостатки
- Под конец описано решение через паттерн "Статический мост", решающие эти недостатки
Есть класс-сервис ProductsManager, содержащий в себе всю логику работу с товарами.
class ProductsManager {
/**
* Возвращает данные о товаре.
*/
public function get (int $id): Product {
return new Product($id, 'Кресло');
}
}
class Product {
private int $_id;
private string $_name;
private string $_photoUrl = '';
public function __construct ($id, $name) {
$this->_id = $id;
$this->_name = $name;
}
public function getId (): int { return $this->_id; }
public function getName (): string { return $this->_name; }
public function getPhotoUrl (): string { /* ... */ }
}Сразу он выбирает из БД только ID и имя товара.
$product = $dependencyInjectionContainer->productsManager->get(1);
var_dump($product->getId(), $product->getName());Для того, чтобы получить фото товара, нужно обратиться к какому-то относительно тяжёлому сервису, да и фото нужно далеко не всегда. Поэтому в объекте Product поле $_photoUrl изначально не инициализировано.
Тем не менее, получение фотки должно происходить прозрачно, как и обращение к ID или имени. Так как вся логика работы инкапсулирована внутри ProductsManager, объект Product для подгрузки фото налету должен обратиться к соответствующему методу ProductsManager.
Для этого классы должны быть написаны следующим образом:
ProductsManagerв качестве зависимости передаёт в инициализируемый объект самого себя.ProductsManagerреализует методgetPhotoUrl()для получения картинки.Productтоже реализует методgetPhotoUrl(), обращающийся кgetPhotoUrl()сервиса.
class ProductsManager {
public function get (int $id): Product {
return new Product($id, 'Вася', $this);
}
public function getPhotoUrl (int $productId): string {
return $this->_heavyPhotosService->get($productId)->url;
}
}
class Product {
private ProductsManager $_productsManager;
private int $_id;
private string $_name;
private string $_photoUrl = '';
public function __construct (int $id, string $name, ProductsManager $productsManager) {
$this->_id = $id;
$this->_name = $name;
$this->_productsManager = $productsManager;
}
public function getId (): int { return $this->_id; }
public function getName (): string { return $this->_name; }
public function getPhotoUrl (): string {
return $this->_productsManager->getPhotoUrl($this->_id);
}
}Для ускорения быстродействия сайта все полученные товары могут кэшироваться через сериализацию. При сериализации фото товара может быть уже получено из базы, или нет. Соответственно, когда товар получен из кэша, обращение к его методу getPhotoUrl() должно налету подгрузить картинку, обращением к сервису.
- При сериализации в кэш будет попадать и тяжёлый объект сервиса
UsersManager, со всеми его зависимостями. - При десериализации у этого сервиса не будут восстанавливаться подключения к ресурсам - соответственно, методы
getPhotoUrl()просто не будут работать.
Добавляем метод __sleep(), указывающий только те поля, которые нужно сохранять при сериализации. И метод __wakeup(), восстанавливающий подключение к сервису ProductsManager.
class Product {
public function __sleep (): array {
return ['_id', '_name', '_photoUrl'];
}
public function __wakeup (): array {
$this->_productsManager = $GLOBALS['dependencyInjectionContainer']->productsManager;
}
}- Нужно всегда следить за списком сериализуемых полей в
__sleep()- добавляя или удаляя ключи синхронно с изменением полей. - Код получения сервиса из контейнера зависимостей вбит жёстко, что нарушает принцип использования инъекции зависимостей.
- Невозможно нормальное внедрение мок-объектов для тестирования
class ProductsManager {
public function deserializeProductsList (array $productsCacheString): array {
$productsList = unserialize($productsCacheString);
foreach ($productsList as $product) {
$product->setManager($this);
}
}
}
class Product {
public function setManager (ProductsManager $productsManager) {
$this->_productsManager = $productsManager;
}
public function __sleep (): array {
return ['_id', '_name', '_photoUrl'];
}
}- Та же проблема с необходимостью следить за актуальностью полей в
__sleep() - Простая десериализация через
unserialize()не работает - нужно использовать вспомогательный методProductsManager::deserializeProductsList(). Если в кэше содержится много разновидностей данных, то нужно знать, какие десериализуются черезunserialize(), а какие - через специальные методы.
Переписываем код следующим образом:
- Дополнительно к сервисам-объектам, лежащим в контейнере зависимостей, создаём сопутствующие классы-сервисы, доступные глобально. Или один класс-сервис с необходимым количеством методов.
- При инициализации сервиса одним из параметров указываем, какое имя класса он должен прокидывать в создаваемые объекты для использования в роли статического моста.
- Объект
Productобращается к сервису через промежуточный глобально доступный статический класс, который был указан в параметрах при инициализации объекта.
class ProductsManager {
private $_staticBridgeClassName = '';
public function __construct (array $params) {
//...
//Сохранение настроек и зависимостей
//...
$this->_staticBridgeClassName = $params['staticBridgeClassName'];
}
public function get (int $id): Product {
return new Product($id, 'Вася', $this->_staticBridgeClassName);
}
public function getPhotoUrl (int $productId): string {
return $this->_heavyPhotosService->get($productId)->url;
}
}
$dependencyInjectionContainer->productsManager = new ProductsManager(
$params + [
'staticBridgeClassName' => '\Our\Namespace\ProductsManagerStaticBridge',
]
);
class ProductsManagerStaticBridge {
public function getManager (): ProductsManager {
return $GLOBALS['dependencyInjectionContainer']->productsManager();
}
}
class Product {
private string $_staticBridgeClassName;
private int $_id;
private string $_name;
private string $_photoUrl = '';
public function __construct (int $id, string $name, string $staticBridgeClassName) {
$this->_id = $id;
$this->_name = $name;
$this->_staticBridgeClassName = $staticBridgeClassName;
}
public function getId (): int { return $this->_id; }
public function getName (): string { return $this->_name; }
public function getPhotoUrl (): string {
return ($this->_staticBridgeClassName)::getManager()->getPhotoUrl($this->_id);
}
}- В кэш сохраняются только нужные поля
- Нет необходимости мудрить с методами
__sleep()и__wakeup(). - Любые объекты десериализуются обычным
deserialize(). - Отсутствует жёстко прописанный код подключения к сервису.
- Для юнит-тестов в объекты можно прокидывать тестовые сервисы, указывая другой класс моста.
- Работает со вложенными объектами.
- При какой-либо смене системы мостов нужно будет сбрасывать соответствующие кэши.
- Обращение к глобальному классу - как вынужденная мера для реализации этого простого паттерна.