«Банда четырёх» была неправа, стандартная библиотека Ruby тоже ошибочна, и Rails – также. Но является ли нечто неправильным, если все так делают?

Да.

Книга «Банды четырёх» "Шаблоны проектирования" даёт нам общий словарь для понимания базовых шаблонов ООП. Она помогает нам использовать одинаковую терминологию при обсуждении софта. К сожалению, она же является причиной путаницы.

Они говорят: «композиция прежде наследования». Отлично, в этом есть смысл. Они говорят: «используйте делегирование». Отлично. Хотя в книге нет ни единого примера делегирования.

Делегирование – это приём, которому приписывают возможность внесения гибкости в программы. Обычно говорят, что делегирование – это способ достичь композиции. Но делегирование – это не то, что вы думаете, и «Банда четырёх» ввела вас в заблуждение. Хуже того, почти все упоминания о делегировании содержат лишь совместную работу объектов с пересылкой (forwarding) сообщений. Это примеры вызовов методов, а не делегирования.

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

Так что есть делегирование?


Делегирование понять легко – но давайте исправим тот факт, что мы постоянно видели этот термин вместе с описанием чего-то другого.

Генри Либерман подробно описал этот термин в статье «Using prototypical objects to implement shared behavior in object-oriented systems». Но я не отсылаю вас к ней, хотя её полезно будет прочесть (или её онлайн-вариант) – я привожу ключевой момент, описывающий делегирование. Либерман обсуждал этот вопрос в контексте инструмента для рисования GUI. Вот основная идея:

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

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

Другое наследование


Наследовать можно не только от классов. Наследование, основанное на прототипах – ещё один способ организовать объекты так, чтобы они могли делиться поведением. Один из подходов устанавливает поведения в абстрактном месте (классе), а другой – в экземплярах (объекты).

Self – язык программирования, реализовавший то, о чём говорит Либерман. В Self есть объекты, содержащие слоты. Каждый слот может содержать метод или ссылку на объект-прототип в родительском слоте. Если объект получает сообщение и не понимает его, он может делегировать его объекту в родительском слоте.

Это наследование прототипов, а не классов. Это примерно то же самое, хотя и не идентично, поведению, когда у объекта есть класс (как у объектов Ruby), содержащий дополнительные возможности поведения.

Предоставление в Self объектов с родительскими слотами аналогично предоставлению в JS объекта со ссылками на прототипы. JS – самый популярный язык с использованием прототипов, и его гораздо легче опробовать в деле. Поэтому не будем учить Self, а сразу попробуем JS. Следующий код можно выполнить даже прямо в консоли браузера.

В JS можно присваивать прототип – эквивалент родительского слота в Self.

function Container(){};
Container.prototype = new Object();
Container.prototype.announce = function(){ alert("these are my things: " + this.things) };

function Bucket(things){this.things = things};
Bucket.prototype = new Container();

bucket = new Bucket("planes, trains, and automobiles")
bucket.announce() // alerts "these are my things: planes, trains, and automobiles"


В контексте делегирования объект bucket – это клиент, пересылающий сообщение делегату. В нашем примере можно видеть, что вычисление this.things происходит в контексте клиентского объекта. Когда мы вызываем announce, она обнаруживается у объекта-делегата. При вычислении функции this указывает на клиента.

Когда у прототипа объекта в JS есть функция, она вычисляется так, будто у объекта есть такой метод. Первый пример показывает, что this (в JS это self) всегда указывает на первоначального получателя сообщения.

А как там в Ruby?


Для начала разберёмся в пересылке сообщений. Пересылка – это передача сообщения от одного объекта другому. Стандартная библиотека forwardable названа именно так, и позволяет вам пересылать сообщения от одного объекта другому.

Давайте теперь возьмём не так удачно названную библиотеку delegate, которая также позволяет пересылать сообщения от одного объекта другому.

require 'delegate'

# assuming we have a Person class with a name method
person = Person.new(:name => 'Jim')

class Greeter < SimpleDelegator
  def hello
    "Hi! I'm #{name}."
  end
end

greeter = Greeter.new(person)

greeter.hello #=> "Hi! I'm Jim."


Что происходит внутри Greeter? Когда его экземпляр инициализируется, то содержит ссылку на person. Когда вызывается неизвестный метод, он пересылается целевому объекту (а именно, person). Ведь мы всё ещё работаем с библиотекой delegate, помогающей нам с пересылкой сообщений. Запутались? И я тоже был в непонятках. Как и весь остальной мир, судя по всему.

Пересылка – это просто пересылка сообщения к объекту – вызов метода.

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

Обычно мы не задумываемся об этом, поскольку возможности method_missing в Ruby выполняют этот фокус внутри SimpleDelegator. И мы думаем, что методы магическим образом вызываются у нужного объекта. И хотя наш Greeter ссылается на self, когда у клиента не находится нужного метода, сообщение пересылается другому объекту, и там обрабатывается.

