Pull to refresh

Примеси VS делегирование: преимущества и недостатки при реализации «плагинов»

Website development *
В данной статье я предлагаю вам свой взгляд на выбор использования примесей или делегирования в проектах для добавления в класс нового функционала.

Начальные условия такие: мы рассматриваем примеси, имеющие свое состояние и имеющие доступ ко всем членам класса-агрегатора. Все публичные члены класса примеси становятся частью агрегатора. Мы оставляем за кадром вопрос быстродействия. Вопрос исследуем на примере добавления нового функционала в модель выдуманного ORM.

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



Данная статья в некотором роде является логическим продолжением этой статьи о реализации примесей в PHP. Если вам интересно — прочтите. Если нет, это не будет проблемой для понимания излагаемого тут материала.

Образцы кода


Сначала рассмотрим различия двух подходов на примере кода, делающего одно и то же.

Примесь


class Aggregator { }

class Mixin {
    public function doSomething() {}
}

Mixins::mix("Aggregator", "Mixin");

$a = new Aggregator ();
$a->doSomething();


Делегирование


Class Aggregator {}

class Mixin {
    public function doSomething() {}
}

class AggregatorMixed extends Aggregator {
    private $mixin;

    function __construct() {
        $mixin = new Mixin();
    }

    public function doSomething() {
        $mixin->doSomething();
    }
}

$a = new AggregatorMixed ();
$a->doSomething();


Обратите внимание, что в некоторых случаях вы можете избежать наследования, если вы можете включить делегируемые методы прямо в класс Aggregator. Но это не всегда возможно и я привел здесь пример именно в таком виде из-за рассмотрения примесей и делегирования в контексте реализации плагинов к ORM, которую вы найдете ниже.

Технические различия


Чем же отличаются два фрагмента с технической точки зрения? Они обеспечивают одинаковый функционал, как видите.

1. Вызов методов примеси осуществляется динамически по имени, а в случае с делегированием компилятор может осуществить проверку до исполнения кода.

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

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

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

Еще одно небольшое следствие состоит в том, что в случае с делегированием, решение по объему делегируемого функционала принимает класс-агрегатор, в случае с примесями – сама примесь.

3. Примесь автоматически имеет доступ ко всем членам класса-агрегатора и может им произвольно манипулировать, при условии, что знает, как он устроен. В случае делегирования этого доступа нет.

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

Теперь давайте посмотрим, как одни и те же возможности можно реализовать примесями и делегированием.

Пример «Поведение в ORM».


Мы сделали на нашем вымышленном фреймворке небольшой сайт. На этом сайте есть каталог статей. Наш фреймворк содержит ORM, в котором есть модель «статьи». В наш ORM также входят классы, поддерживающие распространенные модели поведения: дерево, версионность, мягко-удаляемость и так далее.

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

В следующей версии фреймворка нам нужно добавить в модель версионность и мягкоудаляемость. Что мы с легкостью и делаем либо подмешивая в модель соответствующие примеси, либо расширяя класс модели с помощью делегирования.

Обратите внимание, что в этом примере возможно использование как примесей, так и делегирования.

Теперь давайте рассмотрим аналогичный пример, только теперь в модель функционал добавляем не мы, а плагины к нашему фреймворку, которые пишутся сторонними разработчиками. От нас требуется только предоставить удобный API, чтобы для интеграции плагина в систему не пришлось переписывать большую ее часть.

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

Но авторы плагинов «версионности» и «мягкоудаляемости» хотят, чтобы эти возможности добавлялись в модель статей не «вместо», а «вместе»! Как это можно реализовать? В случае примесей все по-прежнему просто. Мы подмешиваем в класс модели новый и новый фунционал. А с делегированием? По-видимому, тут схема начинает усложняться. Нам нужно, чтобы классы плагинов наследовались друг от друга, а класс первого плагина – от оригинального класса модели. С определенными затратами такой функционал тоже можно добавить. Что для вас предпочтительнее, должны решить вы сами.

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

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

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

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

P.S. Если данная статья показалась вам интересной, предложите в комментариях тему для следующей.
Tags:
Hubs:
Total votes 35: ↑25 and ↓10 +15
Views 2.2K
Comments Comments 32