Я старался не перегружать код комментариями. Если не считать первой главы, то думаю, что достиг этой цели. Писатель может стать не в меру болтливым, но программист-то хочет видеть код (а если не хочет, то должен хотеть).
Иногда примеры выглядят искусственными, за что я приношу свои извинения. Проиллюстрировать какой-то прием или принцип в отрыве от реальной задачи бывает сложно. Но чем сложнее задача, чем выше ее уровень, тем большие усилия я прилагал к подысканию реального примера. Так, если речь идет о конкатенации строк, то, наверное, вы увидите безыскусный фрагмент кода с упоминанием пресловутых "foo" и "bar", но когда рассматривается тема разбора XML-документа, будет приведен куда более содержательный и реалистичный пример.
Есть в этой книге два-три каприза, в которых хочу заранее сознаться. Во-первых, я всеми силами старался избегать «уродливых» глобальных переменных типа $_ и ей подобных, пришедших из языка Perl. Они есть в Ruby и прекрасно работают, даже применяются в повседневной работе всеми или большинством программистов. Но почти всегда от их использования можно уйти, что я и позволил себе чуть ли не во всех примерах.
Другой каприз состоит в том, что я избегаю пользоваться обособленными выражениями, если у них нет побочных эффектов. В Ruby выражения - одна из основ языка, и это прекрасно; я старался извлечь из этой особенности максимум пользы. Но во фрагментах кода предпочитаю не употреблять выражения, которые просто возвращают никак не используемое значение. Например, для иллюстрации конкатенации строк достаточно было бы написать "abc" + "def", но я в этом случае пишу что-то вроде str = "abc" + "def". Кому-то это покажется излишеством, но выглядит естественным для программиста на языке С, привыкшего к тому, что бывают функции типа void и не-void (а также программисту на Pascal, мыслящему в терминах процедур и функций).
Третий каприз заключается в моем нежелании употреблять символ решетки для обозначения методов экземпляра. Многие поклонники Ruby считают, что я проявляю излишнюю болтливость, когда пишу «метод экземпляра crypt класса String», а не просто string#crypt, но я полагаю, что так никто не запутается. (На самом деле мне придется постепенно смириться с использованием такой нотации, так как ясно, что она уже никуда не исчезнет.)
Я старался давать ссылки на внешние ресурсы там, где это уместно. Ограничения по времени и объему не позволили мне включить в книгу все, что я хотел бы, но надеюсь, что это хотя бы отчасти компенсируется указаниями на то, где найти недостающую информацию. Из всех источников самым главным, наверное, следует считать архив приложений Ruby (Ruby Application Archive) в сети; вы не раз встретите ссылки на него.
В начале книги принято приводить соглашения об использовании шрифтов, применяемых для выделения кода, и о том, как отличить пример от обычного текста. Но я не стану оскорблять вас недоверием к вашим умственным способностям, - вы ведь и раньше читали техническую литературу.
Хочу подчеркнуть, что примерно 10% текста книги было написано другими людьми. И это не считая технического редактирования и корректуры!.. Вы просто обязаны прочитать благодарности, приведенные в этой (и любой другой) книге. Большинство читателей пропускают их. Прошу, прочтите прямо сейчас. Это будет так же полезно, как питание овощами.
Об исходных текстах, приведенных в книге
Все сколько-нибудь значительные фрагменты кода собраны в архив, который можно загрузить из сети. Этот архив есть на сайте www.awprofessional.com и на моем собственном сайте (www.rubyhacker.com).
Он предлагается в виде tgz-файла и в виде zip-файла. При именовании файлов в нем принято следующее соглашение: код, которому в тексте соответствует пронумерованный листинг, находится в файле с таким же именем (например, listing14-1.rb). Более короткие фрагменты именуются по номеру страницы, возможно, с добавленной буквой (например, p260a.rb и p260b.rb). Совсем короткие фрагменты, которые нельзя исполнить «вне контекста», в архиве обычно отсутствуют.
«Путь Ruby»
Что мы имеем и виду, творя о Пути Ruby? Я полагаю, что тут есть два взаимосвязанных аспекта: философия проектирования Ruby и философия использования этого языка. Естественно, что дизайн и применение связаны друг с другом, будь то программное или аппаратное обеспечение. Иначе зачем бы существовала наука эргономика?.. Если я снабжаю устройство ручкой, то, наверное, предполагаю, что кто-то за эту ручку возьмется.
В языке Ruby имеется невыразимое словами качество, которое делает его тем, что он есть. Мы наблюдаем это качество в дизайне синтаксиса и семантики языка, присутствует оно и в написанных на нем программах. Все же стоит попытаться сформулировать, в чем состоит эта отличительная особенность.
Очевидно, Ruby — не просто инструмент для написания программ, но и сам по себе является программой. Почему работа программ, написанных на Ruby, должна следовать законам, отличным от тех, которым подчинена работа интерпретатора? В конце концов, Ruby — исключительно динамичный и расширяемый язык. Могут найтись причины, по которым эти два уровня где-то расходятся, вероятно, стараясь приспособиться к несовершенству реального мира. Но в общем случае мыслительные процессы могут и должны быть сходными. Интерпретатор Ruby можно было бы написать на самом Ruby, в полном соответствии с принципом Хофштадтера, хотя в настоящее время это еще не сделано.
Мы нечасто задумываемся над этимологией слова «путь», но оно употребляется в двух разных смыслах. Во-первых, это метод или техника, а во-вторых - дорога. Ясно, что оба значения взаимосвязаны, и, говоря «путь Ruby», я имею в виду и то и другое.
Следовательно, мы говорим о мыслительном процессе, но вместе с тем и о дороге, по которой движемся. Даже величайшие гуру программирования не могут сказать о себе, что достигли совершенства: они лишь на пути к нему. Таких путей может быть несколько, но я здесь говорю только об одном.
Привычная мудрость гласит, что форма определяется функцией. Это верно, спору нет. Однако Фрэнк Ллойд Райт[4] (имея в виду свою собственную область интересов) как-то сказал: «Форма определяется функцией, которая была понята неправильно. Форма и функция должны быть едины, сливаться в духовном единении».
Что Райт имел в виду? Я бы сказал, что на этот вопрос вы найдете ответ не в книгах, а в собственном опыте.
Однако я думаю, что Райт выразил эту мысль где-то еще, разбив ее на части, которые проще переварить. Он был великим поборником простоты, который однажды заметил: «Самые полезные инструменты архитектора — это ластик рядом с чертежной доской и гвоздодер на строительной площадке».
Итак, одним из достоинств Ruby является простота. Надо ли цитировать других мыслителей, высказывавшихся на эту тему? Согласно Антуану де Сент-Экзюпери, «совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего убрать».
Но Ruby — сложный язык. Почему же я называю его простым?
Если бы мы лучше понимали мироздание, то, наверное, открыли бы «закон сохранения сложности» - факт, который вмешивается в нашу жизнь подобно энтропии, которую мы не можем преодолеть, а способны лишь рассеивать.
И в этом ключ. Нельзя избежать сложности, но можно укрыться от нее. Мы можем убрать ее из виду! Это тот же старый добрый принцип черного ящика, внутри которого решается сложная задача, хотя на поверхности лишь голая простота.
Если вам еще не наскучили цитаты, то будет уместно привести слова Альберта Эйнштейна: «Все должно быть просто настолько, насколько возможно, но не проще».
Таким образом, на взгляд программиста, Ruby - это воплощенная простота (хотя у человека, отвечающего за сопровождение интерпретатора, взгляд может быть иной). Но вместе с тем имеется пространство для компромиссов. В реальном мире всем нам приходится немного «прогибаться». К примеру, все сущности в программе на Ruby должны были бы быть истинными объектами, однако некоторые, в том числе целые числа, хранятся как непосредственные значения. Это компромисс, знакомый всем студентам отделений информатики уже много десятилетий: элегантность дизайна приносится в жертву практичности реализации. По существу, мы променяли одну простоту на другую.
То, что Ларри Уолл говорил о языке Perl, остается справедливым: «Когда вы хотите что-то выразить на маленьком языке, оно становится большим. А когда вы пытаетесь выразить то же самое на большом языке, оно становится маленьким». Это верно и в отношении английского языка. Если биолог Эрнст Хэккель смог всего тремя словами выразить глубокую мысль «онтогенез повторяет филогенез», то лишь потому, что эти слова с весьма специфическим смыслом были в его распоряжении. Мы соглашаемся на внутреннюю сложность языка, потому что она позволяет избежать сложности в отдельных высказываниях.
Переформулирую этот принцип по-другому: «не пишите 200 строк кода, когда достаточно 10».
Я считаю само собой разумеющимся, что краткость в общем случае хороша. Короткий фрагмент кода занимает меньше места в мозгу программиста, его проще воспринять как единое целое. Благоприятным побочным эффектом следует считать и то, что в короткой программе будет меньше ошибок.
Конечно, не нужно забывать предупреждение Эйнштейна о простоте. Если расположить краткость слишком высоко в списке приоритетов, то получится совершенно загадочный код. Согласно теории информации, сжатые данные статистически похожи на белый шум. Если вы видели код на С или APL либо регулярное выражение — особенно плохо написанные, то понимаете что я имею в виду. «Просто, но не слишком просто» — это ключевая фраза. Стремитесь к краткости, но не жертвуйте понятностью.
Трюизмом следует считать мысль о том, что краткость в сочетании с понятностью — это хорошо. Но тому есть причина, причем настолько фундаментальная, что мы часто о ней забываем. А состоит она в том, что компьютер создан для человека, а не человек для компьютера.
В старые добрые дни все было почти наоборот. Компьютеры стоили миллионы долларов и пожирали многие киловатты электричества. Люди же вели себя так, будто компьютер - божество, а программисты - его скромные жрецы. Час машинного времени стоил дороже часа личного времени.
Когда компьютеры стали меньше и дешевле, приобрели популярность языки высокого уровня. Они неэффективны с точки зрения машины, зато эффективны с позиции человека. Ruby — всего лишь одно из последних достижений на этом пути. Некоторые даже называют его языком сверхвысокого уровня (VHLL — Very High-Level Language). Хотя этот термин еще не получил четкого определения, я думаю, что он оправдан.
Компьютер призван быть слугой, а не хозяином, а, как сказал Мац, толковый слуга должен выполнять сложное задание при минимуме указаний. Так было на протяжении всей истории информатики. Мы начали с машинного языка, перешли к языку ассемблера, а потом добрались и до языков высокого уровня.
Мы сейчас говорим о смещении парадигмы: от машиноцентрической к человекоцентрической. На мой взгляд, Ruby дает великолепный пример человекоцентрического программирования.
Теперь я хочу взглянуть на вопрос под несколько иным углом. В 1980 году вышла чудесная книжка Джеффри Джеймса «Дао программирования» (Geoffrey James, The Tao of Programming). Каждая строчка из нее достойна цитирования, но я ограничусь лишь одной выдержкой: «Программа должна следовать "закону наименьшего удивления". Что это за закон? Все просто: программа должна отвечать пользователю так, чтобы вызывать у него как можно меньше удивления». (Конечно, если речь идет об интерпретаторе языка, то пользователем является программист.)
Не знаю, Джеймс ли придумал термин «закон наименьшего удивления», но я впервые узнал его из упомянутой книги. Этот закон хорошо известен и часто цитируется в сообществе пользователей Ruby. Правда, обычно его называют «принципом наименьшего удивления» (Principle of Least Surprise, POLS). Лично я упрямо придерживаюсь акронима LOLA — Law of Least Astonishment.
Но, как ни называй, правило остается справедливым и служит основополагающим принципом продолжающейся работы над языком Ruby. О нем полезно помнить и тем, кто разрабатывает библиотеки и пользовательские интерфейсы.
Конечно, одна проблема остается: разные люди удивляются разным вещам; не существует всеобщего согласия о том, как «должен» вести себя объект или метод. Но мы можем стремиться быть последовательными и находить веские обоснования принимаемым проектным решениям, а каждый человек должен тренировать собственную интуицию.
Кстати, Мац как-то заметил, что «принцип наименьшего удивления» должен относиться и к нему как к дизайнеру. Чем больше ваше мышление походит на его, тем меньше удивления будет вызывать Ruby. И смею уверить, подражание Мацу большинству из нас пойдет только на пользу.
Какой бы ни была логическая конструкция системы, тренировать свою интуицию необходимо. Каждый язык программирования - это отдельный мир со своими допущениями, точно так же как и любой естественный язык. Когда я начал изучать немецкий, то обнаружил, что все существительные пишутся с прописной буквы… за исключением слова deutsch (немецкий язык). Я пожаловался профессору, подчеркивая, что ведь это же название самого языка. Он улыбнулся и ответил: «Не надо с этим бороться».
Профессор говорил, что надо позволить немцу оставаться немцем. Продолжая эту мысль, хочу дать совет всем, кто переходит на использование Ruby после освоения других языков. Пусть Ruby остается Ruby! Не ожидайте, что это будет Perl. Не требуйте от него поведения, характерного для языков LISP или Smalltalk. С другой стороны, у Ruby есть элементы, присущие любому из этих трех языков. Для начала действуйте в соответствии с априорными представлениями, но когда они оказываются неверны, не боритесь с установленными правилами (если только Мац не согласится с тем, что в них необходимо внести изменения).
Каждый программист сегодня знает о принципе ортогональности (хотя лучше было бы назвать его принципом ортогональной полноты). Вообразим пару осей, по одной из которых откладываются языковые сущности, а по другой — множество атрибутов и возможностей. Когда мы говорим об ортогональности, то обычно имеем в виду, что пространство, определяемое этими осями, настолько «полно», насколько это логически возможно.
Одна из составных частей Пути Ruby — стремление к ортогональности. Массив в некоторых отношениях подобен хэшу, а потому и операции над ними должны быть похожи. До тех пор пока мы не вступаем в область, где эти сущности начинают отличаться друг от друга.
Мац говорит, что «естественность» важнее ортогональности. Но чтобы понять, что естественно, а что нет, надо долго думать и писать программы.
Ruby стремится быть дружелюбным к программисту. Например, у многих методов есть синонимы; оба метода size и length возвращают число элементов в массиве. Два разных написания слова — indexes и indices — относятся к имени одного и того же метода. Некоторые называют это досадным недоразумением, но я склонен считать такую избыточность хорошим дизайном.
Ruby стремится к последовательности и единообразию. В этом нет ничего мистического: во всех жизненных ситуациях мы жаждем регулярности и размеренности. Сложнее научиться понимать, когда этот принцип следует нарушить.
Например, в Ruby принято добавлять вопросительный знак (?) в конец имени метода, ведущего себя как предикат. Это хорошо и удобно, программа становится яснее, а в пространстве имен легче ориентироваться. Но менее последовательным является аналогичное употребление восклицательного знака для обозначения потенциально «деструктивных» или «опасных» методов (в том смысле, что они модифицируют внутреннее состояние вызывающего объекта). Непоследовательность состоит в том, что не все деструктивные методы помечаются таким образом. Нужно ли восстановить справедливость?
Нет, на самом деле не нужно. Некоторые методы по сути своей изменяют состояние (например, методы replace и concat класса Array). Одни являются «методами установки», которые допускают присваивание атрибуту класса; ясно, что не следует добавлять восклицательный знак к имени атрибута или к знаку равенства. Другие в каком-то смысле изменяют состояние объекта, например read, но это происходит так часто, что нет смысла особо отмечать данный факт. Если бы имя каждого деструктивного метода заканчивалось символом !, то программа превратилась бы в рекламную брошюру фирмы, занимающейся многоуровневым маркетингом.
Вы замечаете действие разнонаправленных сил, тенденцию нарушать все правила? Тогда позвольте мне сформулировать второй закон Фултона: «У каждого правила есть исключения, кроме второго закона Фултона». (Доля шутки тут есть, но небольшая.)
В Ruby мы видим не «педантичную непротиворечивость», а строгое следование набору простых правил. Может быть, отчасти Путь Ruby состоит в том, что его подход не является закостенелым и неподвижным. Мац как-то сказал, что при проектировании языка нужно «следовать велениям своего сердца». И еще один аспект философии Ruby: «Не бойтесь изменений во время выполнения, не бойтесь быть динамичными». Мир динамичен, так почему язык программирования должен быть статичным? Ruby — один из самых динамичных среди существующих языков.
С некоторыми оговорками я бы выделил и такой аспект: «Не будьте рабом производительности». Если производительность оказывается недопустимо низкой, проблему придется решать, но не следует с самого начала выводить ее на первый план. Предпочитайте элегантность эффективности в тех случаях, когда эффективность не слишком критична. Впрочем, когда вы пишете библиотеку, которая будет использоваться непредвиденными способами, о производительности следует задуматься с самого начала.
Когда я смотрю на язык Ruby, то вижу равновесие между разными проектными целями, вижу сложное взаимодействие, напоминающее о задаче
Источником Ruby стала человеческая потребность создавать полезные и красивые вещи. Программы, написанные на Ruby, должны проистекать из того же боговдохновенного источника. Это, на мой взгляд, и является квинтэссенцией Пути Ruby.
Глава 1. Обзор Ruby
Язык формирует способ нашего мышления и определяет то, о чем мы можем размышлять.
Стоит напомнить, что в новом языке программирования иногда видят панацею, особенно его адепты. Но ни один язык не сможет заменить все остальные. Не существует инструмента, безусловно пригодного для решения любой мыслимой задачи. Есть много предметных областей и много ограничений, налагаемых решаемыми в них задачами.
А самое главное — есть разные пути обдумывания задач, и это следствие разного опыта и личных качеств самих программистов. Поэтому в обозримой перспективе будут появляться все новые и новые языки. А пока есть много языков, будет много людей, которые их критикуют и защищают. Короче говоря, «языковым войнам» конца не предвидится, но мы в этой книге не станем принимать в них участия.
И тем не менее в постоянном поиске новой, более удачной системы записи программ нас иногда озаряют идеи, переживающие контекст, в котором зародились. Как Pascal многое позаимствовал у Algol, как Java выросла из С, так и каждый язык что-то берет у своих предшественников.
Язык - это одновременно набор инструментов и площадка для игр. У него есть практическая сторона, но он же служит и полигоном для испытания новых идей, которые могут быть приняты или отвергнуты сообществом программистов.
Одна из наиболее далеко идущих идей — концепция объектно-ориентированного программирования (ООП). Многие скажут, что значимость ООП имеет скорее эволюционный, нежели революционный характер, но никто не возразит против того, что оно оказало огромное влияние на индустрию. Двадцать пять лет назад объектная ориентированность представляла в основном академический интерес; сегодня это универсально принятая парадигма.
Вездесущность ООП породила много «рекламной чепухи» в индустрии. В классической работе, написанной в конце 1980-х годов, Роджер Кинг отметил: «Если вы хотите продать кошку специалисту по компьютерам, скажите, что она объектно-ориентированная». Мнения по поводу того, что на самом деле представляет собой ООП, весьма неоднородны, и даже среди тех, кто разделяет общую точку зрения, имеются разногласия относительно терминологии.
Мы не ставим себе целью поучаствовать в спорах. Мы согласны, что ООП — полезный инструмент и ценная методология решения задач; мы не утверждаем, что она способна излечить рак.
Что касается истинной природы ООП, то у нас есть любимые определения и термины, но мы пользуемся ими лишь для эффективного общения, так что пререкаться по поводу смысла слов не станем.
Обо всем этом пришлось сказать лишь потому, что знакомство с основами ООП необходимо для чтения этой книги и понимания примеров и подходов. Что бы ни говорили о Ruby, он безусловно является объектно-ориентированным языком.
1.1. Введение в объектно-ориентированное программирование
Прежде чем начать разговор о самом языке Ruby, неплохо было бы потолковать об объектно-ориентированном программировании вообще. Поэтому сейчас мы вкратце рассмотрим общие идеи, лишь слегка касаясь Ruby.
1.1.1. Что такое объект
В объектно-ориентированном программировании объект — фундаментальное понятие.
Важно отметить, что любой объектно-ориентированный язык предоставляет механизм инкапсуляции. В общепринятом смысле это означает, во-первых, что атрибуты и методы объекта ассоциированы именно с этим объектом, а во-вторых, что область видимости атрибутов и методов по умолчанию ограничена самим объектом (применение принципа сокрытия информации).
Объект считается экземпляром класса объекта (обычно он называется просто
Создание объекта (экземпляра класса) называют
Иногда возникает ситуация, когда некоторые данные имеют широкую область видимости, не ограниченную одним объектом, и помещать копию такого атрибута в каждый экземпляр класса неправильно. Рассмотрим, к примеру, класс MyDogs и три объекта этого класса: fido, rover и spot. У каждой собаки могут быть такие атрибуты, как возраст и дата вакцинации. Предположим, однако, что нужно сохранить еще и имя владельца всех собак. Можно, конечно, поместить его в каждый объект, но это пустая трата памяти, к тому же искажающая смысл дизайна. Ясно, что атрибут «имя владельца» принадлежит не отдельному объекту, а классу в целом. Такие атрибуты (синтаксис их определения в разных языках различен) называются
Есть немало ситуаций, в которых может понадобиться переменная класса. Допустим, например, что нужно знать, сколько всего было создано объектов некоторого класса. Можно было бы завести переменную класса, инициализировать ее нулем и увеличивать на единицу при создании каждого объекта. Эта переменная ассоциирована именно с классом, а не с каким-то конкретным объектом. С точки зрения области видимости она не отличается от любого другого атрибута, но существует лишь одна ее копия для всего множества объектов данного класса.
Чтобы отличить атрибуты класса от обыкновенных атрибутов, последние часто называют
Точно так же методы объекта служат для управления доступом к его атрибутам и предоставляют четко определенный интерфейс для этой цели. Но иногда желательно или даже необходимо определить метод, ассоциированный с самим классом. Неудивительно, что метод класса управляет доступом к переменным класса, кроме того, выполняя действия, распространяющиеся на весь класс, а не на какой-то конкретный объект. Как и в случае с атрибутами, мы будем считать, что метод принадлежит объекту, если явно не оговорено противное.
Стоит отметить, что в некотором смысле все методы являются методами класса. Не нужно думать, что, создав сто объектов, мы породили сотню копий кода методов! Однако правила ограничения области видимости гласят, что метод каждого объекта оперирует данными только того объекта, от имени которого вызван. Тем самым у нас создается иллюзия, будто методы объекта ассоциированы с самими объектами.
1.1.2. Наследование
Мы подходим к одной из самых сильных сторон ООП — наследованию.
Обычно наследование рассматривается на уровне класса. Если нам необходим какой-то класс, а в наличии имеется более общий, то можно определить свой класс так, чтобы он наследовал поведение уже существующего. Предположим, например, что есть класс Polygon, описывающий выпуклые многоугольники. Тогда класс прямоугольника Rectangle можно унаследовать от Polygon. При этом Rectangle будет иметь все атрибуты и методы класса Polygon. Так, может уже быть написан метод, вычисляющий периметр путем суммирования длин всех сторон. Если все было реализовано правильно, этот метод автоматически будет работать и для нового класса; переписывать код не придется.
Если класс B наследует классу A, мы говорим, что B является подклассом A, а A — суперкласс B. По-другому говорят, что А — базовый или родительский класс, а B — производный или дочерний класс.
Как мы видели, производный класс может трактовать методы своего базового класса как свои собственные. С другой стороны, он может переопределить метод базового класса, предоставив иную его реализацию. Кроме того, в большинстве языков есть возможность вызвать из переопределенного метода метод базового класса с тем же именем. Иными словами, метод fоо класса B знает, как вызвать метод foo класса A. (Любой язык, не предоставляющий такого механизма, можно заподозрить в отсутствии истинной объектной ориентированности.) То же верно и в отношении атрибутов.
Отношение между классом и его суперклассом интересно и важно, обычно его называют отношением «является». Действительно, квадрат Square «является» прямоугольником Rectangle, а прямоугольник Rectangle «является» многоугольником Polygon и т.д. Поэтому, рассматривая иерархию наследования (а такие иерархии в том или ином виде присутствуют в любом объектно-ориентированном языке), мы видим, что в любой ее точке специализированные сущности «являются» подклассами более общих. Отметим, что это отношение транзитивно, — если обратиться к предыдущему примеру, то квадрат «является» многоугольником. Однако отношение «является» не коммутативно — каждый прямоугольник есть многоугольник, но не каждый многоугольник — прямоугольник.
Это подводит нас к теме множественного наследования. Можно представить себе класс, который наследует нескольким классам. Например, классы Dog (Собака) и Cat (Кошка) могут наследовать классу Mammal (Млекопитающее), а Sparrow (Воробей) и Raven (Ворон) — классу WingedCreature (Крылатое). Но как быть с классом Bat (ЛетучаяМышь)? Он с равным успехом может наследовать и Mammal, и WingedCreature! Это хорошо согласуется с нашим жизненным опытом, ведь многие вещи можно отнести не к одной категории, а сразу к нескольким, не вложенным друг в друга.
Множественное наследование, вероятно, наиболее противоречивая часть ООП. Некоторые указывают на потенциальные неоднозначности, требующие разрешения. Например, если в обоих классах Mammal и WingedCreature имеется атрибут size (размер) или метод eat (есть), то какой из них имеется в виду, когда мы обращаемся к нему из объекта класса Bat? С этой трудностью тесно связана проблема ромбовидного наследования; она называется так из-за формы диаграммы наследования, возникающей, когда оба суперкласса наследуют одному классу. Представьте себе, что классы Mammal и WingedCreature наследуют общему предку Organism (Организм); тогда иерархия наследования от Organism к Bat будет иметь форму ромба. Но как быть с атрибутами, которые оба промежуточных класса наследуют от своего родителя? Получает ли Bat две копии? Или они должны быть объединены в один атрибут, поскольку все равно заимствованы у общего предка?
Это скорее проблемы проектировщика языка, а не программиста. В разных объектно-ориентированных языках они решаются по-разному. Иногда вводятся правила, согласно которым какое-то одно определение атрибута «выигрывает». Либо же предоставляется возможность различать одноименные атрибуты. Иногда даже язык позволяет вводить псевдонимы или переименовывать идентификаторы. Многими это рассматривается как аргумент против множественного наследования — о механизмах разрешения подобных конфликтов имен нет единого мнения, поэтому все они «языкозависимы». В языке C++ предлагается минимальный набор средств для разрешения неоднозначностей; механизмы языка Eiffel, наверное, получше, а в Perl проблема решается совсем по-другому.
Есть и альтернатива — полностью запретить множественное наследование. Такой подход принят в языках Java и Ruby. На первый взгляд, это даже не назовешь компромиссным решением, но, вскоре мы убедимся, что все не так плохо, как кажется. Мы познакомимся с приемлемой альтернативой традиционному множественному наследованию, но сначала обсудим полиморфизм — еще одно понятие из арсенала ООП.
1.1.3. Полиморфизм
Термин «полиморфизм», наверное, вызывает самые жаркие семантические споры. Каждый знает, что это такое, но все понимают его по-разному. (Не так давно вопрос «Что такое полиморфизм?» стал популярным во время собеседования при поступлении на работу. Если его зададут вам, рекомендую процитировать какого-нибудь эксперта, например Бертрана Мейера или Бьерна Страуструпа; если собеседник не согласится, то пусть он спорит с классиком, а не с вами.)
Буквально слово «полиморфизм» означает «способность принимать разные формы или обличья». В самом широком смысле так называют ситуацию, когда различные объекты по-разному отвечают на одно и то же сообщение или вызов метода.
Дамиан Конвей (Damian Conway) в книге «Object-Oriented Perl» проводит смысловое различие между двумя видами полиморфизма. Первый,
Если некоторый класс наследует своему суперклассу, то по определению все методы суперкласса присутствуют также и в подклассе. Таким образом, цепочка наследования представляет собой линейную иерархию классов, отвечающих на одни и те же методы. Нужно, конечно, помнить, что в любом подклассе метод может быть переопределен; именно это и составляет сильную сторону наследования. При вызове метода объекта обычно отвечает либо метод, унаследованный от суперкласса, либо более специализированный вариант этого метода, созданный в интересах именно данного подкласса.
В языках со статической типизацией, например в C++, наследственный полиморфизм гарантирует совместимость типов вниз по цепочке наследования (но не в обратном направлении). Скажем, если B наследует A, то указатель на объект класса А может указывать и на объект класса в; обратное же неверно. Совместимость типов — существенная черта ООП в подобных языках, можно даже сказать, что полиморфизм ей и исчерпывается. Но, конечно же, полиморфизм можно реализовать и в отсутствие статической типизации (как в Ruby).
Второй вид полиморфизма, упомянутый Конвеем, — это
Читатели, знакомые с языком Java, понимают, что в нем реализованы оба вида полиморфизма. Класс в Java может расширять другой класс, наследуя ему с помощью ключевого слова extends, а может с помощью ключевого слова implements реализовывать интерфейс, за счет чего приобретает заранее известный набор методов (которые необходимо переопределить). Такой синтаксис позволяет интерпретатору Java во время компиляции определить, можно ли вызывать данный метод для конкретного объекта.
Ruby поддерживает интерфейсный полиморфизм, но по-другому. Он позволяет определять модули, методы которых допускается «подмешивать» к существующим классам. Но обычно модули так не используются. Модуль состоит из методов и констант, которые можно использовать так, будто они являются частью класса или объекта. Когда модуль подмешивается с помощью предложения include, мы получаем ограниченную форму множественного наследования. (По словам проектировщика языка Юкихиро Мацумото, это можно рассматривать как одиночное наследование с разделением реализации.) Таким образом удается сохранить преимущества множественного наследования, не страдая от его недостатков.
1.1.4. Еще немного терминов
В языках, подобных C++, существует понятие
Создатель языка C++ Бьерн Страуструп определяет также понятие
Считается, что некоторые языки поддерживают более «чистую» модель ООП, чем другие. (К ним мы применяем термин «радикально объектно-ориентированный».) Это означает, что любая сущность в языке является объектом, даже примитивные типы представлены полноценными классами, а переменные и константы рассматриваются как экземпляры. В таких языках, как Java, C++ и Eiffel, дело обстоит иначе. В них примитивные типы (особенно константы) не являются настоящими объектами, хотя иногда могут рассматриваться как таковые с помощью «классов-оберток». Вероятно, есть языки, которые более радикально объектно ориентированы, чем Ruby, но их немного.