- Сначала в документе описаны задачи
- Далее - решения, имеющие те или иные недостатки
- Под конец описано решение через паттерн "Статический мост", решающие эти недостатки
Есть класс-сервис 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()
. - Отсутствует жёстко прописанный код подключения к сервису.
- Для юнит-тестов в объекты можно прокидывать тестовые сервисы, указывая другой класс моста.
- Работает со вложенными объектами.
- При какой-либо смене системы мостов нужно будет сбрасывать соответствующие кэши.
- Обращение к глобальному классу - как вынужденная мера для реализации этого простого паттерна.