Статьи

Полиморфизм подтипа — замена реализации во время выполнения

Сколько людей сомневается в актуальности наследования и полиморфизма в объектно-ориентированном дизайне? Вероятно, очень мало, скорее всего из-за невежества или узкого мышления. Но здесь есть одна маленькая ошибка, которую нельзя не заметить. Хотя понять логику, которая тянет бразды правления Наследством, просто, вещи сложнее, когда дело доходит до того, чтобы глубоко погрузиться в гайки и болты Полиморфизма.

Термин «Полиморфизм» сам по себе пугающий, и его академическое определение страдает от нескольких разрозненных наклонностей, которые усложняют понимание того, что на самом деле скрыто под его капюшоном. Периферийные концепции, такие как параметрический полиморфизм и специальный полиморфизм, обычно реализуемые с помощью переопределения / перегрузки методов, в некоторых языках программирования имеют значительные ниши, но последний случай следует отбросить, когда речь идет о разработке расширяемых систем, способных потреблять конкретные контракты (см. абстракции) без необходимости проверки, принадлежат ли реализации ожидаемому типу.

Короче говоря, большую часть времени любая общая ссылка на полиморфизм в ООП неявно предполагается как способность, предоставляемая системой для определения набора контрактов или интерфейсов, которые, в свою очередь, соблюдаются различными реализациями. Этот тип «канонического» Полиморфизма обычно известен как Полиморфизм подтипов, поскольку разработчики интерфейсов считаются их подтипами, независимо от того, существует ли в действительности фактическая иерархия или нет.

Как и следовало ожидать, понимание природы полиморфизма — это только половина процесса обучения; другая половина, конечно, демонстрирует, как проектировать полиморфные системы, которые можно приспособить для работы в довольно реалистичных ситуациях, не попадая в ловушку демонстрации всего лишь «некоторого изящного дидактического кода» (во многих случаях дешевый эвфемизм для игрушечного кода) ).

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

Определение интерфейсов и реализаций компонента

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

Так как же это сделать, не потея слишком много в процессе разработки? Что ж, первым логическим шагом, который нужно предпринять, было бы … да, определить контракт на сегрегированный кеш, который будет согласован позднее различными реализациями, следовательно, используя преимущества Полиморфизма.

На самом базовом уровне вышеупомянутый контракт выглядит так:

<?php namespace LibraryCache; interface CacheInterface { public function set($id, $data); public function get($id); public function delete($id); public function exists($id); } 

Интерфейс CacheInterface — это скелетный контракт, который абстрагирует поведение универсального элемента кэширования. Имея интерфейс, легко создать несколько конкретных реализаций кэша, соответствующих его контракту.

Поскольку я хочу, чтобы все было лаконично и легко усваивалось, драйверы кеша, которые я установил, будут просто тощим дуэтом: первый использует файловую систему в качестве базового бэкэнда для кэширования / выборки данных, а второй использует расширение APC за кулисами.

Вот реализация файлового кэша:

 <?php namespace LibraryCache; class FileCache implements CacheInterface { const DEFAULT_CACHE_DIRECTORY = 'Cache/'; private $cacheDir; public function __construct($cacheDir = self::DEFAULT_CACHE_DIRECTORY) { $this->setCacheDir($cacheDir); } public function setCacheDir($cacheDir) { if (!is_dir($cacheDir)) { if (!mkdir($cacheDir, 0644)) { throw InvalidArgumentException('The cache directory is invalid.'); } } $this->cacheDir = $cacheDir; return $this; } public function set($id, $data) { if (!file_put_contents($this->cacheDir . $id, serialize($data), LOCK_EX)) { throw new RuntimeException("Unable to cache the data with ID '$id'."); } return $this; } public function get($id) { if (!$data = unserialize(@file_get_contents($this->cacheDir . $id, false))) { throw new RuntimeException("Unable to get the data with ID '$id'."); } return $data; } public function delete($id) { if (!@unlink($this->cacheDir . $id)) { throw new RuntimeException("Unable to delete the data with ID '$id'."); } return $this; } public function exists($id) { return file_exists($this->cacheDir . $id); } } 

Управляющая логика класса FileCache должна быть проста для понимания. Безусловно, наиболее важным здесь является то, что он демонстрирует аккуратное полиморфное поведение, поскольку он является верным реализатором более раннего CacheInterface .

Хотя эта способность приятна и очаровательна, сама по себе это не то, что я бы похвалил, если учесть, что цель здесь — создать компонент кэша, способный переключать серверы во время выполнения. Давайте приложим дополнительные усилия от имени поучительной причины и CacheInterface в жизнь еще одного тонкого разработчика CacheInterface .

Реализация ниже придерживается контракта интерфейса, но на этот раз, используя методы, связанные с расширением APC:

 <?php namespace LibraryCache; class ApcCache implements CacheInterface { public function set($id, $data, $lifeTime = 0) { if (!apc_store($id, $data, (int) $lifeTime)) { throw new RuntimeException("Unable to cache the data with ID '$id'."); } } public function get($id) { if (!$data = apc_fetch($id)) { throw new RuntimeException("Unable to get the data with ID '$id'."); } return $data; } public function delete($id) { if (!apc_delete($id)) { throw new RuntimeException("Unable to delete the data with ID '$id'."); } } public function exists($id) { return apc_exists($id); } } 

Класс ApcCache — не самая лучшая оболочка APC, которую вы видите в своей карьере, он упаковывает все функции, необходимые для сохранения, извлечения и удаления данных из памяти.

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

