Статьи

Открытый / Закрытый Принцип

Я должен признать, что когда я впервые заглянул в академическое определение Открытого / Закрытого Принципа , его предикат был удивительно ясен для меня. Конечно, если оставить в стороне весь технический жаргон, его диктат был в значительной степени мантрой, которую мы слышали много раз раньше: «Не взламывайте ядро».

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

Вопрос не в том, какой вариант лучше всего подходит для расширения функциональности. Дискуссия «Наследование против композиции» настолько старая и скучная, что ее вынесение на стол будет пустой тратой времени. Актуальная проблема заключается в выборе методологии программирования, которая обеспечивает достойный уровень закрытия целевого модуля, но в то же время предоставляет ему «крючок», открывающий его для расширения. Теоретически, и Наследование, и Композиция являются хорошими кандидатами для достижения этой цели, поскольку они полагаются на внутренние преимущества, предоставляемые абстракцией, а не конкретной, жесткой реализацией.

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

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

Реализация неполиморфного рендера HTML

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

Скажем, нам нужно создать быстрый и грязный модуль рендеринга HTML, способный выводить несколько объектов HTML, моделирующих элементы div, параграфы и тому подобное. Следуя неуважительно небрежному и неполиморфному подходу, соответствующие HTML-классы и потребительский модуль могут быть записаны следующим образом:

<?php namespace LibraryView; class HtmlDiv { private $text; private $id; private $class; public function __construct($text, $id = null, $class = null) { $this->setText($text); if ($id !== null) { $this->setId($id); } if ($class !== null) { $this->setClass($class); } } public function setText($text) { if (!is_string($text) || empty($text)) { throw new InvalidArgumentException( "The text of the element is invalid."); } $this->text = $text; return $this; } public function setId($id) { if (!preg_match('/^[a-z0-9_-]+$/', $id)) { throw new InvalidArgumentException( "The attribute value is invalid."); } $this->id = $id; return $this; } public function setClass($class) { if (!preg_match('/^[a-z0-9_-]+$/', $id)) { throw new InvalidArgumentException( "The attribute value is invalid."); } $this->class = $class; return $this; } public function renderDiv() { return '<div' . ($this->id ? ' id="' . $this->id . '"' : '') . ($this->class ? ' class="' . $this->class . '"' : '') . '>' . $this->text . '</div>'; } } 
 <?php namespace LibraryView; class HtmlParagraph { private $text; private $id; private $class; public function __construct($text, $id = null, $class = null) { $this->setText($text); if ($id !== null) { $this->setId($id); } if ($class !== null) { $this->setClass($class); } } public function setText($text) { if (!is_string($text) || empty($text)) { throw new InvalidArgumentException( "The text of the element is invalid."); } $this->text = $text; return $this; } public function setId($id) { if (!preg_match('/^[a-z0-9_-]+$/', $id)) { throw new InvalidArgumentException( "The attribute value is invalid."); } $this->id = $id; return $this; } public function setClass($class) { if (!preg_match('/^[a-z0-9_-]+$/', $id)) { throw new InvalidArgumentException( "The attribute value is invalid."); } $this->class = $class; return $this; } public function renderParagraph() { return '<p' . ($this->id ? ' id="' . $this->id . '"' : '') . ($this->class ? ' class="' . $this->class . '"' : '') . '>' . $this->text . '</p>'; } } 
 <?php namespace LibraryView; class HtmlRenderer { private $elements = array(); public function __construct(array $elements = array()) { if (!empty($elements)) { $this->addElements($elements); } } public function addElement($element) { $this->elements[] = $element; return $this; } public function addElements(array $elements) { foreach ($elements as $element) { $this->addElement($element); } return $this; } public function render() { $html = ""; foreach ($this->elements as $element) { if ($element instanceof HtmlDiv) { $html .= $element->renderDiv(); } else if ($element instanceof HtmlParagraph) { $html .= $element->renderParagraph(); } } return $html; } } 

Обязанности HtmlDiv и HtmlParagraph ограничены отображением соответствующих элементов HTML на основе нескольких общих входных аргументов, таких как внутренний текст, а также атрибуты «id» и «class».

