- Сначала в документе описаны задачи
- Далее - решения, имеющие те или иные недостатки
- Под конец описано решение через паттерн "Статический мост", решающие эти недостатки
Есть класс-сервис ProductsManager, содержащий в себе всю логику работу с товарами.
class ProductsManager {
/** * Возвращает данные о товаре. */publicfunctionget (int$id): Product {
returnnewProduct($id, 'Кресло');
}
}
class Product {
privateint$_id;
privatestring$_name;
privatestring$_photoUrl = '';
publicfunction__construct ($id, $name) {
$this->_id = $id;
$this->_name = $name;
}
publicfunctiongetId (): int { return$this->_id; }
publicfunctiongetName (): string { return$this->_name; }
publicfunctiongetPhotoUrl (): 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 {
publicfunctionget (int$id): Product {
returnnewProduct($id, 'Вася', $this);
}
publicfunctiongetPhotoUrl (int$productId): string {
return$this->_heavyPhotosService->get($productId)->url;
}
}
class Product {
privateProductsManager$_productsManager;
privateint$_id;
privatestring$_name;
privatestring$_photoUrl = '';
publicfunction__construct (int$id, string$name, ProductsManager$productsManager) {
$this->_id = $id;
$this->_name = $name;
$this->_productsManager = $productsManager;
}
publicfunctiongetId (): int { return$this->_id; }
publicfunctiongetName (): string { return$this->_name; }
publicfunctiongetPhotoUrl (): string {
return$this->_productsManager->getPhotoUrl($this->_id);
}
}Для ускорения быстродействия сайта все полученные товары могут кэшироваться через сериализацию. При сериализации фото товара может быть уже получено из базы, или нет. Соответственно, когда товар получен из кэша, обращение к его методу getPhotoUrl() должно налету подгрузить картинку, обращением к сервису.
- При сериализации в кэш будет попадать и тяжёлый объект сервиса
UsersManager, со всеми его зависимостями. - При десериализации у этого сервиса не будут восстанавливаться подключения к ресурсам - соответственно, методы
getPhotoUrl()просто не будут работать.
Добавляем метод __sleep(), указывающий только те поля, которые нужно сохранять при сериализации. И метод __wakeup(), восстанавливающий подключение к сервису ProductsManager.
class Product {
publicfunction__sleep (): array {
return ['_id', '_name', '_photoUrl'];
}
publicfunction__wakeup (): array {
$this->_productsManager = $GLOBALS['dependencyInjectionContainer']->productsManager;
}
}- Нужно всегда следить за списком сериализуемых полей в
__sleep()- добавляя или удаляя ключи синхронно с изменением полей. - Код получения сервиса из контейнера зависимостей вбит жёстко, что нарушает принцип использования инъекции зависимостей.
- Невозможно нормальное внедрение мок-объектов для тестирования
class ProductsManager {
publicfunctiondeserializeProductsList (array$productsCacheString): array {
$productsList = unserialize($productsCacheString);
foreach ($productsListas$product) {
$product->setManager($this);
}
}
}
class Product {
publicfunctionsetManager (ProductsManager$productsManager) {
$this->_productsManager = $productsManager;
}
publicfunction__sleep (): array {
return ['_id', '_name', '_photoUrl'];
}
}- Та же проблема с необходимостью следить за актуальностью полей в
__sleep() - Простая десериализация через
unserialize()не работает - нужно использовать вспомогательный методProductsManager::deserializeProductsList(). Если в кэше содержится много разновидностей данных, то нужно знать, какие десериализуются черезunserialize(), а какие - через специальные методы.
Переписываем код следующим образом:
- Дополнительно к сервисам-объектам, лежащим в контейнере зависимостей, создаём сопутствующие классы-сервисы, доступные глобально. Или один класс-сервис с необходимым количеством методов.
- При инициализации сервиса одним из параметров указываем, какое имя класса он должен прокидывать в создаваемые объекты для использования в роли статического моста.
- Объект
Productобращается к сервису через промежуточный глобально доступный статический класс, который был указан в параметрах при инициализации объекта.
class ProductsManager {
private$_staticBridgeClassName = '';
publicfunction__construct (array$params) {
//...//Сохранение настроек и зависимостей//...$this->_staticBridgeClassName = $params['staticBridgeClassName'];
}
publicfunctionget (int$id): Product {
returnnewProduct($id, 'Вася', $this->_staticBridgeClassName);
}
publicfunctiongetPhotoUrl (int$productId): string {
return$this->_heavyPhotosService->get($productId)->url;
}
}
$dependencyInjectionContainer->productsManager = newProductsManager(
$params + [
'staticBridgeClassName' => '\Our\Namespace\ProductsManagerStaticBridge',
]
);
class ProductsManagerStaticBridge {
publicfunctiongetManager (): ProductsManager {
return$GLOBALS['dependencyInjectionContainer']->productsManager();
}
}
class Product {
privatestring$_staticBridgeClassName;
privateint$_id;
privatestring$_name;
privatestring$_photoUrl = '';
publicfunction__construct (int$id, string$name, string$staticBridgeClassName) {
$this->_id = $id;
$this->_name = $name;
$this->_staticBridgeClassName = $staticBridgeClassName;
}
publicfunctiongetId (): int { return$this->_id; }
publicfunctiongetName (): string { return$this->_name; }
publicfunctiongetPhotoUrl (): string {
return ($this->_staticBridgeClassName)::getManager()->getPhotoUrl($this->_id);
}
}- В кэш сохраняются только нужные поля
- Нет необходимости мудрить с методами
__sleep()и__wakeup(). - Любые объекты десериализуются обычным
deserialize(). - Отсутствует жёстко прописанный код подключения к сервису.
- Для юнит-тестов в объекты можно прокидывать тестовые сервисы, указывая другой класс моста.
- Работает со вложенными объектами.
- При какой-либо смене системы мостов нужно будет сбрасывать соответствующие кэши.
- Обращение к глобальному классу - как вынужденная мера для реализации этого простого паттерна.