Однако я должен подчеркнуть, что фактический полиморфизм подтипа был достигнут путем реализации контракта, определенного через конструкцию интерфейса, что является довольно повсеместным подходом. Однако ничто не должно мешать вам быть менее ортодоксальным и получать те же результаты, переключая интерфейс, объявленный как набор абстрактных методов, размещаемых внутри абстрактного класса. Если вы чувствуете себя предприимчивым и хотите идти по этому пути, контракт и соответствующие реализации могут быть реорганизованы следующим образом:

 <?php namespace LibraryCache; abstract class AbstractCache { abstract public function set($id, $data); abstract public function get($id); abstract public function delete($id); abstract public function exists($id); } 
 <?php namespace LibraryCache; class FileCache extends AbstractCache { // the same implementation goes here } 
 <?php namespace LibraryCache; class ApcCache extends AbstractCache { // the same implementation goes here } 

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

К этому моменту я мог бы опустить занавес, написать несколько причудливых заключительных комментариев, болтать о наших впечатляющих навыках кодирования и хвастаться гибкостью или компонентом кэширования, но это будет плохая услуга. Полиморфизм раскрывает свой наиболее соблазнительный аспект, когда существует некоторый клиентский код, способный потреблять несколько реализаций, не проверяя, являются ли они типами чего-либо, если они придерживаются ожидаемого контракта. Итак, давайте раскроем этот аспект, подключив компонент кеша к базовому клиентскому классу представления, что позволит нам выполнять аккуратное HTML-кэширование без особых хлопот.

Использование драйверов кеша

Кэширование вывода HTML с помощью нашего примера модуля кеша настолько банально, что я сохраню любое подробное объяснение в другой раз. Весь процесс кэширования может быть сведен к наивному классу представления, подобному этому:

 <?php namespace LibraryView; interface ViewInterface { public function setTemplate($template); public function __set($field, $value); public function __get($field); public function render(); } 
 <?php namespace LibraryView; use LibraryCacheCacheInterface; class View implements ViewInterface { const DEFAULT_TEMPLATE = 'default'; private $template; private $fields = array(); private $cache; public function __construct(CacheInterface $cache, $template = self::DEFAULT_TEMPLATE) { $this->cache = $cache; $this->setTemplate($template); } public function setTemplate($template) { $template = $template . '.php'; if (!is_file($template) || !is_readable($template)) { throw new InvalidArgumentException( "The template '$template' is invalid."); } $this->template = $template; return $this; } public function __set($name, $value) { $this->fields[$name] = $value; return $this; } public function __get($name) { if (!isset($this->fields[$name])) { throw new InvalidArgumentException( "Unable to get the field '$field'."); } return $this->fields[$name]; } public function render() { try { if (!$this->cache->exists($this->template)) { extract($this->fields); ob_start(); include $this->template; $this->cache->set($this->template, ob_get_clean()); } return $this->cache->get($this->template); } catch (RuntimeException $e) { throw new Exception($e->getMessage()); } } } 

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

Допустим, шаблон представления по умолчанию имеет следующую структуру:

 <!doctype html> <html> <head> <meta charset="utf-8"> <title>The Default Template</title> </head> <body> <header> <h1>You are viewing the default page!</h1> <?php echo $this->header;?> </header> <section> <?php echo $this->body;?> </section> <footer> <?php echo $this->footer;?> </footer> </body> </html> 

Теперь давайте повеселимся и ApcCache представление с экземпляром класса ApcCache :

 <?php use LibraryLoaderAutoloader, LibraryCacheFileCache, LibraryCacheApcCache, LibraryViewView; require_once __DIR__ . '/Library/Loader/Autoloader.php'; $autoloader = new Autoloader; $autoloader->register(); $view = new View(new ApcCache()); $view->header = 'This is my fancy header section'; $view->body = 'This is my fancy body section'; $view->footer = 'This is my fancy footer section'; echo $view->render(); 

Довольно мило, правда? Но подожди минутку! Я был настолько захвачен моментом, что забыл упомянуть, что приведенный выше фрагмент будет взорван в любой системе, где расширение APC не установлено (непослушные системные администраторы!). Означает ли это, что красиво созданный модуль кэша больше не пригоден для повторного использования?

Именно здесь на помощь приходит файловый драйвер, который можно вставить в клиентский код, не получив от него ни единого соответствия:

 <?php $view = new View(new FileCache()); 

Вышеуказанный однострочник явно заявляет, что представление будет кэшировать свои выходные данные, используя файловую систему вместо разделяемой памяти. Такое переключение внутренних частей кеша на лету показывает в двух словах, почему полиморфизм так важен, когда речь идет о разработке модулей с высокой степенью развязки. Это позволяет нам легко переназначать вещи во время выполнения, не допуская появления артефактов, связанных с хрупкостью / жесткостью, в другие части нашей системы.

Заключительные замечания

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

Полиморфные системы по своей природе гораздо более ортогональны, их легче расширять, и они гораздо менее уязвимы для нарушения центральных парадигм, таких как принцип Open / Closed и мудрая мантра «Программирование на интерфейсах». Хотя наш кеш-модуль довольно примитивен, он является ярким примером этих достоинств в действии.

Если вы еще не провели рефакторинг своих приложений, чтобы они могли воспользоваться преимуществами, которые принес Полиморфизм, то вам лучше поторопиться, потому что вы теряете джекпот!

Изображение через Fotolia