30

30


Как наследование, так и композиция позволяют создавать новые типы на осно­ве уже существующих. Композиция обычно применяется для повторного ис­пользования реализации в новом типе, а наследование — для повторного исполь­зования интерфейса. Так как производный класс имеет интерфейс базового класса, к нему можно применить восходящее преобразование к базовому клас­су; это очень важно для работы полиморфизма (см. следующую главу).
Несмотря на особое внимание, уделяемое наследованию в ООП, при началь­ном проектировании обычно предпочтение отдается композиции, а к наследо­ванию следует обращаться только там, где это абсолютно необходимо. Компо­зиция обеспечивает несколько большую гибкость. Вдобавок, применяя хитрости наследования к встроенным типам, можно изменять точный тип и, соответст­венно, поведение этих встроенных объектов во время исполнения. Таким обра­зом, появляется возможность изменения поведения составного объекта во вре­мя исполнения программы.
При проектировании системы вы стремитесь создать иерархию, в которой каждый класс имеет определенную цель, чтобы он не был ни излишне большим (не содержал слишком много функциональности, затрудняющей его повторное использование), ни раздражающе мал (так, что его нельзя использовать сам по себе, не добавив перед этим дополнительные возможности). Если архитекту­ра становится слишком сложной, часто стоит внести в нее новые объекты, раз­бив существующие объекты на меньшие составные части.
Важно понимать, что проектирование программы является пошаговым, по­следовательным процессом, как и обучение человека. Оно основано на экспери­ментах; сколько бы вы ни анализировали и ни планировали, в начале работы над проектом у вас еще останутся неясности. Процесс пойдет более успешно — и вы быстрее добьетесь результатов, если начнете «выращивать» свой проект как живое, эволюционирующее существо, нежели «воздвигнете» его сразу, как небоскреб из стекла и металла. Наследование и композиция — два важнейших инструмента объектно-ориентированного программирования, которые помогут вам выполнять эксперименты такого рода.



Полиморфизм

М
еня спрашивали: «Скажите, мистер Бэббидж, если заложить в машину неверные числа, на выходе она все равно выдаст правильный ответ?» Не представляю, какую же кашу надо иметь в голове, чтобы задавать подобные вопросы.Чарльз Бэббидж (1791-1871)
Полиморфизм является третьей неотъемлемой чертой объектно-ориентиро- ванного языка, вместе с абстракцией данных и наследованием.
Он предоставляет еще одну степень отделения интерфейса от реализации, разъединениячтооткак. Полиморфизм улучшает организацию кода и его чи­таемость, а также способствует созданиюрасширяемыхпрограмм, которые мо­гут «расти» не только в процессе начальной разработки проекта, но и при до­бавлении новых возможностей.
Инкапсуляция создает новые типы данных за счет объединения характери­стик и поведения. Сокрытие реализации отделяет интерфейс от реализации за счет изоляции технических подробностей в private-частях класса. Подобное механическое разделение понятно любому, кто имел опыт работы с процедур­ными языками. Но полиморфизм имеет дело с логическим разделением в кон­текстетипов. В предыдущей главе вы увидели, что наследование позволяет ра­ботать с объектом, используя как его собственный тип, так и его базовый тип. Этот факт очень важен, потому что он позволяет работать со многими типами (производными от одного базового типа) как с единым типом, что дает возмож­ность единому коду работать с множеством разных типов единообразно. Вы­зов полиморфного метода позволяет одному типу выразить свое отличие от другого, сходного типа, хотя они и происходят от одного базового типа. Это отличие выражается различным действием методов, вызываемых через базо­вый класс.
В этой главе рассматривается полиморфизм (также называемыйдинамиче­ским связыванием, илипоздним связыванием, илисвязыванием во время выпол­нения).Мы начнем с азов, а изложение материала будет поясняться простыми примерами, полностью акцентированными на полиморфном поведении про­граммы.

