Pull to refresh

«Банда четырёх» была неправа, а вы не знаете, что такое делегирование

Reading time6 min
Views70K
Original author: Jim
«Банда четырёх» была неправа, стандартная библиотека 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, с головой уходит в понимание делегирования и исследует возможности того, как правильно организовать ваши проекты, сделать их поддерживаемыми и слабо связанными. Она даёт вам способы организации разделения поведения и уточняет определение важных концепций.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+3
Comments26

Articles

Change theme settings