2

2


В Java также есть доступ «по умолчанию», используемый при отсутствии како¬го-либо из перечисленных спецификаторов. Он также иногда называется дос¬тупом в пределах пакета (package access), поскольку классы могут использовать дружественные члены других классов из своего пакета, но за его пределами те же дружественные члены приобретают статус private.



Повторное использование реализации



Созданный и протестированный класс должен (в идеале) представлять собой полезный блок кода. Однако оказывается, что добиться этой цели гораздо труд¬нее, чем многие полагают; для разработки повторно используемых объектов требуется опыт и понимание сути дела. Но как только у вас получится хорошая конструкция, она будет просто напрашиваться на внедрение в другие программы. Многократное использование кода — одно из самых впечатляющих преиму¬ществ объектно-ориентированных языков.



Проще всего использовать класс повторно, непосредственно создавая его объект, но вы можете также поместить объект этого класса внутрь нового класса. Мы называем это внедрением объекта. Новый класс может содержать любое ко¬личество объектов других типов, в любом сочетании, которое необходимо для достижения необходимой функциональности. Так как мы составляем новый класс из уже существующих классов, этот способ называется композицией (если композиция выполняется динамически, она обычно именуется агрегировани¬ем). Композицию часто называют связью типа «имеет» (has-a), как, например, в предложении «у автомобиля есть двигатель».



Автомобиль  Двигатель






(На UML-диаграммах композиция обозначается закрашенным ромбом. Я несколько упрощу этот формат: оставлю только простую линию, без ромба, чтобы обозначить связь .)



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



Наследование играет важную роль в объектно-ориентированном програм¬мировании, поэтому на нем часто акцентируется повышенное внимание, и но¬вичок может подумать, что наследование должно применяться повсюду. А это чревато созданием неуклюжих и излишне сложных решений. Вместо этого при создании новых классов прежде всего следует оценить возможность компози¬ции, так как она проще и гибче. Если вы возьмете на вооружение рекомендуе¬мый подход, ваши программные конструкции станут гораздо яснее. А по мере накопления практического опыта понять, где следует применять наследование, не составит труда.



Наследование



Сама по себе идея объекта крайне удобна. Объект позволяет совмещать данные и функциональность на концептуальном уровне, то есть вы можете представить нужное понятие проблемной области прежде, чем начнете его конкретизиро¬вать применительно к диалекту машины. Эти концепции и образуют фундамен¬тальные единицы языка программирования, описываемые с помощью ключево¬го слова class.



(Стрелка на UML-диаграмме направлена от производного класса к базовому классу. Как вы вскоре увидите, может быть и больше одного производного класса.)



Но согласитесь, было бы обидно создавать какой-то класс, а потом проделы¬вать всю работу заново для похожего класса. Гораздо рациональнее взять гото¬вый класс, «клонировать» его, а затем внести добавления и обновления в полу¬ченный клон. Это именно то, что вы получаете в результате наследования, с одним исключением — если изначальный класс (называемый также базовым- классом, суперклассом или родительским классом) изменяется, то все измене¬ния отражаются и на его «клоне» (называемом производным классом, унаследо¬ванным классом, подклассом или дочерним классом).









Тип определяет не только свойства группы объектов; он также связан с дру¬гими типами. Два типа могут иметь общие черты и поведение, но различаться количеством характеристик, а также способностью обработать большее число сообщений (или обработать их по-другому). Для выражения этой общности ти¬пов при наследовании используется понятие базовых и производных типов. Ба¬зовый тип содержит все характеристики и действия, общие для всех типов, про¬изводных от него. Вы создаете базовый тип, чтобы представить основу своего представления о каких-то объектах в вашей системе. От базового типа порож¬даются другие типы, выражающие другие реализации этой сущности.



Например, машина по переработке мусора сортирует отходы. Базовым ти¬пом будет «мусор», и каждая частица мусора имеет вес, стоимость и т. п., и мо¬жет быть раздроблена, расплавлена или разложена. Отталкиваясь от этого, на¬следуются более определенные виды мусора, имеющие дополнительные характеристики (бутылка имеет цвет) или черты поведения (алюминиевую банку можно смять, стальная банка притягивается магнитом). Вдобавок, неко¬торые черты поведения могут различаться (стоимость бумаги зависит от ее типа и состояния). Наследование позволяет составить иерархию типов, описы¬вающую решаемую задачу в контексте ее типов.



Второй пример — классический пример с геометрическими фигурами. Базо¬вым типом здесь является «фигура», и каждая фигура имеет размер, цвет, рас¬положение и т. п. Каждую фигуру можно нарисовать, стереть, переместить, за¬красить р т. д. Далее производятся (наследуются) конкретные разновидности фигур: окружность, квадрат, треугольник и т. п., каждая из которых имеет свои дополнительные характеристики и черты поведения. Например, для некоторых фигур поддерживается операция зеркального отображения. Отдельные черты поведения могут различаться, как в случае вычисления площади фигуры. Ие¬рархия типов воплощает как схожие, так и различные свойства фигур.