Снова о восходящем преобразовании
Как было показано в главе 7, с объектом можно работать с использованием как его собственного типа, так и его базового типа. Интерпретация ссылки на объ­ект как ссылки на базовый тип называетсявосходящим преобразованием.Также были представлены проблемы, возникающие при восходящем преоб­разовании и наглядно воплощенные в следующей программе с музыкальными инструментами. Поскольку мы будем проигрывать с их помощью объекты Note (нота), логично создать эти объекты в отдельном пакете:
II polymorphism/music/Musi с java
// Объекты Note для использования с Instrument
package polymorphism.music,
public enum Note {
MIDDLE_C. C_SHARP, B_FLAT, // И т.д } /// ~
Перечисления были представлены в главе 5. В следующем примере Wind яв­ляется частным случаем инструмента (Instrument), поэтому класс Wind наследу­ет от Instrument:
//• polymorphism/music/instrument java
package polymorphism.music,
import static net mindview.util.Print.*,
class Instrument {
public void play(Note n) {
print("Instrument.pi ay(Г);
}
}
III ~
//• polymorphism/music/Wind java package polymorphism.music;
// Объекты Wind также являются объектами Instrument, II поскольку имеют тот же интерфейс: public class Wind extends Instrument { // Переопределение метода интерфейса public void pi ay(Note n) {
System out pri ntl n( "Wind playO " + n),
}
} III-
II polymorphism/music/Music.java II Наследование и восходящее преобразование package polymorphism music,
public class Music {
public static void tune(Instrument i) { // ...
i.play(Note.MIDDLE_C),
}
public static void main(String[] args) {
Wind flute = new WindO
tune(flute). // Восходящее преобразование
}
} /* Output
Wind playO MIDDLE_C
*/// -
Метод Music.tune() получает ссылку на Instrument, но последняя также может указывать на объект любого класса, производного от Instrument. В методе main() ссылка на объект Wind передается методу tune() без явных преобразований. Это нормально; интерфейс класса Instrument должен существовать и в классе Wind, поскольку последний был унаследован'от Instrument. Восходящее преобразова­ние от Wind к Instrument способно «сузить» этот интерфейс, но не сделает его «меньше», чем полный интерфейс класса Instrument.
Потеря типа объекта
Программа Music.java выглядит немного странно. Зачем умышленноигнориро­ватьфактический тип объекта? Именно это мы наблюдаем при восходящем преобразовании, и казалось бы, программа стала яснее, если бы методу tune() передавалась ссылка на объект Wind. Но при этом мы сталкиваемся с очень важ­ным обстоятельством: если поступить подобным образом, то потом придется писать новый метод tune() для каждого типа Instrument, присутствующего в системе. Предположим, что в систему были добавлены новые классы Stringed и Brass:
// polymorphi sm/musi c/Musi c2.java // Перегрузка вместо восходящего преобразования package polymorphism.music, import static net.mindview util Print *;
class Stringed extends Instrument {
public void play(Note n) {
pri nt ("Stri nged.pl ay() " + n):
}
}
class Brass extends Instrument {
public void play(Note n) {
printC'Brass playO " + n),
}
}
public class Music2 {
public static void tune(Wind i) { i.play(Note MIDDLE_C),
}
public static void tune(Stringed i) { i.play(Note MIDDLE'C);
}
public static void tune(Brass i) { i play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind(),
Stringed violin = new StnngedO.
Brass frenchHorn = new BrassO.
tune(flute), // Без восходящего преобразования
tune(violin);
tune(frenchHorn).
}
} /* Output
Wind playO MIDDLE_C
Stringed.pi ayО MIDDLE_C
Brass pi ayО MIDDLE_C *///-
Программа работает, но у нее есть огромный недостаток: для каждого нового Instrument приходится писать новый, зависящий от конкретного типа метод tune(). Объем программного кода увеличивается, а при добавлении нового ме­тода (такого, как tune()) или нового типа инструмента придется выполнить не­мало дополнительной работы. А если учесть, что компилятор не выводит сооб­щений об ошибках, если вы забудете перегрузить один из ваших методов, весь процесс работы с типами станет совершенно неуправляемым.
Разве не лучше было бы написать единственный метод, в аргументе которо­го передается базовый класс, а не один из производных классов? Разве не удоб­нее было бы забыть о производных классах и написать обобщенный код для ба­зового класса?
Именно это и позволяет делать полиморфизм. Однако большинство про­граммистов с опытом работы на процедурных языках при работе с полимор­физмом испытывают некоторые затруднения.
Особенности
Сложности с программой Music.java обнаруживаются после ее запуска. Она вы­водит строку Wind.play(). Именно это и требуется, но не понятно, откуда берется такой результат. Взгляните на метод tune():
public static void tune(Instrument i) {
//
i play(Note.MIDDLE_C),
}
Метод получает ссылку на объект Instrument. Как компилятор узнает, что ссылка на Instrument в данном случае указывает на объект Wind, а не на Brass или Stringed? Компилятор и не знает. Чтобы в полной мере разобраться в сути про­исходящего, необходимо рассмотреть понятиесвязывания(binding).
Связывание «метод-вызов»
Присоединение вызова метода к телу метода называетсясвязыванием.Если связывание проводится перед запуском программы (компилятором и компонов­щиком, если он есть), оно называетсяранним связыванием(early binding). Возмож­но, ранее вам не приходилось слышать этот термин, потому что в процедурных языках никакого выбора связывания не было. Компиляторы С поддерживают только один тип вызова — раннее связывание.
Неоднозначность предыдущей программы кроется именно в раннем связы­вании: компилятор не может знать, какой метод нужно вызывать, когда у него есть только ссылка на объект Instrument
Проблема решается благодаряпозднему связыванию(late binding), то есть связыванию, проводимому во время выполнения программы, в зависимости от типа объекта. Позднее связывание также называютдинамическим(dynamic) илисвязыванием на стадии выполнения(runtime binding). В языках, реализую­щих позднее связывание, должен существовать механизм определения факти­ческого типа объекта во время работы программы, для вызова подходящего ме­тода. Иначе говоря, компилятор не знает тип объекта, но механизм вызова методов определяет его и вызывает соответствующее тело метода. Механизм позднего связывания зависит от конкретного языка, но нетрудно предполо­жить, что для его реализации в объекты должна включаться какая-то дополни­тельная информация.
Для всех методов Java используется механизм позднего связывания, если только метод не был объявлен как final (приватные методы являются final по умолчанию). Следовательно, вам не придется принимать решений относитель­но использования позднего связывания — оно осуществляется автоматически.
Зачем объявлять метод как final? Как уже было замечено в предыдущей гла­ве, это запрещает переопределение соответствующего метода. Что еще важнее, это фактически «отключает» позднее связывание или, скорее, указывает компи­лятору на то, что позднее связывание не является необходимым. Поэтому для методов final компилятор генерирует чуть более эффективный код. Впрочем, в большинстве случаев влияние на производительность вашей программы незна­чительно, поэтому final лучше использовать в качестве продуманного элемента своего проекта, а не как средство улучшения производительности.
Получение нужного результата
Теперь, когда вы знаете, что связывание всех методов в Java осуществляется полиморфно, через позднее связывание, вы можете писать код для базового класса, не сомневаясь в том, что для всех производных классов он также будет работать верно. Другими словами, вы «посылаете сообщение объекту и позво­ляете ему решить, что следует делать дальше».
Классическим примером полиморфизма в ООП является пример с геомет­рическими фигурами. Он часто используется благодаря своей наглядности, но, к сожалению, некоторые новички начинают думать, что ООП подразумева­ет графическое программирование — а это, конечно же, неверно.