Если нам надо поделиться поведением без расширения объекта дополнительными методами, тогда нам могут помочь method_missing и/или SimpleDelegator. Для простых вариантов это работает хорошо. Но эта система ломает ссылка на класс объекта.

Допустим, нам надо сослаться на класс объекта клиента с каким-нибудь новым типом приветствия. Вместо обычного, давайте скажем: «Hi! I’m the esteemed Person, Jim». Переписывать метод не будем, а просто понадеемся на super, чтобы получить то, что определено в обычном классе Greeter.

class ProperGreeter < Greeter
  def name
    "the esteemed " + self.class.name + ", " + super
  end
end

proper_greeter = ProperGreeter.new(person)

proper_greeter.hello #=> "Hi! I'm the esteemed ProperGreeter, Jim."


Получилось немного не то, что мы хотели. Мы-то хотели увидеть «the esteemed Person».

Это простой пример того, что у нас на самом деле есть два объекта, и того, как в ООП начинается шизофрения из-за self. У каждого объекта своя идентичность и своё понимание self. Мы можем исправить ситуацию, поменять ссылку на __getobj__ (как этого ждёт от нас SimpleDelegator) вместо self, но это пример того, как self не ссылается на то, что нам нужно. Мы должны работать с двумя объектами и наше представление о работе программы требует, чтобы мы думали о двух объектах, взаимодействующих друг с другом, в то время, как мы, по сути, изменяем поведение только одного.

Это не делегирование, а взаимодействие двух объектов. Несмотря на кучу статей, книг и библиотек, которые убеждают вас в обратном.

Ну и назовём это как угодно


Кому какая разница – ведь все так делают?

Угу, в «Шаблонах проектирования» приведены примеры на C++. А C++ не умеет делегировать. Достаточно ли этого аргумента для переопределения смысла термина? Если используемый вами язык не имеет такой возможности – не говорите, что он умеет «делегировать».

Исправляем концепции


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

Книга «Банды четырёх» «Шаблоны проектирования» даёт нам общий словарь для понимания базовых шаблонов ООП. Она помогает нам использовать одинаковую терминологию при обсуждении софта.

Когда я начал вести свои исследования для написания книги Clean Ruby, я обратился к «Шаблонам проектирования». «Уж наверняка эта книга поможет выстроить правильное понимание использования разных шаблонов»,- подумал я. К сожалению, в ней есть грубая ошибка, которую за ней повторили в огромном количестве книг, статей и библиотек.

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

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

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

Комментарии (13)


  1. Fen1kz
    05.04.2016 13:39
    -1

    26 "это"! Это то, что раздражает в том, что является переводом.


    1. Fil
      05.04.2016 13:55
      +2

      В вашем последнем переводе, меньшим по объему текста, их 15.


      1. Fen1kz
        05.04.2016 14:46

        Спасибо, сократил количество до семи на ~1000 слов. Здесь на 26 на ~1300, так что и тут сократить бы не мешало.


  1. dima_mendeleev
    06.04.2016 10:50

    ясно, хотя и не до конца… совсем.


  1. chaetal
    06.04.2016 15:04

    + уже просто за упоминание Self :)


  1. stychos
    06.04.2016 16:55

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


  1. prefrontalCortex
    07.04.2016 09:18
    +2

    JavaScript makes me want to flip the table and say “F*ck this sh*t”, but I can never be sure what “this” refers to.


  1. RealFLYNN
    07.04.2016 11:29

    Что-то я запутался. Если прототипное наследование в JS — пример правильной делегации, то что же тогда, с точки зрения паттернов, происходит в ES6 классах? С одной стороны, это то самое классовое наследование, от которого следует отказаться в пользу делегирования, а с другой — всего лишь синтаксический сахар над прототипным наследованием.

    Извините, если у меня каша в голове — я правда запутался и буду признателен, если кто-то поможет поставить понятия на место 0_о


    1. chaetal
      07.04.2016 12:06
      +2

      Вряд ли вообще что-то в JS может быть примером правильной реализации. :)


  1. atc
    07.04.2016 18:46
    +3

    Если делегирование по-прежнему лучше наследования, но при этом делегирование — всего лишь передача полномочий прототипу, то какой вывод можно сделать из первого утверждения:
    1) прототипное наследование лучше наследования классов?
    2) делегирование невозможно без прототипного наследования?

    Кажется автор вводит свою уникальную терминологию, которая не вяжется с терминологией «банды четырёх», либо я просто не смог понять посыл автора.


  1. waplab
    07.04.2016 19:08
    +1

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

    И при чем здесь GOF если термин делегирования используется повсеместно не так как понимал Либерман?

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


  1. pushtaev
    07.04.2016 21:48
    +1

    С чего вообще автор взял, что «делегирование» — это когда self сохраняется? Вы обратите внимание на раздел «Ну и назовём это как угодно» — он там даже не отвечает на свой же вопрос, а фактически говорит: «То, что я называю „делегирование“ в C++ отсутствует, значит это не делегирование».

    Эээ. Что?


  1. anonymous
    00.00.0000 00:00