Принцип открытости / закрытости (open closed solid)

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

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

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

Реализация неполиморфной библиотеки HTML Renderer

Хотя идея связана в основном с практическим опытом, иногда легче понять концепцию, сначала показав неправильный способ делать что-то. В этом случае я собираюсь взглянуть на проблему с точки зрения прагматического подхода и продемонстрировать, почему внедрение системы модулей, которые не подчиняются идее «Закрыто для модификации», установленной принципом открытости/закрытости, может привести к печальным последствиям.
Нам нужно построить на скорую руку модуль HTML Renderer, способную хранить несколько HTML объектов: слои, параграфы и тому подобное. Следуя небрежному и неполиморфному подходу, соответствующие 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, безусловно, может быть расширен либо через Наследование, либо через Композицию (читай открыт для расширения). В чем же будет заключаться проблема? Ну, самый неуклюжий аспект опирается на тот факт, что уровень закрытия для модификации в этом случае просто блеф, так как обрабатываемые HTML-объекты не являются полиморфными. В своем нынешнем состоянии класс способен обработать ветки HtmlDiv и HtmlParagraph объектов и… извините, больше ничего. Если мы когда-нибудь захотим добавить новый тип объекта к его репертуару, метод render() необходимо реструктурировать и дополнить условными выражениям. С точки зрения принципа открытости/закрытости класс отнюдь не закрыт для изменений.

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

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

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

Теперь пришло время сделать небольшую очистку и инкапсулировать всю логику, разделенную HTML классами внутри границ абстрактного Супертип слоя

<?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 теперь представляют собой красивые полиморфные структуры, которые соблюдают общий контракт, стало просто провести рефакторинг HTML Renderer в потребителя любого исполнителя 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 далеко не сногсшибательная, но зато теперь это крепкий модуль, который придерживается принципа окрытости/закрытости. Кроме того, она не только обеспечивает хороший уровень закрытия для модификации, но и стало возможным вносить во время выполнения с несколькими исполнителями 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 принципов, принцип открытости/закрытости, безусловно, — одна из основ ООП. Выгода от ее использования настолько значима, что несправедливо недооценивать ее. Однако, само собой разумеется, ключ к достижению этой цели требует разработки таких компонентов, как полиморфные элементы, поведение которых четко определено посредством абстрактных контрактов. С этого момента то, какой подход самый эффективный для их расширения (Наследство / Композиция), и то, какой уровень закрытия от внешнего мира они должны поддерживать, полностью зависит от вас.