Да.
Книга «Банды четырёх» "Шаблоны проектирования" даёт нам общий словарь для понимания базовых шаблонов ООП. Она помогает нам использовать одинаковую терминологию при обсуждении софта. К сожалению, она же является причиной путаницы.
Они говорят: «композиция прежде наследования». Отлично, в этом есть смысл. Они говорят: «используйте делегирование». Отлично. Хотя в книге нет ни единого примера делегирования.
Делегирование – это приём, которому приписывают возможность внесения гибкости в программы. Обычно говорят, что делегирование – это способ достичь композиции. Но делегирование – это не то, что вы думаете, и «Банда четырёх» ввела вас в заблуждение. Хуже того, почти все упоминания о делегировании содержат лишь совместную работу объектов с пересылкой (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)
stychos
06.04.2016 16:55Месяцев пять этот незаконченный перевод в черновиках валяется) — так и не осилил некоторые обороты грамотно перевести. А статья хорошая, даже мне похапэшнику понравилась.
prefrontalCortex
07.04.2016 09:18+2JavaScript makes me want to flip the table and say “F*ck this sh*t”, but I can never be sure what “this” refers to.
RealFLYNN
07.04.2016 11:29Что-то я запутался. Если прототипное наследование в JS — пример правильной делегации, то что же тогда, с точки зрения паттернов, происходит в ES6 классах? С одной стороны, это то самое классовое наследование, от которого следует отказаться в пользу делегирования, а с другой — всего лишь синтаксический сахар над прототипным наследованием.
Извините, если у меня каша в голове — я правда запутался и буду признателен, если кто-то поможет поставить понятия на место 0_о
atc
07.04.2016 18:46+3Если делегирование по-прежнему лучше наследования, но при этом делегирование — всего лишь передача полномочий прототипу, то какой вывод можно сделать из первого утверждения:
1) прототипное наследование лучше наследования классов?
2) делегирование невозможно без прототипного наследования?
Кажется автор вводит свою уникальную терминологию, которая не вяжется с терминологией «банды четырёх», либо я просто не смог понять посыл автора.
waplab
07.04.2016 19:08+1Название статьи весьма провокационное. Может трюк, чтобы продать концепцию и книжку заодно.
А по сути буквоедство и ограниченность мышления.
И при чем здесь GOF если термин делегирования используется повсеместно не так как понимал Либерман?
Можно дальше придираться к использованию терминов «интерфейс», «абстракция», «наследование» в программировании и пройтись по всем книжкам где они используются не так как считает автор и заодно порекламить книжку.
pushtaev
07.04.2016 21:48+1С чего вообще автор взял, что «делегирование» — это когда self сохраняется? Вы обратите внимание на раздел «Ну и назовём это как угодно» — он там даже не отвечает на свой же вопрос, а фактически говорит: «То, что я называю „делегирование“ в C++ отсутствует, значит это не делегирование».
Эээ. Что?
Fen1kz
26 "это"! Это то, что раздражает в том, что является переводом.
Fil
В вашем последнем переводе, меньшим по объему текста, их 15.
Fen1kz
Спасибо, сократил количество до семи на ~1000 слов. Здесь на 26 на ~1300, так что и тут сократить бы не мешало.