Почти все известные объектно-ориентированные языки (Java, C #, JavaScript, Python, Ruby, …) имеют одну отправку: методы выбираются в зависимости от (одного) получателя сообщения. С другой стороны, существует многократная отправка, которая представляет собой интересную смесь выбора функционального метода с изменяемым состоянием. Он используется в менее известных объектно-ориентированных языках, таких как Common Lisp.
В этой статье мы сначала рассмотрим отдельную диспетчеризацию Java и ее перегрузку, а затем воспользуемся тем, что мы узнали, для понимания множественной диспетчеризации и того, как она решает некоторые проблемы проектирования, которые невозможно решить с помощью одной диспетчеризации.
Начнем с динамической отправки:
public class Empty extends Object {
@Override
public String toString() {
return "Empty";
}
public static void main(String[] args) {
Object empty = new Empty();
System.out.println(empty.toString());
}
}
Каков вывод этой программы? Это «Пусто». Если это кажется очевидным, это потому, что вы уже хорошо знакомы с динамической диспетчеризацией : Java во время выполнения определяет, к какому классу принадлежит экземпляр, и выбирает соответствующий, возможно, переопределенный метод. Для приведенного выше примера это означает, что мы используем не метод toString () из класса Object, а метод toString () из класса Empty. В некоторых языках, таких как C ++, вы должны явно указать, что вы хотите динамическую диспетчеризацию.
Теперь перегрузка, еще один способ выбора реализации метода.
public class Printer {
public void print(String str) {
System.out.println("String: "+str);
}
public void print(Object obj) {
System.out.println("Object: "+obj);
}
public static void main(String[] args) {
Object obj = "abc";
new Printer().print(obj);
}
}
Здесь вывод «Объект: abc». Реализация метода выбирается статически во время компиляции: внутренне компилятор использует статические типы аргументов метода для устранения неоднозначности имен методов. На этот раз результат неожиданный, даже действительно хорошие Java-программисты, которых я попросил, ошибаются. Из-за ощущения динамической отправки, которое является естественным для получателя сообщения (= «this»), мы ожидаем, что оно будет работать одинаково для аргументов. Существует некоторая асимметрия между получателем и аргументами метода, и эта асимметрия отражается и в синтаксисе вызова.
При множественной диспетчеризации методы становятся конструкциями верхнего уровня. Это похоже на реализацию одного метода отправки «foo» с двумя аргументами в качестве статического метода:
public static void foo(this, arg1, arg2) {
if (this instanceof A) {
...
} else if (this instanceof B) {
...
} ...
}
Получатель сообщения «this» становится просто еще одним аргументом, и все варианты метода объединяются в одном месте (такой взгляд на метод помогает понять реализации, использующие полиморфизм, но я отвлекся). Вместо myObj.foo (x, y) вы теперь вызываете foo как foo (myObj, x, y). Это все еще единственная отправка. Множественная отправка вкладывает проверки экземпляров для arg1 и arg2 внутри проверок «this». Только после проверки типов всех аргументов мы решаем, какой вариант метода использовать. Common Lisp вызывает foo универсальную функцию, и фрагмент кода внутри ее методов .
Обратите внимание, что операторы if были только для иллюстрации, языки с множественной диспетчеризацией имеют эффективные алгоритмы для выполнения проверок и выбора метода.
Какие преимущества это имеет?
Универсальная функция может «принадлежать» нескольким классам. Это помогает всякий раз, когда аргументы метода являются не истинными параметрами (данными), а сотрудниками (которые предлагают услуги) для алгоритма. Например, если у вас есть метод Database.export (Filesystem, UserFeedback), этот метод может содержать столько же вызовов базы данных, сколько вызовов файловой системы. Особый случай бинарных операторов как методов демонстрирует ту же трудность: следует ли считать оператор «String + Integer» частью класса Integer или частью класса String? По какой-то причине в UML есть специальные диаграммы для совместной работы, и эти диаграммы представляют собой сквозные классы.
Другим примером взаимодействующих объектов является шаблон посетителя: это неуклюжая симуляция множественной отправки с одной отправкой. По сути, у вас есть объект для алгоритма, взаимодействующий с объектом для данных. С многократной диспетчеризацией все намного проще, меньше кода для написания и объекты данных не должны быть подготовлены для посетителей. Интересно, что даже явный объект для алгоритма исчезает, потому что универсальная функция заменяет его.
Другая область, где показана асимметрия одиночной отправки, имеет нулевое значение. Например, «abc» .equals (null) в порядке, в то время как null.equals («abc») вызывает исключение (и даже не является прямо синтаксически правильным). Если вы введете нулевые проверки в качестве критерия выбора для методов, то обработка нулевых значений будет простой с многократной отправкой.
Расширение класса тривиально с многократной отправкой, просто создайте новую универсальную функцию, которая принимает экземпляры этого класса в качестве аргумента. В Java люди часто пропускают внешние статические методы, которые фактически расширяют данный класс, потому что они не знают, где искать. Например, если вы плохо знаете Java, вы можете быть озадачены, почему у List нет метода sort (). Если вы это сделаете, вы знаете, что у класса Collections есть статический метод sort (List), который вы должны использовать. В языках с множественной диспетчеризацией уже предполагается, что в общем случае универсальная функция актуальна для нескольких классов. Инструменты разработки помогают находить все функции, которые применяются к данному классу, обеспечивая повторное использование кода, а не повторного изобретения.
Тесная интеграция кода с данными менее желательна в настройках, где вы сериализуете объекты. При использовании общих функций код и данные разделены, и проще использовать одинаковые структуры данных на сервере и клиенте. Сервер может содержать много кода, который генерирует или изменяет данные. Клиенту нужно только отобразить данные и позволить серверу обрабатывать более сложные вещи. Это частый сценарий, когда клиент-сервер взаимодействует с Google Web Toolkit. Поскольку Java не имеет универсальных функций, функциональность только для сервера должна быть перенесена во внешние статические вспомогательные методы. Следовательно, вещи еще менее инкапсулированы, немного грязны и теряют полиморфизм.
Предикатная отправка
Обобщением множественной диспетчеризации является предикатная диспетчеризация., При множественной диспетчеризации методы функции выбираются на основе типов аргументов. При предикатной отправке можно указывать более сложные условия. Это упрощает изменение поведения экземпляра в зависимости от его состояния и устраняет необходимость в шаблоне стратегии. Например: предположим, что экземпляр класса Bar должен вести себя иначе, если для флага ошибки установлено значение true. Соответствующие универсальные функции будут содержать один метод, который вызывается, если флаг имеет значение true, и другой, если флаг имеет значение false. К первому методу прикреплено явное условие выбора, например myBar.error. Второй метод имеет такое условие, как! MyBar.error. Эти условия выбора являются дополнительными способами классификации экземпляров. Можно сказать, что myBar меняет свой класс в зависимости от значения его поляошибка .
Вывод
Мы видели, что многократная отправка может сделать несколько вещей, которые не может одна отправка. Но я думаю, что оба дополняют друг друга. Передача сообщений — это хорошая и чистая метафора для вызова метода, которая хорошо работает с распределенными вычислениями. Он рассматривает объекты как компоненты, которые предоставляют услуги. С другой стороны, объекты как данные приводят к явлениям (бинарным операторам и т. Д.), Которые лучше всего реализовать с помощью множественной диспетчеризации.
дальнейшее чтение
Основы в глубине
- « Многократная отправка на практике »: Раду Мушевич, Алекс Потанин, Эван Темперо, Джеймс Нобл.
- Тодд Миллштейн, Кристофер Фрост, Джейсон Райдер, Алессандро Варт, « Экспрессивная и модульная рассылка предикатов для Java ». Статья о расширении Java с помощью предикатной отправки.
- « Модель посетителей по сравнению с мультиметодами ». Объясняет, как эти два связаны, через язык программирования Nice.
Расширенные темы
- « Python 3000 — адаптация или общие функции? Гвидо ван ван Россум. Иллюстрирует, как универсальные функции включают адаптацию.
- « Архитектура DCI: новое видение объектно-ориентированного программирования », Трюгве Реенскауг, Джеймс О. Коплиен. Объясняет, что объекты как данные достаточно стабильны, а алгоритмы, охватывающие несколько объектов, — нет. Я подозреваю, что их решение могло бы выиграть от многократной отправки.
Языки, близкие к Java с множественной диспетчеризацией
- « Проект MultiJava ». Расширяет Java с многократной отправкой. Статьи на этом сайте предоставляют больше информации о многократной рассылке.
- « JPred: Практическая рассылка предикатов для Java ». Расширяет Java с помощью предикатной отправки.
- « Хороший язык программирования ». Язык на основе JVM с множественной диспетчеризацией, синтаксис которого относительно близок к Java.