Приведение решения к понятиям, использованным в примере, чрезвычайно удобно, потому что вам не потребуется множество промежуточных моделей, связывающих описание решения с описанием задачи. При работе с объектами первичной моделью становится иерархия типов, так что вы переходите от опи¬сания системы реального мира прямо к описанию системы в программном коде. На самом деле одна из трудностей в объектно-ориентированном планировании состоит в том, что уж очень просто вы проходите от начала задачи до конца ре¬шения. Разум, натренированный на сложные решения, часто заходит в тупик при использовании простых подходов.



Используя наследование от существующего типа, вы создаете новый тип. Этот новый тип не только содержит все члены существующего типа (хотя члены, помеченные как private, скрыты и недоступны), но и, что еще важнее, повторя¬ет интерфейс базового класса. Значит, все сообщения, которые вы могли по¬сылать базовому классу, вы также вправе посылать и производному классу. А так как мы различаем типы классов по совокупности сообщений, которые мо¬жем им посылать, это означает, что производный класс является частным слу¬чаем базового класса. В предыдущем примере «окружность есть фигура». Экви¬валентность типов, достигаемая при наследовании, является одним из осно¬вополагающих условий понимания смысла объектно-ориентированного про¬граммирования.



Так как и базовый, и производный классы имеют одинаковый основной ин¬терфейс, должна существовать и реализация для этого интерфейса. Другими словами, где-то должен быть код, выполняемый при получении объектом опре¬деленного сообщения. Если вы просто унаследовали класс и больше не пред¬принимали никаких действий, методы из интерфейса базового класса перейдут в производный класс без изменений. Это значит, что объекты производного класса не только однотипны, но и обладают одинаковым поведением, а при этом само наследование теряет смысл.



Существует два способа изменения нового класса по сравнению с базовым классом. Первый достаточно очевиден: в производный класс включаются новые методы. Они уже не являются частью интерфейса базового класса. Видимо, ба¬зовый класс не делал всего, что требовалось в данной задаче, и вы дополнили его новыми методам. Впрочем, такой простой и примитивный подход к насле¬дованию иногда оказывается идеальным решением проблемы. Однако надо внимательно рассмотреть, действительно ли базовый класс нуждается в этих дополнительных методах. Процесс выявления закономерностей и пересмотра архитектуры является повседневным делом в объектно-ориентированном про¬граммировании.









Хотя наследование иногда наводит на мысль, что интерфейс будет дополнен новыми методами (особенно в Java, где наследование обозначается ключевым словом extends, то есть «расширять»), это совсем не обязательно. Второй, более важный способ модификации классов заключается в изменении поведения уже существующих методов базового класса. Это называется переопределением (или замещением) метода.






Для замещения метода нужно просто создать новое определение этого мето¬да в производном классе. Вы как бы говорите: «Я использую тот же метод ин¬терфейса, но хочу, чтобы он выполнял другие действия для моего нового типа».



Отношение «является» в сравнении с «похоже»



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



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



Предположим, что ваш дом снабжен всем необходимым оборудованием для контроля процесса охлаждения. Представим теперь, что кондиционер сломался и вы заменили его обогревателем, способным как нагревать, так и охлаждать. Обогреватель «похож на» кондиционер, но он способен и на большее. Так как система управления вашего дома способна контролировать только охлаждение, она ограничена в коммуникациях с охлаждающей частью нового объекта. Ин¬терфейс нового объекта был расширен, а существующая система ничего не при¬знает, кроме оригинального интерфейса.






Конечно, при виде этой иерархии становится ясно, что базовый класс «охла¬ждающая система» недостаточно гибок; его следует переименовать в «систему контроля температуры» так, чтобы он включал и нагрев, — и после этого зара¬ботает принцип замены. Тем не менее эта диаграмма представляет пример того, что может произойти в реальности.



После знакомства с принципом замены может возникнуть впечатление, что этот подход (полная замена) — единственный способ разработки. Вообще говоря, если ваши иерархии типов так работают, это действительно хорошо. Но в неко¬торых ситуациях совершенно необходимо добавлять новые методы к интерфей¬су производного класса. При внимательном анализе оба случая представляются достаточно очевидными.



Взаимозаменяемые объекты и полиморфизм



При использовании иерархий типов часто приходится обращаться с объектом определенного типа как с базовым типом. Это позволяет писать код, не завися¬щий от конкретных типов. Так, в примере с фигурами методы манипулируют просто фигурами, не обращая внимания на то, являются ли они окружностями, прямоугольниками, треугольниками или некоторыми еще даже не определен¬ными фигурами. Все фигуры могут быть нарисованы, стерты и перемещены, а методы просто посылают сообщения объекту «фигура»; им безразлично, как объект обойдется с этим сообщением.

Report Page