13 Ноя 2010

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

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

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

В основном данная статья относится к 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. Если данная статья показалась вам интересной, предложите в комментариях тему для следующей.

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

  1. OZ говорит:

    Для использования делегированного метода вообще не нужно никакого наследования. Просто в теле основного класса вызываете ДРУГОЙ класс, передаёте ему параметры и получаете результат. Вот и всё. Даже если вы этот делегируемый метод делаете публичными и вам приходится его декорировать — это не требует наследования.

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

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

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

  2. FractalizeR говорит:

    Для использования делегированного метода вообще не нужно никакого наследования

    В каком-то смысле вы правы. Просто я рассматривал ситуацию на примере именно добавления поведения к готовым классам. Я прокомментировал этот момент в статье.

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

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

    Вы вообще понимаете, о чём пишете?

    Я буду благодарен, если вы докажете мне, что я неправ. Можете изложить, как в случае с делегированием проблема добавления функционала в ORM сторонними разработчиками из моего примера будет решаться легко и просто?

    Примеси это разрушение инкапсуляции,

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

    Спасибо за комментарий.

  3. FractalizeR говорит:

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

  4. OZ говорит:

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

    Не примеси – нарушение инкапсуляции, а возможность примеси изменять защищенные поля агрегата.
    Естественно. Именно этим примесь и разрушает инкапсуляцию.

    как в случае с делегированием проблема добавления функционала в ORM сторонними разработчиками из моего примера будет решаться легко и просто?
    Хоть в вашем примере, хоть в каком другом — создаётся новый класс, который выполняет новый функционал. В родительском классе добавляется метод, который ссылается на метод нового класса.

  5. FractalizeR говорит:

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

    Согласен, я слежу 🙂 Изложение получилось несколько скомканым и недодуманным. Поторопился, наверное. Но в целом, вроде ничего. Посмотрим, что будет дальше, хотя уже видно, что ажиотаж не создан 🙂 Может, ночь на дворе? 🙂

    В родительском классе добавляется метод, который ссылается на метод нового класса.

    По условию задачи это невозможно. Плагины пишут сторонние разработчики и их добавление в проект не должно требовать модификации кода.

    Естественно. Именно этим примесь и разрушает инкапсуляцию.

    Что ж, хоть в чистом ООП это и плохо, но вот скажем, в Ruby инкапсуляции в том смысле, в каком вы сейчас использовали это слово, вообще нет. То есть, любой класс может получить доступ к любому полю другого. И ничего. Я не сторонник догм, каких бы то ни было. Любое правило можно нарушить, если готов взять на себя ответственность за это. Конечно, открывать доступ к скрытым членам — плохо. И делать это нужно с большой осторожностью, если делать вообще. Для новичков — это смерть, конечно. Но я бы, хоть и с большим скрипом, реализовал бы такое решение, если бы увидел, что это наряду с привнесенными недостатками дает мне такие преимущества, которые недостатки перевешивают. Судить нужно осторожно и всегда строго после знакомства с ситуацией.

Ответить

Для отправки комментария вам нужно зарегистрироваться. Войти.