Программирование — это сбалансированное сочетание искусства (иногда эвфемизм для импровизации) и набор хорошо зарекомендовавших себя эвристик, используемых для решения определенных проблем и их достойного решения. Мало кто не согласится с тем, что художественная сторона на сегодняшний день является самой трудной для полировки и дистилляции. С другой стороны, укрощение сил, лежащих в основе эвристики, имеет основополагающее значение для возможности разработки программного обеспечения, основанного на хорошем дизайне.
С таким большим количеством эвристик, утверждающих, как и почему программные системы должны цепляться за определенный подход, довольно обидно не видеть их более широкую реализацию в мире PHP. Например, Закон Деметры, вероятно, является одним из самых недооцененных в области языка.
По сути, в законе мантра «говорите с вашими самыми близкими друзьями», похоже, все еще находится в довольно незрелом состоянии в PHP, что способствует ухудшению общего качества нескольких объектно-ориентированных баз кода. Некоторые популярные структуры активно продвигают его вперед, пытаясь быть более приверженными законам. Бросать вину за нарушение Закона Деметры бессмысленно, так как лучший способ смягчить такие нарушения — просто прагматично и понять, что на самом деле скрыто под законом, и, следовательно, сознательно применять его при написании объектно-ориентированного кода.
В попытке присоединиться к справедливому делу и немного углубиться в закон с практической точки зрения, в следующих нескольких строках я продемонстрирую на нескольких практических примерах, почему что-то такое простое, как соблюдение принципов закона может стать настоящим стимулом при разработке слабосвязанных программных модулей.
Знать слишком много — нехорошо
Часто называемые Принципом Наименьшего Знания, правила, продвигаемые Законом Деметры, легко усваиваются. Проще говоря, и предполагая, что у вас есть красиво созданный класс, который реализует данный метод, данный метод должен быть ограничен для вызова других методов, которые принадлежат следующим объектам:
- Экземпляр исходного класса метода.
- Объекты, которые являются аргументами целевого метода.
- Объекты, созданные целевым методом.
- Объекты, которые являются зависимостями исходного класса метода.
- Глобальные объекты (ой!), К которым может обращаться исходный класс в целевом методе.
Хотя этот список и не является формальным (если он немного более формален, ознакомьтесь с Википедией ), пункты довольно легко понять.
В традиционном дизайне тот факт, что объект знает слишком много о другом (и это подразумевает знание того, как получить доступ к третьему объекту), считается ошибочным, поскольку существуют ситуации, когда объект вынужден без необходимости проходить сверху вниз неуклюжего посредника, чтобы найти фактические зависимости, необходимые для правильной работы. По понятной причине это серьезный недостаток дизайна. Вызывающая сторона обладает довольно обширными и подробными знаниями о внутренней структуре посредника, даже если к ней обращаются через несколько получателей.
Более того, использование промежуточного объекта для доступа к объекту, требуемому вызывающей стороной, делает заявление самостоятельно. В конце концов, зачем использовать такой запутанный путь для получения зависимости или вызывать один из его методов, если тот же результат может быть достигнут путем непосредственного введения зависимости? Процесс не имеет никакого смысла вообще.
Допустим, нам нужно создать модуль хранения файлов, который использует внутренне полиморфный кодер для извлечения и сохранения данных в заданный целевой файл. Если бы мы были намеренно неаккуратными и подключили модуль к вводимому локатору службы, его реализация выглядела бы так:
<?php namespace LibraryFile; use LibraryDependencyInjectionServiceLocatorInterface; class FileStorage { const DEFAULT_STORAGE_FILE = "data.dat"; private $locator; private $file; public function __construct(ServiceLocatorInterface $locator, $file = self::DEFAULT_STORAGE_FILE) { $this->locator = $locator; $this->setFile($file); } public function setFile($file) { if (!is_readable($file) || !is_writable($file)) { throw new InvalidArgumentException( "The target file is invalid."); } $this->file = $file; return $this; } public function write($data) { try { return file_put_contents($this->file, $this->locator->get("encoder")->encode($data), LOCK_EX); } catch (Exception $e) { throw new $e( "Error writing data to the target file: " . $e->getMessage()); } } public function read() { try { return $this->locator->get("encoder")->decode( @file_get_contents($this->file)); } catch(Exception $e) { throw new $e( "Error reading data from the target file: " . $e->getMessage()); } } }
Оставляя в стороне некоторые нерелевантные детали реализации, основное внимание уделяется конструктору класса FileStorage
и его FileStorage
write()
и read()
. Класс внедряет экземпляр все еще неопределенного локатора службы, который позже используется для получения зависимости (вышеупомянутый кодировщик) для извлечения и сохранения данных в целевом файле.
Это типичное нарушение закона Деметры, учитывая, что класс сначала проходит через локатор и, в свою очередь, достигает энкодера. Вызывающий FileStorage
слишком много знает о внутренностях локатора, в том числе о том, как получить доступ к кодировщику, что, безусловно, не является способностью, о которой я бы хотел петь. Это артефакт, изначально связанный с природой сервис-локаторов (и поэтому некоторые видят в них анти-паттерн) или любой другой вид статических или динамических реестров, на что я указывал ранее .
Чтобы иметь более общее представление о проблеме, давайте проверим реализацию локатора:
<?php namespace LibraryDependencyInjection; interface ServiceLocatorInterface { public function set($name, $service); public function get($name); public function exists($name); public function remove($name); public function clear(); }
<?php namespace LibraryDependencyInjection; class ServiceLocator implements ServiceLocatorInterface { private $services = []; public function set($name, $service) { if (!is_object($service)) { throw new InvalidArgumentException( "Only objects can register with the locator."); } if (!in_array($service, $this->services, true)) { $this->services[$name] = $service; } return $this; } public function get($name) { if (!$this->exists($name)) { throw new InvalidArgumentException( "The requested service is not registered."); } return $this->services[$name]; } public function exists($name) { return isset($this->services[$name]); } public function remove($name) { if (!$this->exists($name)) { throw new InvalidArgumentException( "The requested service is not registered."); } unset($this->services[$name]); return $this; } public function clear() { $this->services = []; return $this; } }
с<?php namespace LibraryDependencyInjection; class ServiceLocator implements ServiceLocatorInterface { private $services = []; public function set($name, $service) { if (!is_object($service)) { throw new InvalidArgumentException( "Only objects can register with the locator."); } if (!in_array($service, $this->services, true)) { $this->services[$name] = $service; } return $this; } public function get($name) { if (!$this->exists($name)) { throw new InvalidArgumentException( "The requested service is not registered."); } return $this->services[$name]; } public function exists($name) { return isset($this->services[$name]); } public function remove($name) { if (!$this->exists($name)) { throw new InvalidArgumentException( "The requested service is not registered."); } unset($this->services[$name]); return $this; } public function clear() { $this->services = []; return $this; } }
В этом случае я реализовал локатор в виде простого динамического реестра без дополнительных наворотов, поэтому за ним легко следить. Вы можете украсить его дополнительной функциональностью, если у вас настроение.
Последнее, что мы должны сделать, — это создать хотя бы одну конкретную реализацию соответствующего кодера, чтобы мы могли заставить работать класс хранения файлов. Этот класс должен хорошо справиться с задачей:
<?php namespace LibraryEncoder; interface EncoderInterface { public function encode($data); public function decode($data); }
<?php namespace LibraryEncoder; class Serializer implements EncoderInterface { 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 data."); } return $data; } public function decode($data) { if (!is_string($data)|| empty($data)) { throw new InvalidArgumentException( "The data to be unserialized must be a non-empty string."); } if (($data = @unserialize($data)) === false) { throw new RuntimeException( "Unable to unserialize the data."); } return $data; } }
С установленным кодировщиком, теперь давайте начнем работать, используя все примеры классов вместе:
<?php use LibraryLoaderAutoloader, LibraryEncoderSerializer, LibraryDependencyInjectionServiceLocator, LibraryFileFileStorage; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader(); $autoloader->register(); $locator = new ServiceLocator(); $locator->set("encoder", new Serializer()); $fileStorage = new FileStorage($locator); $fileStorage->write(["This", "is", "my", "sample", "array"]); print_r($fileStorage->read());
Нарушение закона в этом случае является довольно скрытой проблемой, которую трудно отследить с поверхности, за исключением использования мутатора локатора, который предполагает, что в некоторый момент кодер будет доступен и использован в той или иной форме экземпляром FileStorage
. Несмотря на это, мы знаем, что нарушение просто скрыто от внешнего мира, факт, который не только раскрывает слишком много информации о структуре локатора, но и без необходимости FileStorage
класс FileStorage
с самим локатором.
Просто придерживаясь правил закона и избавившись от локатора, мы FileStorage
бы связь, и в то же время предоставили FileStorage
с действующим соавтором, необходимым для ведения бизнеса. Нет больше неуклюжих, разоблачающих посредников по пути!
К счастью, все эти болтовни можно легко перевести в рабочий код всего за несколько усилий. Просто проверьте улучшенную версию класса FileStorage
соответствующую Закону Деметры:
<?php namespace LibraryFile; use LibraryEncoderEncoderInterface; class FileStorage { const DEFAULT_STORAGE_FILE = "data.dat"; private $encoder; private $file; public function __construct(EncoderInterface $encoder, $file = self::DEFAULT_STORAGE_FILE) { $this->encoder = $encoder; $this->setFile($file); } public function setFile($file) { // the sample implementation } public function write($data) { try { return file_put_contents($this->file, $this->encoder->encode($data), LOCK_EX); } catch (Exception $e) { throw new $e( "Error writing data to the target file: " . $e->getMessage()); } } public function read() { try { return $this->encoder->decode( @file_get_contents($this->file)); } catch(Exception $e) { throw new $e( "Error reading data from the target file: " . $e->getMessage()); } } }
Это было легко реорганизовать, действительно. Теперь класс напрямую потребляет любые реализации интерфейса EncoderInterface
, избегая прохождения внутренностей ненужного промежуточного звена. Этот пример, безусловно, тривиален, но он действительно дает обоснование и демонстрирует, почему соблюдение заповедей Закона Деметры — одна из лучших вещей, которую вы можете сделать, чтобы улучшить дизайн ваших классов.
Тем не менее, есть особый случай закона, подробно рассмотренный в книге Роберта Мартина «Чистый код: Справочник по мастерству гибкого программного обеспечения» , который заслуживает особого анализа. Подумайте об этом на мгновение: что произойдет, если FileStorage будет определен для получения своего соавтора через объект передачи данных (DTO), как это?
<?php namespace LibraryFile; interface FileStorageDefinitionInterface { public function getEncoder(); public function getFile(); }
<?php namespace LibraryFile; use LibraryEncoderEncoderInterface; class FileStorageDefinition implements FileStorageDefinitionInterface { const DEFAULT_STORAGE_FILE = "data.dat"; private $encoder; private $file; public function __construct(EncoderInterface $encoder, $file = self::DEFAULT_STORAGE_FILE) { if (!is_readable($file) || !is_writable($file)) { throw new InvalidArgumentException( "The target file is invalid."); } $this->encoder = $encoder; $this->file = $file; } public function getEncoder() { return $this->encoder; } public function getFile() { return $this->file; } }
<?php namespace LibraryFile; class FileStorage { private $storageDefinition; public function __construct(FileStorageDefinitionInterface $storageDefinition) { $this->storageDefinition = $storageDefinition; } public function write($data) { try { return file_put_contents( $this->storageDefinition->getFile(), $this->storageDefinition->getEncoder()->encode($data), LOCK_EX ); } catch (Exception $e) { throw new $e( "Error writing data to the target file: " . $e->getMessage()); } } public function read() { try { return $this->storageDefinition->getEncoder()->decode( @file_get_contents($this->storageDefinition->getFile()) ); } catch(Exception $e) { throw new $e( "Error reading data from the target file: " . $e->getMessage()); } } }
Это определенно интересный уклон для реализации класса хранения файлов, так как теперь он использует инъекционный DTO для передачи и внутреннего потребления кодировщика. Вопрос, который требует ответа, состоит в том, действительно ли этот подход нарушает закон. В чистом смысле это так, поскольку DTO, несомненно, является посредником, раскрывающим всю свою структуру вызывающей стороне. Тем не менее, DTO — это просто простая структура данных, которая, в отличие от более раннего локатора служб, вообще не работает. И именно цель структур данных состоит в том, чтобы… да, выставлять свои данные. Это означает, что до тех пор, пока посредник не реализует поведение (что в точности противоположно тому, что делает обычный класс, поскольку он демонстрирует поведение, скрывая свои данные), закон Деметры будет оставаться аккуратно сохраненным.
В следующем фрагменте показано, как использовать FileStorage
с соответствующим DTO:
<?php $fileStorage = new FileStorage(new FileStorageDefinition(new Serializer())); $fileStorage->write(["This", "is", "my", "sample", "array"]); print_r($fileStorage->read());
Этот подход намного более обременителен, чем просто прямая передача кодировщика в класс хранения файлов, но пример показывает, что некоторые хитрые реализации, которые на первый взгляд кажутся вопиющими нарушителями закона, в целом довольно безвредны, пока поскольку они используют структуры данных без привязанного к ним поведения.
Заключительные мысли
С многочисленными запутанными, иногда эзотерическими, эвристиками, пробивающимися через ООП, кажется, что бессмысленно добавлять еще одну в кучу, что, по-видимому, не оказывает видимого положительного влияния на дизайн компонентов слоя. Закон Деметры — это все, кроме принципа, который практически не применяется в реальном мире.
Несмотря на свое процветающее название, Закон Деметры является мощной парадигмой, основная цель которой состоит в том, чтобы содействовать внедрению компонентов приложения с высокой степенью разделения, устраняя ненужных посредников. Просто следуйте его заповедям, не впадая в слепой догматизм, конечно, и вы увидите, как улучшится качество вашего кода. Гарантированный.
Изображение через Fotolia