Если бы мне когда-либо пришлось принять специальное «соломоновое» решение относительно актуальности каждого принципа SOLID , я бы просто осмелился сказать, что принцип инверсии зависимости (DIP) является самым недооцененным из всех.
В то время как некоторые центральные концепции в области объектно-ориентированного проектирования, как правило, поначалу труднее переварить, такие как разделение интересов и переключение реализации, с другой стороны, более интуитивно понятные и распутанные парадигмы проще, например, программирование для интерфейсов. К сожалению, формальное определение DIP окружено обоюдоострым проклятием / благословением, которое часто заставляет программистов приукрашивать его, поскольку во многих случаях существует неявное предположение, что этот принцип является не чем иным, как причудливым выражением для вышеупомянутой команды «программирование для интерфейсов». :
- Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
На первый взгляд вышеприведенные высказывания кажутся очевидными. Учитывая, что в этот момент никто не согласится с тем, что системы, построенные на сильной зависимости от конкретных реализаций, являются зловещим признаком плохого проектирования, переключение между несколькими абстракциями имеет смысл. Таким образом, это привело бы нас к началу, думая, что основной задачей DIP является программирование интерфейсов.
На самом деле, отделение интерфейса от реализации — это всего лишь полусумный подход, когда дело доходит до выполнения требований принципа. Недостающая часть достигает реального процесса инверсии. Конечно, возникает вопрос: инверсия чего?
В традиционном смысле системы всегда были разработаны для работы с компонентами высокого уровня, будь то классы или процедурные процедуры, в зависимости от компонентов низкого уровня (подробности). Например, модуль журналирования может сильно зависеть от набора конкретных регистраторов, которые фактически регистрируют информацию в системе. Поэтому неудивительно, что эта схема будет шумно рябь побочных эффектов вверх к верхним уровням всякий раз, когда изменяется протокол, принадлежащий регистраторам, даже если протокол был удален.
Внедрение DIP, тем не менее, помогает смягчить эту рябь, позволяя вместо этого протоколирующему модулю владеть протоколом, что инвертирует общий поток зависимостей. После инверсии регистраторы должны строго придерживаться протокола, следовательно, соответствующим образом изменяя и приспосабливаясь к его колебаниям, если когда-либо произойдет дальше.
В двух словах, это показывает, что DIP немного сложнее, чем просто полагаться на кучу преимуществ, которые дает стандартная развязка реализации интерфейса. Да, речь идет о том, чтобы сделать и высокоуровневые, и низкоуровневые модули зависимыми от абстракций, но в то же время высокоуровневые модули должны владеть этими абстракциями — тонкая, но важная деталь, которую просто так не упустить.
Как и следовало ожидать, одним из способов, который поможет вам легче понять, что на самом деле находится под эгидой DIP, является несколько практических примеров кода. Поэтому в этой статье я настрою несколько примеров, чтобы вы могли узнать, как максимально эффективно использовать этот принцип SOLID при разработке приложений PHP.
Разработка наивного модуля хранения (недостающее «Я» в DIP)
Многие разработчики, особенно те, кто не любит холодную воду объектно-ориентированного PHP, склонны рассматривать DIP и другие принципы SOLID в качестве жесткой догмы, которая сильно борется с присущим языку прагматизмом. Я могу понять такое мышление, поскольку довольно трудно найти прагматичные примеры PHP в дикой природе, которые демонстрируют реальные преимущества принципа. Не то чтобы я хотел проявить себя как просвещенный программист (этот костюм просто не подходит), но было бы полезно стремиться к доброму делу и продемонстрировать с практической точки зрения, как реализовать DIP в реалистичном использовании. кейс.
Для начала рассмотрим реализацию простого модуля хранения файлов. Модуль отвечает за чтение и запись данных в указанный целевой файл. На очень минималистском уровне рассматриваемый модуль может быть написан так:
<?php namespace LibraryEncoderStrategy; class Serializer implements Serializable { protected $unserializeCallback; public function __construct($unserializeCallback = false) { $this->unserializeCallback = (boolean) $unserializeCallback; } public function getUnserializeCallback() { return $this->unserializeCallback; } public function serialize($data) { if (is_resource($data)) { throw new InvalidArgumentException( "PHP resources are not serializable."); } if (($data = serialize($data)) === false) { throw new RuntimeException( "Unable to serialize the supplied data."); } return $data; } public function unserialize($data) { if (!is_string($data) || empty($data)) { throw new InvalidArgumentException( "The data to be decoded must be a non-empty string."); } if ($this->unserializeCallback) { $callback = ini_get("unserialize_callback_func"); if (!function_exists($callback)) { throw new BadFunctionCallException( "The php.ini unserialize callback function is invalid."); } } if (($data = @unserialize($data)) === false) { throw new RuntimeException( "Unable to unserialize the supplied data."); } return $data; } }
<?php namespace LibraryFile; class FileStorage { const DEFAULT_STORAGE_FILE = "default.dat"; protected $serializer; protected $file; public function __construct(Serializable $serializer, $file = self::DEFAULT_STORAGE_FILE) { $this->serializer = $serializer; $this->setFile($file); } public function getSerializer() { return $this->serializer; } public function setFile($file) { if (!is_file($file) || !is_readable($file)) { throw new InvalidArgumentException( "The supplied file is not readable or writable."); } $this->file = $file; return $this; } public function getFile() { return $this->file; } public function resetFile() { $this->file = self::DEFAULT_STORAGE_FILE; return $this; } public function write($data) { try { return file_put_contents($this->file, $this->serializer->serialize($data)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } public function read() { try { return $this->serializer->unserialize( @file_get_contents($this->file)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } }
Модуль представляет собой довольно наивную структуру, состоящую из нескольких основных компонентов. Первый класс читает и записывает данные в файловую систему, а второй — это упрощенный сериализатор PHP, используемый для генерации хранимого представления данных.
Эти примерные компоненты аккуратно выполняют свою работу изолированно и могут быть соединены для совместной работы следующим образом:
<?php use LibraryLoaderAutoloader, LibraryEncoderStrategySerializer, LibraryFileFileStorage; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $fileStorage = new FileStorage(new Serializer); $fileStorage->write(new stdClass()); print_r($fileStorage->read()); $fileStorage->write(array("This", "is", "a", "sample", "array")); print_r($fileStorage->read()); $fileStorage->write("This is a sample string."); echo $fileStorage->read();
На первый взгляд, модуль демонстрирует довольно приличное поведение, учитывая, что его функциональные возможности позволяют без особых хлопот сохранять и извлекать разнообразные данные из файловой системы. Кроме того, класс FileStorage
внедряет в конструктор интерфейс Serializable
, таким образом, в зависимости от гибкости, предоставляемой абстракцией, а не жесткой конкретной реализацией. С этим множеством преимуществ, ярко сияющих сами по себе, что может быть не так с модулем?
Как обычно, мелкие первые впечатления могут быть хитрыми и размытыми. Если посмотреть немного ближе, то не только ясно, что FileStorage
действительно зависит от сериализатора, но и из-за этой тесной зависимости хранение и извлечение данных из целевого файла ограничено использованием только собственного механизма сериализации PHP. Что произойдет, если данные должны быть переданы во внешнюю службу в виде XML или JSON? Тщательно разработанный модуль больше не пригоден для повторного использования. Грустно, но правда!
Ситуация поднимает несколько интересных моментов. Прежде всего, FileStorage
прежнему сильно зависит от низкоуровневого Serializer
, даже когда протокол, который позволяет им взаимодействовать друг с другом, изолирован от реализации. Во-вторых, уровень универсальности, представляемый рассматриваемым протоколом, является очень ограничительным, ограниченным просто заменой одного сериализатора на другой. В этом случае не только зависимость от абстракции является обманчивым восприятием, но реальный процесс инверсии, поддерживаемый DIP, никогда не достигается.
Можно реорганизовать некоторые части файлового модуля, чтобы он точно соответствовал требованиям DIP. При этом класс FileStorage
получит право собственности на протокол, используемый для хранения и извлечения файловых данных, что избавит от зависимости от сериализатора более низкого уровня и даст вам возможность переключаться между несколькими стратегиями хранения во время выполнения. При этом вы получите большую гибкость бесплатно. Поэтому давайте продолжим и посмотрим, как превратить модуль хранения файлов в настоящую DIP-совместимую структуру.
Инвертирование владения протоколом и развязывание интерфейса от реализации (получение максимальной отдачи от DIP)
Хотя по сути нет множества вариантов, тем не менее, есть несколько подходов, которые можно использовать для эффективной инверсии владения протоколом между классом FileStorage
и его низкоуровневым соавтором, сохраняя при этом протокол абстрактным. Тем не менее, есть один, который оказывается довольно интуитивным, поскольку он опирается на естественную инкапсуляцию, предоставляемую прямо из коробки пространствами имен PHP.
Чтобы перевести эту несколько нематериальную концепцию в конкретный код, первое изменение, которое необходимо сделать в модуле, — это определить более упрощенный протокол для сохранения и извлечения данных файла, чтобы было легко манипулировать им с помощью форматов, отличных от только сериализации PHP.
Тонкий, отдельный интерфейс, подобный показанному ниже, делает элегантность и простоту:
<?php namespace LibraryFile; interface EncoderInterface { public function encode($data); public function decode($data); }
Существование EncoderInterface
, кажется, не оказывает глубокого влияния на общий дизайн файлового модуля, но делает намного больше, чем обещает за чистую монету. Первое улучшение — это определение универсального протокола для кодирования и декодирования данных. Второе, что не менее важно, чем первое, заключается в том, что теперь владелец протокола принадлежит классу FileStorage
поскольку интерфейс живет и дышит в пространстве имен класса. Проще говоря, нам удалось сделать все еще неопределенными низкоуровневые кодеры / декодеры зависимыми от высокоуровневого FileStorage
просто написав правильно FileStorage
интерфейс имен. Короче говоря, это фактический процесс инверсии, который DIP продвигает за своей академической вуалью.
Естественно, инверсия была бы неуклюжей попыткой на полпути, если FileStorage
класс FileStorage
не был изменен, чтобы внедрить реализатор предыдущего интерфейса, поэтому вот измененная версия:
<?php namespace LibraryFile; class FileStorage { const DEFAULT_STORAGE_FILE = "default.dat"; protected $encoder; protected $file; public function __construct(EncoderInterface $encoder, $file = self::DEFAULT_STORAGE_FILE) { $this->encoder = $encoder; $this->setFile($file); } public function getEncoder() { return $this->encoder; } public function setFile($file) { if (!is_file($file) || !is_readable($file)) { throw new InvalidArgumentException( "The supplied file is not readable or writable."); } $this->file = $file; return $this; } public function getFile() { return $this->file; } public function resetFile() { $this->file = self::DEFAULT_STORAGE_FILE; return $this; } public function write($data) { try { return file_put_contents($this->file, $this->encoder->encode($data)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } public function read() { try { return $this->encoder->decode( @file_get_contents($this->file)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } }
Теперь, когда FileStorage
явно объявляет в конструкторе право собственности на протокол кодирования / декодирования, остается только создать набор конкретных низкоуровневых кодеров / декодеров, что позволяет обрабатывать данные файлов в нескольких форматах.
Первый из этих компонентов — это просто переработанная реализация сериализатора PHP, написанная ранее:
<?php namespace LibraryEncoderStrategy; class Serializer implements LibraryFileEncoderInterface { protected $unserializeCallback; public function __construct($unserializeCallback = false) { $this->unserializeCallback = (boolean) $unserializeCallback; } public function getUnserializeCallback() { return $this->unserializeCallback; } public function encode($data) { if (is_resource($data)) { throw new InvalidArgumentException( "PHP resources are not serializable."); } if (($data = serialize($data)) === false) { throw new RuntimeException( "Unable to serialize the supplied data."); } return $data; } public function decode($data) { if (!is_string($data) || empty($data)) { throw new InvalidArgumentException( "The data to be decoded must be a non-empty string."); } if ($this->unserializeCallback) { $callback = ini_get("unserialize_callback_func"); if (!function_exists($callback)) { throw new BadFunctionCallException( "The php.ini unserialize callback function is invalid."); } } if (($data = @unserialize($data)) === false) { throw new RuntimeException("Unable to unserialize the supplied data."); } return $data; } }
Конечно, было бы излишним анализировать логику Serializer
. Несмотря на то, что стоит отметить, что теперь это зависит не только от более разрешающей абстракции кодирования / декодирования, но и право владения абстракции явно раскрывается на уровне пространства имен.
Аналогичным образом, мы могли бы пойти еще дальше и начать писать еще несколько кодеров, чтобы подчеркнуть преимущества DIP. С учетом сказанного, вот как можно написать еще один дополнительный низкоуровневый компонент:
<?php namespace LibraryEncoderStrategy; class JsonEncoder implements LibraryFileEncoderInterface { public function encode($data) { if (is_resource($data)) { throw new InvalidArgumentException( "PHP resources cannot be JSON-encoded."); } if (($data = json_encode($data)) === false) { throw new RuntimeException( "Unable to JSON-encode the supplied data."); } return $data; } public function decode($data) { if (!is_string($data) || empty($data)) { throw new InvalidArgumentException( "The data to be decoded must be a non-empty string."); } if (($data = json_decode($data)) === false) { throw new RuntimeException( "Unable to JSON-decode the supplied data."); } return $data; } }
Как и ожидалось, основная логика, стоящая за дополнительными кодировщиками, обычно напоминает логику первого сериализатора PHP, за исключением любых очевидных улучшений и вариантов. Кроме того, компоненты соответствуют требованиям, предъявляемым DIP, поэтому придерживаются протокола кодирования / декодирования, определенного в пространстве имен FileStorage
.
Поскольку в файловом модуле находятся как высокоуровневые, так и низкоуровневые компоненты, зависящие от абстракции, и кодеры, демонстрирующие явную зависимость от класса хранилища файлов, мы можем смело утверждать, что модуль ведет себя как настоящая DIP-совместимая структура.
Кроме того, в следующем примере показано, как собрать компоненты вместе:
<?php use LibraryLoaderAutoloader, LibraryEncoderStrategyJsonEncoder, LibraryEncoderStrategySerializer, LibraryFileFileStorage; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $fileStorage = new FileStorage(new JsonEncoder); $fileStorage->write(new stdClass()); print_r($fileStorage->read()); $fileStorage = new FileStorage(new Serializer); $fileStorage->write(array("This", "is", "a", "sample", "array")); print_r($fileStorage->read());
Помимо некоторых наивных тонкостей, которые модуль раскрывает клиентскому коду, полезно высказаться и продемонстрировать довольно поучительно, почему предикаты DIP на самом деле более обширны, чем старая парадигма «программирование на интерфейс». Он описывает и явно предписывает инверсию зависимостей, и, как таковой, он должен выполняться с помощью различных механизмов. Пространства имен PHP — отличный способ сделать это без излишней нагрузки, хотя традиционные подходы, такие как определение хорошо структурированных, выразительных макетов приложений, могут дать те же результаты.
Заключительные замечания
В целом, мнения, основанные на субъективной экспертизе, имеют тенденцию быть довольно предвзятыми, и, конечно, те, которые я высказал в начале этой статьи, не являются исключением. Однако существует небольшая тенденция упускать из виду принцип инверсии зависимостей в пользу его более замысловатых аналогов SOLID, так как его довольно просто неправильно понять как синоним зависимости от абстракций. Кроме того, некоторые программисты склонны реагировать интуитивно и воспринимать термин «инверсия» как сокращенное выражение для инверсии контроля , которое, хотя и связано друг с другом, в конечном итоге является неверной концепцией.
Теперь, когда вы знаете, что на самом деле скрыто под DIP, обязательно воспользуйтесь всеми преимуществами, которые он приносит на стол, что наверняка сделает ваши приложения намного менее уязвимыми для проблем хрупкости и жесткости, которые могут в конечном итоге возникнуть по мере их роста. через некоторое время.
Изображение через kentoh / Shutterstock