Класс, который требует внимания, это искусственный HtmlRenderer , который, безусловно, может быть расширен в будущем с помощью Inheritance или Composition (читай, открыто для расширения). Разоблачая такую ​​добродетель для внешнего мира, что может быть не так с рендером? Что ж, его самый неуклюжий аспект основывается на том факте, что уровень закрытия для модификации в этом случае является просто блефом, потому что объекты HTML, которые он обрабатывает, не являются полиморфными. В своем текущем состоянии класс просто способен отображать пакеты объектов HtmlDiv и HtmlParagraph и… извините, ничего больше. Если мы когда-нибудь захотим добавить новый тип объекта в свой репертуар, метод render() должен быть подвергнут рефакторингу и загрязнен большим количеством условных выражений. Чтобы выразить это в терминах Открытого / Закрытого Принципа, класс никоим образом не закрыт для модификации.

Обращаясь к преимуществам полиморфизма

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

Сегрегированный интерфейс, который хорошо выполняет свою работу:

 <?php namespace LibraryView; interface HtmlElementInterface { public function render(); } 

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

 <?php namespace LibraryView; abstract class AbstractHtmlElement implements HtmlElementInterface { protected $text; protected $id; protected $class; public function __construct($text, $id = null, $class = null) { $this->setText($text); if ($id !== null) { $this->setId($id); } if ($class !== null) { $this->setClass($class); } } public function setText($text) { if (!is_string($text) || empty($text)) { throw new InvalidArgumentException( "The text of the element is invalid."); } $this->text = $text; return $this; } public function setId($id) { $this->checkAttribute($id); $this->id = $id; return $this; } public function setClass($class) { $this->checkAttribute($class); $this->class = $class; return $this; } protected function checkAttribute($value) { if (!preg_match('/^[a-z0-9_-]+$/', $value)) { throw new InvalidArgumentException( "The attribute value is invalid."); } } } 

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

 <?php namespace LibraryView; class HtmlDiv extends AbstractHtmlElement { public function render() { return '<div' . ($this->id ? ' id="' . $this->id . '"' : '') . ($this->class ? ' class="' . $this->class . '"' : '') . '>' . $this->text . '</div>'; } } 
 <?php namespace LibraryView; class HtmlParagraph extends AbstractHtmlElement { public function render() { return '<p' . ($this->id ? ' id="' . $this->id . '"' : '') . ($this->class ? ' class="' . $this->class . '"' : '') . '>' . $this->text . '</p>'; } } 

Учитывая, что HtmlDiv и HtmlParagraph теперь являются красиво полиморфными структурами, которые HtmlDiv общий контракт, просто преобразовать соответствующий HTML-рендеринг в потребителя любого разработчика интерфейса HtmlElementInterface :

 <?php namespace LibraryView; class HtmlRenderer { private $elements = array(); public function __construct(array $elements = array()) { if (!empty($elements)) { $this->addElements($elements); } } public function addElement(HtmlElementInterface $element) { $this->elements[] = $element; return $this; } public function addElements(array $elements) { foreach ($elements as $element) { $this->addElement($element); } return $this; } public function render() { $html = ""; foreach ($this->elements as $element) { $html .= $element->render(); } return $html; } } 

Я знаю, что новая реализация класса HtmlRenderer далека от того, чтобы быть сногсшибательной, но, тем не менее, теперь это солидный модуль, который придерживается предикатов Open / Closed Principle. Кроме того, он не только предоставляет хороший уровень закрытия для модификации, так как его можно HtmlElementInterface во время выполнения несколькими HtmlElementInterface интерфейса HtmlElementInterface не внося в него ни единого фрагмента, но эта функция сама по себе доказывает, что она полностью открыта для расширение также. Не стесняйтесь погладить себя по спине, потому что мы выиграли битву на двух фронтах!

Вот как модуль может работать для рендеринга пары HTML-объектов:

 <?php use LibraryLoaderAutoloader, LibraryViewHtmlDiv, LibraryViewHtmlParagraph, LibraryViewHtmlRenderer; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader(); $autoloader->register(); $div = new HtmlDiv("This is the text of the div.", "dID", "dClass"); $p = new HtmlParagraph("This is the text of the paragraph.", "pID", "pClass"); $renderer = new HtmlRenderer(array($div, $p)); echo $renderer->render(); 

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

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

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