Таким образом, первой причиной для ограничения доступа является необхо¬димость уберечь «хрупкие» детали от программиста-клиента — части внутрен¬ней «кухни», не являющиеся составляющими интерфейса, при помощи которого пользователи решают свои задачи. На самом деле это полезно и пользователям — они сразу увидят, что для них важно, а что они могут игнорировать.
Вторая причина появления ограничения доступа — стремление позволить разработчику библиотеки изменить внутренние механизмы класса, не беспоко¬ясь о том, как это работает на программисте-клиенте. Например, вы можете реализовать определенный класс «на скорую руку», чтобы ускорить разработку программы, а затем переписать его, чтобы повысить скорость работы. Если вы правильно разделили и защитили интерфейс и реализацию, сделать это будет совсем несложно.
Java использует три явных ключевых слова, характеризующих уровень дос¬тупа: public, private и protected. Их предназначение и употребление очень про¬сты. Эти спецификаторы доступа определяют, кто имеет право использовать следующие за ними определения. Слово public означает, что последующие опре¬деления доступны всем. Наоборот, слово private значит, что следующие за ним предложения доступны только создателю типа, внутри его методов. Термин private — «крепостная стена» между вами и программистом-клиентом. Если кто-то по¬пытается использовать private-члены, он будет остановлен ошибкой компиля¬ции. Спецификатор protected действует схоже с private, за одним исключени¬ем — производные классы имеют доступ к членам, помеченным protected, но не имеют доступса к private-членам (наследование мы вскоре рассмотрим).
В Java также есть доступ «по умолчанию», используемый при отсутствии како¬го-либо из перечисленных спецификаторов. Он также иногда называется дос¬тупом в пределах пакета (package access), поскольку классы могут использовать дружественные члены других классов из своего пакета, но за его пределами те же дружественные члены приобретают статус private.
Повторное использование реализации
Созданный и протестированный класс должен (в идеале) представлять собой полезный блок кода. Однако оказывается, что добиться этой цели гораздо труд¬нее, чем многие полагают; для разработки повторно используемых объектов требуется опыт и понимание сути дела. Но как только у вас получится хорошая конструкция, она будет просто напрашиваться на внедрение в другие программы. Многократное использование кода — одно из самых впечатляющих преиму¬ществ объектно-ориентированных языков.
Проще всего использовать класс повторно, непосредственно создавая его объект, но вы можете также поместить объект этого класса внутрь нового класса. Мы называем это внедрением объекта. Новый класс может содержать любое ко¬личество объектов других типов, в любом сочетании, которое необходимо для достижения необходимой функциональности. Так как мы составляем новый класс из уже существующих классов, этот способ называется композицией (если композиция выполняется динамически, она обычно именуется агрегировани¬ем). Композицию часто называют связью типа «имеет» (has-a), как, например, в предложении «у автомобиля есть двигатель».
Автомобиль Двигатель
(На UML-диаграммах композиция обозначается закрашенным ромбом. Я несколько упрощу этот формат: оставлю только простую линию, без ромба, чтобы обозначить связь .)
Композиция — очень гибкий инструмент. Объекты-члены вашего нового класса обычно объявляются закрытыми (private), что делает их недоступными для программистов-клиентов, использующих класс. Это позволяет вносить изме¬нения в эти объекты-члены без модификации уже существующего клиентского кода. Вы можете также изменять эти члены во время исполнения программы, чтобы динамически управлять поведением вашей программы. Наследование, описанное ниже, не имеет такой гибкости, так как компилятор накладывает оп¬ределенные ограничения на классы, созданные с применением наследования.
Наследование играет важную роль в объектно-ориентированном програм¬мировании, поэтому на нем часто акцентируется повышенное внимание, и но¬вичок может подумать, что наследование должно применяться повсюду. А это чревато созданием неуклюжих и излишне сложных решений. Вместо этого при создании новых классов прежде всего следует оценить возможность компози¬ции, так как она проще и гибче. Если вы возьмете на вооружение рекомендуе¬мый подход, ваши программные конструкции станут гораздо яснее. А по мере накопления практического опыта понять, где следует применять наследование, не составит труда.
Наследование
Сама по себе идея объекта крайне удобна. Объект позволяет совмещать данные и функциональность на концептуальном уровне, то есть вы можете представить нужное понятие проблемной области прежде, чем начнете его конкретизиро¬вать применительно к диалекту машины. Эти концепции и образуют фундамен¬тальные единицы языка программирования, описываемые с помощью ключево¬го слова class.
(Стрелка на UML-диаграмме направлена от производного класса к базовому классу. Как вы вскоре увидите, может быть и больше одного производного класса.)
Но согласитесь, было бы обидно создавать какой-то класс, а потом проделы¬вать всю работу заново для похожего класса. Гораздо рациональнее взять гото¬вый класс, «клонировать» его, а затем внести добавления и обновления в полу¬ченный клон. Это именно то, что вы получаете в результате наследования, с одним исключением — если изначальный класс (называемый также базовым- классом, суперклассом или родительским классом) изменяется, то все измене¬ния отражаются и на его «клоне» (называемом производным классом, унаследо¬ванным классом, подклассом или дочерним классом).
Тип определяет не только свойства группы объектов; он также связан с дру¬гими типами. Два типа могут иметь общие черты и поведение, но различаться количеством характеристик, а также способностью обработать большее число сообщений (или обработать их по-другому). Для выражения этой общности ти¬пов при наследовании используется понятие базовых и производных типов. Ба¬зовый тип содержит все характеристики и действия, общие для всех типов, про¬изводных от него. Вы создаете базовый тип, чтобы представить основу своего представления о каких-то объектах в вашей системе. От базового типа порож¬даются другие типы, выражающие другие реализации этой сущности.
Например, машина по переработке мусора сортирует отходы. Базовым ти¬пом будет «мусор», и каждая частица мусора имеет вес, стоимость и т. п., и мо¬жет быть раздроблена, расплавлена или разложена. Отталкиваясь от этого, на¬следуются более определенные виды мусора, имеющие дополнительные характеристики (бутылка имеет цвет) или черты поведения (алюминиевую банку можно смять, стальная банка притягивается магнитом). Вдобавок, неко¬торые черты поведения могут различаться (стоимость бумаги зависит от ее типа и состояния). Наследование позволяет составить иерархию типов, описы¬вающую решаемую задачу в контексте ее типов.
Второй пример — классический пример с геометрическими фигурами. Базо¬вым типом здесь является «фигура», и каждая фигура имеет размер, цвет, рас¬положение и т. п. Каждую фигуру можно нарисовать, стереть, переместить, за¬красить р т. д. Далее производятся (наследуются) конкретные разновидности фигур: окружность, квадрат, треугольник и т. п., каждая из которых имеет свои дополнительные характеристики и черты поведения. Например, для некоторых фигур поддерживается операция зеркального отображения. Отдельные черты поведения могут различаться, как в случае вычисления площади фигуры. Ие¬рархия типов воплощает как схожие, так и различные свойства фигур.