Подытожить природу объектов можно двумя фразами: инкапсуляция функциональности и поддержка состояния. (Много лет назад я уже воспользовался этими фразами в своей книге REALbasic: The Definitive Guide.)

 

Инкапсуляция функциональности

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

Поддержка состояния

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

В качестве примера представьте себе объект, работа которого заключается в реализации стека, — это может быть экземпляр класса Stack. Стек — это структура данных, которая поддерживает набор данных в порядке LIFO (last in, first out; последним вошел — первым вышел). Она реагирует на два сообщения: push и pop. Сообщение push означает добавление в набор данных нового элемента. Сообщение pop означает удаление из стека элемента, который был помещен в него последним. Стек можно представить как стопку тарелок: тарелки помещают в стопку сверху и забирают из стопки также сверху, так что тарелка, помещенная в стопку первой, не может быть забрана из стопки, пока из нее не будут удалены все тарелки, помещенные туда после первой (рис. 2.4).

Стек

Рис. 2.4. Стек

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

Объект стека иллюстрирует поддержку состояния, потому что объект не просто способ доступа к данным стека — это сами данные стека. Другие объекты могут получить доступ к этим данным только в силу доступа к самому объекту стека и только таким образом, каким это позволяет делать объект стека. Данные стека полностью скрыты внутри объекта стека; никто другой не может их видеть. Все, что может сделать другой объект, — это послать объекту стека сообщения push и pop. Если на вершине стека имеется некоторый элемент, то любой объект, который отправит стеку сообщение pop, получит в ответ этот элемент. Если никакой объект не отправляет сообщение pop объекту стека, то этот элемент на вершине стека будет просто ожидать, когда он кому-нибудь потребуется.

За вторым примером философии и природы объектно-ориентированного программирования я хочу обратиться к воображаемому сценарию, использованному мною в книге о REALbasic. Представьте, что мы пишем игру, в которой пользователь “стреляет” в движущиеся цели, и при каждом попадании в цель растет счет. Думаю, у вас сразу же получается представление о том, как можно организовать код игры с использованием объектно-ориентиро-ванного программирования.

  • В игре имеется класс целевого объекта Target. Каждый целевой объект является экземпляром этого класса. Такое решение имеет смысл, потому что мы хотим, чтобы все цели вели себя одним и тем же образом. Цель должна знать, как изобразить себя на экране, и это знание является частью целевого класса — что имеет смысл, поскольку все цели будут рисовать себя одинаково. Таким образом, здесь мы имеем взаимоотношения между классом и экземпляром.
  • Цели могут рисовать себя одним и тем же способом, но при этом они могут отличаться внешним видом. Возможно, некоторые из целей синие, другие красные, и так далее. Такое различие между отдельными целями может быть выражено с помощью переменной экземпляра; назовем ее color. Каждый раз при создании экземпляра мы будем назначать цвет объект. Код класса Target для рисования отдельных объектов будет получать значение переменной экземпляра color объекта и использовать его при рисовании. Очевидно, что можно не ограничиваться цветом: цели могут иметь различные размеры, формы и так далее, и все эти различия можно описывать с помощью переменных экземпляра. Таким образом, у нас есть как инкапсуляция функциональных возможностей, так и поддержка состояния. Объект цели имеет состояние (параметры, которые описывают, как цель должна выглядеть на экране), а также возможность изображать самого себя, т.е. визуально выражать это состояние.
  • Когда пользователь попадает в цель, она взрывается. Вероятно, класс Target будет иметь метод экземпляра explode, и, таким образом, каждый объект цели знает, как надо взрываться. При взрыве цели должно выполниться еще одно действие — увеличение счета пользователя. Соответственно, представим еще один объект счета — экземпляр класса Score. Когда цель взрывается, одно из действий, выполняемых методом explode, — отправка сообщения increase (увеличить) объекту счета. Таким образом, у нас есть как инкапсуляции функциональных возможностей, так и поддержка состояния. Объект счета одинаково реагирует на любой объект, который отправляет ему сообщение increase; ему не нужно знать, почему он получил сообщение. Ему не надо знать ни о существовании объекта цели, ни о том, что сам он является частью игры — он просто поддерживает счет и при получении сообщения increase увеличивает его.

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

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

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

Эффективное применение объектно-ориентированного программирования для того, чтобы программа делала то, что вы от нее хотите, и при этом оставалась понятной и легко поддерживаемой — скорее искусство; по мере приобретения опыта ваши способности также будут улучшаться. Возможно, вы захотите почитать дополнительную литературу по эффективному планированию и построению архитектуры объектно-ориентированных программ. В таком случае позвольте порекомендовать вам две классические книги. Это Refactoring Мартина Фаулера (Martin Fowler) (Addison-Wesley, 1999), в которой описано, как почувствовать, как именно следует распределить методы по классам (и как победить свой страх перед этой работой). Другой книгой является Design Patterns Эриха Гаммы (Erich Gamma), Ричарда Хелма (Richard Helm), Ральфа Джонсона (Ralph Johnson) и Джона Влиссидеса (John Vlissides) (известных также как “Банда четырех”) (Addison-Wesley, 1994), которая фактически стала Библией в области проектирования объектно-ориентированных программ.


Похожие статьи

 

 

 

Добавить комментарий