Skip to content

Latest commit

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Паттерн "Статический мост"

Структура

  1. Сначала в документе описаны задачи
  2. Далее - решения, имеющие те или иные недостатки
  3. Под конец описано решение через паттерн "Статический мост", решающие эти недостатки

Исходная позиция

Есть класс-сервис 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());

Задача 1: получение фото товара

Для того, чтобы получить фото товара, нужно обратиться к какому-то относительно тяжёлому сервису, да и фото нужно далеко не всегда. Поэтому в объекте Product поле $_photoUrl изначально не инициализировано.

Тем не менее, получение фотки должно происходить прозрачно, как и обращение к ID или имени. Так как вся логика работы инкапсулирована внутри ProductsManager, объект Product для подгрузки фото налету должен обратиться к соответствующему методу ProductsManager.

Для этого классы должны быть написаны следующим образом:

  1. ProductsManager в качестве зависимости передаёт в инициализируемый объект самого себя.
  2. ProductsManager реализует метод getPhotoUrl() для получения картинки.
  3. 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);
}
}

Задача 2: сериализация и десериализация объекта Product

Для ускорения быстродействия сайта все полученные товары могут кэшироваться через сериализацию. При сериализации фото товара может быть уже получено из базы, или нет. Соответственно, когда товар получен из кэша, обращение к его методу getPhotoUrl() должно налету подгрузить картинку, обращением к сервису.

Возникающие проблемы

  1. При сериализации в кэш будет попадать и тяжёлый объект сервиса UsersManager, со всеми его зависимостями.
  2. При десериализации у этого сервиса не будут восстанавливаться подключения к ресурсам - соответственно, методы getPhotoUrl() просто не будут работать.

Решение 1: __sleep() плюс __wakeup()

Добавляем метод __sleep(), указывающий только те поля, которые нужно сохранять при сериализации. И метод __wakeup(), восстанавливающий подключение к сервису ProductsManager.

class Product {
publicfunction__sleep (): array {
return ['_id', '_name', '_photoUrl'];
}
publicfunction__wakeup (): array {
$this->_productsManager = $GLOBALS['dependencyInjectionContainer']->productsManager;
}
}

Проблемы

  1. Нужно всегда следить за списком сериализуемых полей в __sleep() - добавляя или удаляя ключи синхронно с изменением полей.
  2. Код получения сервиса из контейнера зависимостей вбит жёстко, что нарушает принцип использования инъекции зависимостей.
  3. Невозможно нормальное внедрение мок-объектов для тестирования

Решение 2: дополнительный код для сериализации/десериализации

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'];
}
}

Проблемы

  1. Та же проблема с необходимостью следить за актуальностью полей в __sleep()
  2. Простая десериализация через unserialize() не работает - нужно использовать вспомогательный метод ProductsManager::deserializeProductsList(). Если в кэше содержится много разновидностей данных, то нужно знать, какие десериализуются через unserialize(), а какие - через специальные методы.

Решение: паттерн "Статический мост"

Переписываем код следующим образом:

  1. Дополнительно к сервисам-объектам, лежащим в контейнере зависимостей, создаём сопутствующие классы-сервисы, доступные глобально. Или один класс-сервис с необходимым количеством методов.
  2. При инициализации сервиса одним из параметров указываем, какое имя класса он должен прокидывать в создаваемые объекты для использования в роли статического моста.
  3. Объект 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);
}
}

Преимущества

  1. В кэш сохраняются только нужные поля
  2. Нет необходимости мудрить с методами __sleep() и __wakeup().
  3. Любые объекты десериализуются обычным deserialize().
  4. Отсутствует жёстко прописанный код подключения к сервису.
  5. Для юнит-тестов в объекты можно прокидывать тестовые сервисы, указывая другой класс моста.
  6. Работает со вложенными объектами.

Недостатки

  1. При какой-либо смене системы мостов нужно будет сбрасывать соответствующие кэши.
  2. Обращение к глобальному классу - как вынужденная мера для реализации этого простого паттерна.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors