Объектно-ориентированное программирование

Т. Бадд

Введение.

  1. Введение и общий замысел. Глава 1 дает неформальное определение базовых концепцийобъектно-ориентированного программирования. Глава2 вводит принцип разработки на основе обязанностей. Эти две главы являются фундаментальными, и их следует изучить подробно. В частности, я настоятельно рекомендую выполнить по крайней мере одно упражнение сCRC-карточками из главы2. Техника CRC-карточек, по моему мнению, является одной из лучших для определения функциональности, ответственности и инкапсуляции при базовой разработке проекта.
  2. Классы, методы и сообщения Главы 3 и4 определяют синтаксис, используемый в языках Smalltalk, C++, Java, Objective-C иObject Pascal для задания классов,

методов и посылки сообщений. Глава3 заостряет внимание на статических свойствах (классах и методах), в то время как глава4 описывает динамические аспекты(создание объектов и пересылку сообщений). Главы5 и6 развивают эти идеи. Здесь же начинаются обучающие примеры— образцы программ, разработанных в объектно-ориентированной манере и иллюстрирующих различные черты объектной техники.

III. Наследование и повторное использование кода Главы 7, 8 и9 вводят концепцию

наследования и объясняют ее применение для обеспечения повторного использования кода. Пример из главы8, написанный на языке Java, иллюстрирует также применение стандартного прикладного программного интерфейса(API —

application program interface). В главе9 противопоставляются наследование и

композиция в качестве альтернативных техник обеспечения повторного использования кода.

  1. Более подробно о наследовании. В главах с10 по13 концепция наследования анализируется более детально. Введение наследования оказывает влияние на почти все аспекты языка программирования, которое зачастую не сразу очевидно для начинающего. В главе10 обсуждается поиск методов и их связывание с сообщениями. Там же иллюстрируется тот факт, что подклассы и подтипы— это не одно и то же. В главе11 обсуждается семантика переопределения методов и отмечаются две совершенно различные интерпретации этого понятия. В главе12

продолжается тема переопределения и исследуются некоторые следствия наследования применительно к механизмам управления памятью, присваивания и сравнения. Наконец, в главе13 изучается множественное наследование.

  1. Полиморфизм. В значительной степени мощь объектно-ориентированного программирования проистекает из применения различных форм полиморфизма. В главе14 читатель знакомится с основными механизмами полиморфизма в объектно-ориентированных языках и двумя показательными обучающими примерами. Первый пример в главе15 рассматривает создание библиотек общего назначения. Конкретная библиотека, а именно недавно разработанная стандартная

библиотека шаблонов (STL — Standard Template Library)для языка С++,

обсуждается в главе 16.

  1. Разработка программного обеспечения. В главе17 обсуждается ряд стандартных тем компьютерной инженерии в контексте объектно-ориентированного программирования. Глава18 знакомит с несколькими относительно новыми концепциями— средой разработки приложений и шаблонами разработки. Оба подхода основаны на использовании наборов классов. Наконец, в главе19 приводится конкретный пример среды разработки.

VII. Продвинутое изучение. Концепция классов при внимательном рассмотрении не столь проста, как нас пытались убедить в главе 3. В главе 20 рассмотрены более глубокие аспекты объектно-ориентированного программирования. Там же обсуждаются делегирование (являющееся примером объектно-ориентированного программирования без классов) и понятие метакласса(на уровне собственно языка программирования). В главе 21 в общих чертах описаны разнообразные техники реализации, применяющиеся при создании объектно-ориентированных языков.

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

Первое издание книги я закончил главой  «Дополнительная информация». К сожалению, объектно-ориентированное программирование развивается так быстро, что любая дополнительная информация почти сразу устаревает. Поэтому я не включил во второе издание главу с таким названием. Вместо этого я попытаюсь поддерживать страничку Web с последними сведениями.

Как получить исходные тексты

Исходные тексты обучающих примеров, представленных в книге, можно получить анонимно, обратившись через ftp по адресуftp.cs.orst.edu, каталог/pub/budd/oopintro. В том же каталоге можно будет найти дополнительную информацию, например список ошибок, обнаруженных в книге, упражнения, копии  «прозрачек», которые я использую в своем курсе. Все это можно также увидеть через World Wide Web на моих личных домашних страницах по адресу http://www.cs.orst.edu/~budd/oopintro. Вопросы вы можете посылать электронной почтой по адресуbudd@cs.orst.edu или обычной почтой: Professor Timothy A. Budd, Department of Computer Science, Oregon State University, Corvallis, Oregon, 97331.

Что требуется знать для чтения книги

Я предполагаю, что читатель знаком хотя бы с одним традиционным языком программирования, например Pascal или С. Мои курсы были вполне успешно восприняты студентами последнего года undegraduate level и первого graduate level. В некоторых случаях(особенно в последней четверти книги) более глубокие знания окажутся полезны, но они не являются обязательными. Например, студент, который специализируется на разработке программного обеспечения, легче воспримет материал главы17, а обучающийся построению компиляторов сочтет главу21 вполне понятной. Тематику обеих глав можно упростить при необходимости.

Глава 1 : Объектно-ориентированное мышление

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

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

∙ООП — это революционная идея, совершенно непохожая на что-либо выдвигавшееся в программировании.

∙ООП — это эволюционный шаг, естественным образом вытекающий из предшествующей истории.

1.1. Почему ООП так популярно?

Я перечислю некоторые (на мой взгляд— самые главные) причины огромной популярности объектно-ориентированного программирования в последнее десятилетие:

∙надежда, что ООП может просто и быстро привести к росту продуктивности и улучшению надежности программ, помогая тем самым разрешить кризис в программном обеспечении;

∙желание перейти от существующих языков программирования к новой технологии;

∙вдохновляющее сходство с идеями, родившимися в других областях.

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

Несмотря на то что объектно-ориентированное программирование действительно помогает при создании сложных программных систем, важно помнить, что ООП не является  «серебряной пулей» (термин, ставший популярным благодаря Фреду Бруксу[Brooks 1987]), которая запросто справляется с чудовищем. Программирование по- прежнему является одной из наиболее трудных задач, взваливаемых на себя человеком. Чтобы стать профессионалом в программировании, необходимы талант, способность к творчеству, интеллект, знания, логика, умение строить и использовать абстракции и, самое главное, опыт— даже в том случае, когда используются лучшие средства разработки.

Я подозреваю, что есть и другая причина особой популярности таких языков программирования, как C++ и Object Pascal (по контрасту соSmalltalk иBeta). Она состоит в том, что и администрация и разработчики надеются, что программист на языках C или Pascal может перейти на C++ или Object Pascal с той же легкостью, с которой происходит добавление нескольких букв на титульный лист сертификата о специальности. К сожалению, так происходит не всегда. Объектно-ориентированное программирование является новым пониманием того, что собственно называется вычислениями, а также того, как мы можем структурировать информацию внутри компьютера. Чтобы стать профессионалом в технике ООП, требуется полная переоценка привычных методов разработки программ.

1.2. Язык и мышление

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

 

иллюзия — полагать, что кто-то может согласовать себя с сущностью реальности без использования языка и что язык— всего лишь случайное средство решения конкретных задач общения или мышления. Суть вопроса в том, что  «реальный мир» в значительной степени неосознанно строится на языковых привычках группы людей… Мы видим, слышим и испытываем остальные ощущения так, как мы это делаем, в значительной степени потому, что языковые обычаи нашего общества предрасполагают к определенному выбору способа интерпретации.

Эдвард Сапир (цитировано по[Whorf 1956]).

Цитата подчеркивает тот факт, что язык, на котором мы говорим, непосредственно влияет на способ восприятия мира. Это справедливо не только для естественных языков, подобных тем, что изучались в начале двадцатого века американскими лингвистами Эдвардом Сапиром и Ли Ворфом, но также и для искусственных языков, наподобие тех, что мы используем в программировании.

1.2.1. Эскимосы и снег

Примером, почти повсеместно цитируемым(хотя зачастую ошибочно— см.

[Pillum 1991]) в качестве иллюстрации того, как язык влияет на мышление, является тот  «факт», что в эскимосских(или юитских) языках имеется множество слов для описания различных типов снежного покрова— мокрого, плотного, подмерзшего и т. д. Это-то как раз не является удивительным. Любое сообщество с общими интересами естественным образом разрабатывает специализированный словарь необходимых понятий.

Что действительно важно — не слишком абсолютизировать вывод, который мы можем сделать из этого простого наблюдения. Главное не в том, что глаз эскимосов в каком-то

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

Таким образом, различные языки(например, эвенкийский) могут привести(но не обязательно требуют этого) к тому, чтобы смотреть на мир с разных сторон.

Чтобы эффективно использовать ООП, требуется глядеть на мир иным способом. Само по себе применение объектно-ориентированного языка программирования(такого, как C++) не вынуждает статьобъектно-ориентированнымпрограммистом. Использованиеобъектно-ориентированногоязыка упрощает разработку объектно-ориентированных приложений, но, как было остроумно замечено,  «программа фортрановского типа может быть написана на любом языке».

1.2.2.Пример из области программирования

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

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

 

генетических исследований, столкнулся с необходимостью анализа последовательностей ДНК. Проблема могла быть сведена к относительно простой задаче. Молекула ДНК представляется в виде вектора из N целочисленных значений, где N очень велико(порядка десятков тысяч). Нужно было проверить, не является ли какой-либо участок длины M

(M — фиксированная константа порядка5–10) повторяющимся в последовательности ДНК.

ACTCGGATCTTGCATTTCGGCAATTGGACCCTGACTTGGCCA…

Программист, не долго думая, написал простую и прямолинейную программу на Fortran — нечто вроде

DO 10 I = 1, N-M

DO 10 J = 1, N-MFOUND=.TRUE.

DO 20 K = 1, M

20 IF (X(I+K-1).NE.X(J+K-1))FOUND=.FALSE. IF (FOUND) …

10 CONTINUE

Он был неприятно разочарован, когда пробные запуски программы показали, что она потребует многих часов для завершения работы. Студент обсудил эту проблему со студенткой, которая оказалась профессионалом в программировании на языке APL. Она сказала, что могла бы попробовать написать программу для решения этой задачи. Студент был в сомнении: Fortran известен как один из наиболее «эффективных» компилируемых языков, а APL реализовывался с помощью интерпретатора. Таким образом, тот факт, что APL-программист способен составить алгоритм, который требует для работы минуты, а не часы, был воспринят с определенной дозой недоверия.

APL-программист капере формулировала задачу. Вместо того чтобы работать с вектором из N элементов, она представила данные в виде матрицы, имеющей приблизительно N строк и M столбцов:

A C T C G G позиции 1 — M
C T C G G A позиции 2 — M+1
T C G G A T позиции 3 — M+2
C G G A T T позиции 4 — M+3
G G A T T C позиции 5 — M+4
G A T T C T позиции 6 — M+5
T . . . A C C
G G
G G A C C C

Затем студентка отсортировала матрицу по строкам. Если какой-то фрагмент оказывается повторяющимся, то в отсортированной матрице две соседние строки должны оказаться идентичными.

T . . . C C
G G A
T G G A C C
. . .

Проверка этого условия оказывается тривиальной задачей. Причина, по которой APL- программа оказалась быстрее, не имела ничего общего со скоростью работы APL по сравнению с Fortran. Главным было то, что программа на Fortran использовала алгоритм со сложностью O(M ґN 2), в то время как алгоритм сортировки APL-программы требовал примерно O(M ґN log N) операций.

 

 

 

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

1.2.3. Принцип Чёрча и гипотеза Ворфа

Легко поверить в утверждение, что язык, на котором высказывается идея, направляет мышление. Однако есть более сильное утверждение, известное среди лингвистов как гипотеза Сапира–Ворфа. Она идет еще дальше, хотя и является спорной[Pullum 1991].

Гипотеза Сапира–Ворфа утверждает, что индивидуум, использующий некоторый язык, в состоянии вообразить или придумать нечто, не могущее быть переведенным или даже понятым индивидуумами из другой языковой среды. Такое происходит, если в языке

второго индивидуума нет эквивалентных слов и отсутствуют концепции или категории для идей, вовлеченных в рассматриваемую мысль. Интересно сравнить данную идею с прямо противоположной концепцией в информатике— а именно принципом Чёрча.

В 30-хгодах у математиков пробудился большой интерес к различным формализмам, которые могут быть использованы при вычислениях. Эти идеи получили развитие в40– 50-хгодах, когда они привлекли внимание молодого сообщества специалистов по информатике. Примерами таких систем являются модели, предложенные Чёрчем

[Church 1936], Постом [Post 1936],Марковым [Markov 1951],Тьюрингом [Turing 1936],

Клини [Kleene 1936] и другими. В одно время приводилось множество аргументов, доказывающих, что каждая из этих систем может быть использована для моделирования остальных. Часто такие доводы были двухсторонними, показывая, что обе модели эквивалентны с некой общей точки зрения. Все это привело логику Алонзо Чёрча к гипотезе, которая теперь связана с его именем.

Принцип Чёрча: Любое вычисление, для которого существует эффективная процедура, может быть реализовано на машине Тьюринга.

По самой своей природе это утверждение недоказуемо, поскольку мы не имеем строгого определения термина «эффективная процедура». Тем не менее до сих пор не было найдено контр примера, и убедительность очевидности, по-видимому, благоприятствует принятию этого утверждения1 .

1 Создание математического формализма вычислимости было связано с необходимостью определить понятие алгоритма. Пока исследования в этой области шли успешно, каждая новая формализованная последовательность вычислений получала имя «алгоритм» просто по определению. Когда же математики столкнулись с задачами, для которых пришлось доказывать отсутствие алгоритма, потребовалось формальное определение. В настоящий момент принято считать, что алгоритмом является последовательность действий, которая может быть сведена к программе, выполняемой с помощью машины Тьюринга. Или, в эквивалентной форме: последовательность действий, которая может быть сведена к программе для машины Поста, или конечного автомата Маркова, или же к последовательности рекурсивных функций Клини и Чёрча, является алгоритмом. Доказано, что все эти формальные системы вычислимости являются эквивалентными. Тем самым принцип Чёрча является аксиомой, не требующей доказательства, которая формализует понятие алгоритма( «эффективной процедуры») и в силу статуса аксиомы опровергающего контр примера иметь не может. — Примеч. перев.

 

Признание принципа Чёрча имеет важное и глубокое следствие для языков программирования. Машины Тьюринга являются изумительно простыми механизмами. От языка программирования требуется немного, чтобы смоделировать такое устройство. В1960-хгодах, к примеру, было показано, что машина Тьюринга может быть смоделирована на любом языке программирования, в котором содержатся условные операторы и операторы цикла[Bohm 1966]. Этот не совсем правильно понимаемый результат был одним из основных доводов в защиту утверждения о том, что знаменитый оператор goto является ненужным.

Если мы признаем принцип Чёрча, то любой язык, на котором можно смоделировать машину Тьюринга, является достаточно мощным, чтобы осуществить любой реализуемый алгоритм. Для решения проблемы надо построить машину Тьюринга, которая выдаст желаемый результат, — согласно принципу Чёрча такая машина должна существовать для каждого алгоритма. Затем остается только смоделировать машину Тьюринга на вашем любимом языке программирования. Тем самым споры об относительной «мощности» языков программирования— если под мощностью мы понимаем «способность решать задачи», — оказываются бессмысленными. Позднее Алан Перлис ввел удачный термин для подобных аргументов, назвав их «тьюринговская пропасть», поскольку из них так сложно выбраться, в то время как сами они столь фундаментально бесполезны.

Заметим, что принцип Чёрча является в определенном смысле точной противоположностью гипотезы Сапира–Ворфа. Принцип Чёрча утверждает, что по своей сути все языки программирования идентичны. Любая идея, которая выражается на одном языке, может(согласно теории) быть реализована на другом. Гипотеза же Сапира–Ворфа,как вы помните, утверждает, что существуют идеи, не согласующиеся с этим принципом.

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

Итак, как для компьютерных, так и для естественных языков справедливо: язык направляет мысли, но не предписывает их.

1.3. Новая парадигма

Объектно-ориентированное программирование часто называют новой парадигмой программирования. Другие парадигмы программирования: директивное(языки типа Pascal или C), логическое(языки типа Prolog) и функциональное(языки типа Lisp, FP или Haskell) программирование.

Интересно исследовать слово  «парадигма». Следующий фрагмент взят из толкового словаря American Heritage Dictionary of the English Language:

par-a-digm(сущ.) 1. Список всех вариантов окончаний слова, рассматриваемый как иллюстративный пример того, к какому спряжению или склонению оно относится. 2.

 

Любой пример или модель (от латинскогоparadigma и греческогоparadeigma — модель, отparadeiknunai — сравнивать, выставлять).

На первый взгляд, склонение и спряжение слов(например, латинских) имеет мало общего с компьютерными языками. Чтобы понять связь, мы должны заметить, что слово «парадигма» пришло в программирование из оказавшей большое влияние книги «Структура научных революций», написанной историком науки Томасом Куном

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

Именно в этом смысле — как модель или пример, а также как организующий подход— это слово использовал Роберт Флойд, лауреат премии Тьюринга1979 года, в лекции «Парадигмы программирования» [Floyd 1979]. Парадигмы в программировании— это способ концептуализации, который определяет, как проводить вычисления и как работа, выполняемая компьютером, должна быть структурирована и организована.

Хотя сердцевина объектно-ориентированного программирования— техника организации вычислений и данных является новой, ее зарождение можно отнести по крайней мере к временам Линнея(1707–1778), если не Платона. Парадоксально, но стиль решения задач, воплощенный вобъектно-ориентированной технике, нередко используется в повседневной жизни. Тем самым новички в информатике часто способны воспринять основные идеи объектно-ориентированного программирования сравнительно легко, в то время как люди, более осведомленные в информатике, зачастую становятся в тупик из-за своих представлений. К примеру, Алан Кей обнаружил, что легче обучать языку Smalltalk детей, чем профессиональных программистов[Kay 1977].

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

1.4. Способ видения мира

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

Предположим, я хочу послать цветы своей бабушке(которую зовут Элси) в ее день рождения. Она живет в городе, расположенном за много миль от меня, так что вариант, когда я сам срываю цветы и кладу их к ее порогу, не подлежит обсуждению. Тем не менее послать ей цветы— это достаточно простая задача: я иду в ближайший цветочный магазин, хозяйку которого(какое совпадение) зовут Фло(florist — цветочница), называю ей тип и количество цветов, которые бы я хотел послать моей бабушке, и(за приемлемую цену) я могу быть уверен, что цветы будут доставлены в срок, по нужному адресу.

 

1.4.1 Агенты, обязанности, сообщения и методы

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

Однако если бы я исследовал этот вопрос, я, возможно, обнаружил бы, что Фло пошлет свое сообщение хозяину цветочного магазина в городе, где живет моя бабушка. Хозяин

цветочного магазина в свою очередь примет необходимые меры и подготовит распоряжение (сообщение) для человека, ответственного за доставку цветов, и т. д. Тем

самым мой запрос в конечном счете будет удовлетворен через последовательность запросов, пересылаемых от одного агента к другому.

Итак, первым принципом объектно-ориентированного подхода к решению задач является способ задания действий.

Действие в  объектно-ориентированном программировании инициируется посредством передачи сообщений агенту(объекту), ответственному за действие. Сообщение содержит

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

Мы заметили, что существует важный принцип маскировки информации в отношении пересылки сообщений. А именно: клиенту, посылающему запрос, не требуется знать о фактических средствах, с помощью которых его запрос будет удовлетворен. Существует и другой принцип, также вполне человеческий, который мы видели в неявной форме при пересылке сообщений. Если имеется работа, которую нужно выполнить, то первая мысль клиента— найти кого-либо еще, кому можно было бы ее поручить. Такая вполне нормальная реакция почти полностью атрофировалась у программиста, имеющего большой опыт в традиционном программировании. Ему трудно представить, что он(или она) не должен все полностью программировать сам, а может обратиться к услугам других. Важная часть объектно-ориентированного программирования— разработка повторно используемых компонент, и первым шагом в этом направлении является желание попробовать этот путь.

Скрытие информации является важным принципом и в традиционных языках программирования. Тогда в чем пересылка сообщений отличается от обычного вызова процедуры? В обоих случаях имеется последовательность точно определенных действий, которые будут инициированы в ответ на запрос. Однако имеются два существенных отличия.

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

 

получателем сообщения является первый аргумент в вызове процедуры — примерно так и реализуются получатели сообщений).

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

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

1.4.2. Обязанности и ответственности

Фундаментальной концепцией в  объектно-ориентированном программировании является понятие обязанности или ответственности за выполнение действия. Мой запрос выражает только стремление получить желаемый результат(а именно доставить цветы бабушке).

Хозяйка цветочного магазина свободна в выборе способа, который приведет к желаемому результату, и не испытывает препятствий с моей стороны в этом аспекте.

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

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

Задавайтесь вопросом не о том, что вы можете сделать для своих структур данных, а о том, что структуры данных могут сделать для вас.

1.4.3. Классы и экземпляры

Хотя я имел дело с Фло лишь несколько раз, у меня имеется примерное представление о ее реакции на мой запрос. Я могу сделать определенные предположения, поскольку имею

 

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

Все объекты являются представителями, или экземплярами, классов. Метод, активизируемый объектом в ответ на сообщение, определяется классом, к которому принадлежит получатель сообщения. Все объекты одного класса используют одни и те же методы в ответ на одинаковые сообщения.

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

1.4.4. Иерархии классов и наследование

О Фло у меня имеется больше информации, чем содержится в категории Florist. Я знаю, что она разбирается в цветах и является владелицей магазина(shopkeeper). Я догадываюсь, что, вероятно, меня спросят о деньгах в процессе обработки моего запроса и что после оплаты мне будет выдана квитанция. Все вышеперечисленное справедливо также для зеленщиков, киоскеров, продавцов магазинов и т. д. Поскольку категория Florist является более узкой, чем Shopkeeper, то любое знание, которым я обладаю о категории Shopkeeper, справедливо также и для Florist, и, в частности, для Фло.

Один из способов представить организацию моего знания о Фло — это иерархия категорий(рис. 1.1). Фло принадлежит к категории Florist; Florist является подкатегорией категории Shopkeeper. Далее, представитель Shopkeeper заведомо является человеком, то есть принадлежит к категории Human — тем самым я знаю, что Фло с большой вероятностью является двуногим существом. Далее, категория Human включена в категорию млекопитающих(Mammal), которые кормят своих детенышей молоком, а млекопитающие являются подкатегорией животных(Animal) и, следовательно, дышат кислородом. В свою очередь животные являются материальными объектами дуумов с различными линиями наследования. Классы представляются в виде иерархической древовидной структуры, в которой более абстрактные классы(такие, как Material Object или Animal) располагаются в корне дерева, а более специализированные классы и в конечном итоге индивидуумы располагаются на его конце, в ветвях. Рисунок1.2 показывает такую иерархию классов для Фло. Эта же самая иерархия включает в себя мою жену Бет, собаку Флеш, Фила— утконоса, живущего в зоопарке, а также цветы, которые я послал своей бабушке.

 

 

Рис. 1.2. Иерархическое дерево классов, представляющих различные материальные

объекты

Поскольку Фло — человек, та информация о ней, которой я обладаю, применима также, к примеру, к моей жене Бет. Те данные, которыми я располагаю в силу принадлежности последней к классу млекопитающих, имеют также отношение к моей собаке Флеш. Информация об объектах как о вещах материальных имеет смысл в отношении Фло, и ее цветов. Мы выражаем все это в виде идеи наследования:

Классы могут быть организованы в иерархическую структуру с наследованием свойств. Дочерний класс (или подкласс) наследует атрибуты родительского класса(или надкласса), расположенного выше в иерархическом дереве1 . Абстрактный родительский класс — это класс, не имеющий экземпляров(его примером может служить Mammal на рис. 1.2). Он используется только для порождения подклассов.

1.4.5. Связывание и переопределение методов

Утконос Фил представляет собой проблему для нашей простой структуры. Я знаю, что млекопитающие являются живородящими, но Фил определенно является млекопитающим, хотя он(точнее, его подруга Филлис) кладет яйца. Чтобы включить его в нашу схему, мы должны найти технику для представления исключений из общего правила.

Мы сделаем это, допустив правило, что информация, содержащаяся в подклассе, может-

1 Здесь придется попросить читателя вернуться к рис. 1.2 и обратить внимание на то, что согласно принятой схеме дерево растет сверху вниз. — Примеч. ред.

 

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

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

Тот факт, что моя жена Бет и хозяйка цветочного магазина Фло будут реагировать на мое сообщение с применением различных методов, является одним из примеров полиморфизма. Мы будем обсуждать эту важную составную часть  объектно-ориентированного программирования в главе14. То, что я, как уже говорилось, не знаю и не хочу знать, какой именно метод будет использован Фло для выполнения моего запроса, является примером маскировки информации, которая анализируется в главе17.

1.4.6. Краткое изложение принципов

Алан Кей, которого кое-кто называет отцом объектно-ориентированного программирования, считает следующие положения фундаментальными характеристиками ООП[Kay 1993]:

1.Все является объектом.

2.Вычисления осуществляются путем взаимодействия (обмена данными) между объектами, при котором один объект требует, чтобы другой объект выполнил некое действие. Объекты взаимодействуют, посылая и получая сообщения.

Сообщение — это запрос на выполнение действия, дополненный набором аргументов, которые могут понадобиться при выполнении действия.

3.Каждый объект имеет независимую память, которая состоит из других объектов.

4.Каждый объект является представителем класса, который выражает общие свойства объектов(таких, как целые числа или списки).

5.В классе задается поведение (функциональность) объекта. Тем самым все объекты, которые являются экземплярами одного класса, могут выполнять одни и те же действия.

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

 

1.5. Вычисление и моделирование

Взгляд на программирование, проиллюстрированный на примере с цветами, весьма отличается от привычного понимания того, что такое компьютер. Традиционная модель, описывающая выполнение программы на компьютере, базируется на дуализме процесс

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

Хотя антропоморфные описания, подобные тем, что цитировались выше в тексте Ингалса, могут шокировать людей, фактически они являются отражением огромной выразительной силы метафор. Журналисты используют метафоры каждый день, подобно тому, как это сделано в нижеследующем фрагменте из NewsWeek: В отличие от обычного метода программирования— то есть написания программы строчка за строчкой, —  « объектно- ориентированная» система компьютеров NeXT предлагает строительные блоки большего размера, которые разработчик может быстро собирать воедино, подобно тому, как дети складывают мозаику.

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

1.5.2. Как избежать бесконечной регрессии

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

В языках, где директивный и объектно-ориентированный подходы уживаются вместе(таких, какC++, Object Pascal иObjective-C), реальные действия выполняются методами, написанными на основном(не объектно-ориентированном) языке. В чисто  объектно- ориентированных языках(таких, какSmalltalk иJava) это выполняется с помощью «примитивных» или «встроенных» операций, которые обеспечиваются исполнительной системой более низкого уровня.

1.6. Барьер сложности

 

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

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

1.6.1. Нелинейное увеличение сложности

По мере того как программные проекты становились все сложнее, было замечено интересное явление. Задача, для решения которой одному программисту требовалось два месяца, не решалась двумя программистами за месяц. Согласно замечательной фразе Фреда Брукса,  «рождение ребенка занимает девять месяцев независимо от того, сколько женщин занято этим» [Brooks 1975].

Причиной такого нелинейного поведения является сложность. В частности, взаимосвязи между программными компонентами стали сложнее, и разработчики вынуждены были постоянно обмениваться между собой значительными объемами информации. Брукс также сказал:

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

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

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

 

1.6.2. Механизмы абстрагирования

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

Главный из них — это абстрагирование, то есть способность отделить логический смысл фрагмента программы от проблемы его реализации. В некотором смысле  объектно-

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

Процедуры

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

механизмом маскировки деталей организации данных и только отчасти снимали проблему использования разными программистами одинаковых имен.

Пример: стек

Чтобы проиллюстрировать эти проблемы, рассмотрим ситуацию, когда программисту нужно реализовать управление простым стеком. Следуя старым добрым принципам разработки программного обеспечения, наш программист прежде всего определяет внешний интерфейс— скажем, набор из четырех процеду рinit, push, pop иtop. Затем он выбирает подходящий метод реализации. Здесь есть из чего выбрать: массив с указателем на вершину стека, связный список и т. д. Наш бесстрашный разработчик выбирает один из методов, а затем приступает к кодированию, как показано в листинге1.1.

Легко увидеть, что данные, образующие стек, не могут быть сделаны локальными для какой-то из четырех процедур, поскольку эти данные являются общими для всех из них. Но если у нас есть только локальные или глобальные переменные(как это имеет место для Fortran или было вC, до того как ввели модификато рstatic), то данные стека должны содержаться в глобальных переменных. Однако если переменные являются глобальными, то нет способа ограничить доступ к ним или их видимость. Например, если стек реализован как массив с именем datastack, то об этом должны знать все остальные программисты, поскольку они могут захотеть создать переменные с таким же именем, чего делать ни в коем случае нельзя. Запрет на использование имени datastack необходим,

даже если сами данные важны только для подпрограмм работы со стеком и не будут использоваться за пределами этих четырех процедур. Аналогично имена init, push, pop и top являются теперь зарезервированными и не должны встречаться в других частях программы(разве что с целью вызова процедур), даже если эти части не имеют ничего общего с подпрограммами, обслуживающими стек.

 

Листинг 1.1. Процедуры не годятся для маскировки информации

int datastack[100]; int datatop = 0; void init()

{

datatop = 0;

}

void push(int val)

{if (datatop < 100)

datastack [datatop++] = val;

}

int top()

{

if (datatop > 0)

return datastack [datatop — 1]; return 0;

}

int pop()

{

if (datatop > 0)

return datastack [—datatop];return 0;

}

Область видимости для блоков

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

begin var

datastack : array [1..100] of integer; datatop : integer;

procedure init; . . .

procedure push(val : integer); . . .

function pop : integer; . . .

. . .

end;

Модули

В некотором смысле модули можно рассматривать просто как улучшенный метод создания и управления совокупностями имен и связанными с ними значениями. Наш пример со стеком является типичным в том аспекте, что имеется определенная информация(интерфейсные процедуры), которую мы хотим сделать широко и открыто используемой, в то время как доступ к некоторым данным(собственно данные стека) должен быть ограничен. Если рассматривать модуль как абстрактную концепцию, сведенную к своей простейшей форме, то ее суть состоит в разбиении пространства имен на две части. Открытая(public) часть является доступной извне модуля, закрытая(private)

 

часть доступна только внутри модуля. Типы, данные(переменные) и процедуры могут быть отнесены к любой из двух частей.

Дэвид Парнас [Parnas 1972] популяризовал понятие модулей. Он сформулировал следующие два принципа их правильного использования:

1.Пользователя, который намеревается использовать модуль, следует снабдить всей информацией, необходимой, чтобы делать это корректно, и не более того.

2.Разработчика следует снабдить всей информацией, необходимой для создания модуля, и не более того.

Эта философия в значительной мере напоминает военную доктрину  «необходимого знания»: если вам не нужно знать определенную информацию, вы и не должны иметь к ней доступа. Это явное, намеренное и целенаправленное утаивание информации называется маскировкой информации(information hiding).

Модули решают некоторые, но не все проблемы разработки программного обеспечения. Например, они позволяют нашему программисту скрыть детали реализации стека, но что делать, если другие пользователи захотят иметь два(или более) стека?

В качестве более сложного примера предположим, что программист заявляет, что им разработан новый тип числовых объектов, названныйComplex. Он определил арифметические операции для комплексных величин— сложение, вычитание, умножение и т. д. и ввел подпрограммы для преобразования обычных чисел в комплексные и обратно. Имеется лишь одна маленькая проблема: можно манипулировать только с одним комплексным числом.

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

Абстрактные типы данных

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

Модули часто используются при реализации абстрактных типов данных.

Непосредственной логической взаимосвязи между понятиями модуля и абстрактного типа данных нет. Эти две идеи близки, но не идентичны. Чтобы построить абстрактный тип данных, мы должны уметь:

 

1.Экспортировать определение типа данных.

2.Делать доступным набор операций, использующихся для манипулирования экземплярами типа данных.

3.Защищать данные, связанные с типом данных, чтобы с ними можно было работать только через указанные подпрограммы.

4.Создавать несколько экземпляров абстрактного типа данных.

В нашем определении модули служат только как механизм маскировки информации и тем самым непосредственно связаны только со свойствами 2 и3 из нашего списка. Остальные

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

программирования, как CLU или Ada, тесно связаны с перечисленными выше требуемыми свойствами абстрактных типов данных.

В определенном смысле объект — это просто абстрактный тип данных. Говорили, к примеру, что программисты на языке Smalltalk пишут наиболее «структурированные» программы, потому что они не имеют возможности написать что-либо кроме определений абстрактных типов данных. Истинная правда, что объект является абстрактным типом данных, но понятия объектно-ориентированного программирования, хотя и строятся на идеях абстрактных типов данных, добавляют к ним важные новшества по части разделения и совместного использования программного кода.

Объекты: сообщения, наследование и полиморфизм

объектно-ориентированное программирование добавляет несколько новых важных идей к концепции абстрактных типов данных. Главная из них— пересылка сообщений. Действие инициируется по запросу, обращенному к конкретному объекту, а не через вызов функции. В значительной степени это просто смещение ударения: традиционная точка зрения делает основной упор на операции, в то время как ООП на первое место ставит собственно значение. (Вызываете ли вы подпрограмму push со стеком и значением в качестве аргументов, или же вы просите объект stack поместить нужное значение к нему внутрь?) Если бы это было все, что имеется в объектно-ориентированном программировании, эта техника не рассматривалась бы как принципиальное нововведение. Но к пересылке сообщений добавляются мощные механизмы переопределения имен и совместного/многократного использования программного кода.

Неявной в идее пересылки сообщений является мысль о том, что интерпретация сообщения может меняться для различных объектов. А именно поведение и реакция, инициируемые сообщением, зависят от объекта, который получает сообщение. Тем самымpush может означать одно действие для стека и совсем другое для блока управления механической рукой. Поскольку имена операций не обязаны быть уникальными, могут использоваться простые и явные формы команд. Это приводит к более читаемому и понятному коду.

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

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

 

Все эти идеи будут описаны более подробно в последующих главах.

1.7. Многократно используемое программное обеспечение

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

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

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

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

используемых программных компонентов становится быстро развивающейся отраслью индустрии программного обеспечения.

1.8. Резюме

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

ООП рассматривает программы как совокупность свободно (гибко) связанных между собой агентов, называемых объектами. Каждый из них отвечает за конкретные задачи. Вычисление осуществляется посредством взаимодействия объектов. Следовательно, в определенном смысле программирование— это ни много ни мало, как моделирование мира.

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

 

Поведение объекта диктуется его классом. Каждый объект является экземпляром некоторого класса. Все экземпляры одного класса будут вести себя одинаковым образом(то есть вызывать те же методы) в ответ на одинаковые запросы.

Объект проявляет свое поведение путем вызова метода в ответ на сообщение. Интерпретация сообщения(то есть конкретный используемый метод) зависит от объекта и может быть различной для различных классов объектов.

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

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

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

Что читать дальше

Я отметил ранее, что Алан Кей считается отцом объектно-ориентированного программирования. Подобно многим простым высказываниям, данное утверждение выдерживает критику лишь отчасти. Сам Кей[Kay 1993] считает, что его вклад состоит преимущественно в разработке языкаSmalltalk на основе более раннего языка программированияSimula, созданного в Скандинавии в60-хгодах[Dahl 1966, Kirkerud 1989]. История свидетельствует, что большинство принципов  объектно-

ориентированного программирования было полностью разработано создателями языка Simula, но этот факт в значительной степени игнорировался профессионалами до тех пор, пока они(принципы) не были вновь открыты Кеем при разработке языка программированияSmalltalk. Пользующийся широкой популярностью журналByte в1981 году сделал многое для популяризации концепций, разработанных Кеем и его командой из группыXerox PARC.

Термин  «кризис программного обеспечения», по-видимому, был изобретен Дугом Мак- Илроем во время конференции НАТО1968 года по программным технологиям. Забавно, что мы находимся в этом кризисе и сейчас, по прошествии половины срока существования информатики как независимой дисциплины. Несмотря на окончание холодной войны, выход из кризиса программного обеспечения не ближе к нам, чем это было в1968 году— см., к примеру, статью Гиббса «Хронический кризис программного обеспечения» в сентябрьском выпускеScientific American за1994 год[Gibbs 1994].

До некоторой степени кризис программного обеспечения — в значительной мере иллюзия. Например, задачи, рассматривавшиеся как чрезвычайно сложные пять лет назад, редко считаются таковыми сегодня. Проблемы, которые мы желаем решить сейчас, ранее считались непреодолимыми— по-видимому, это показывает, что разработка программного обеспечения год от года прогрессирует.

 

Цитата американского лингвиста Эдварда Сапира (стр. 21) взята из статьи «Связь поведения и мышления с языком», перепечатанной в сборнике «Мышление и реальность» [Whorf 1956]. В нем содержится несколько интересных работ по связям между языком и процессом мышления. Я настоятельно рекомендую каждому серьезному студенту, занимающемуся компьютерными языками, прочитать эти статьи. Некоторые из них имеют удивительно близкое отношение к искусственным языкам.

Другая интересная книга — это «Эффект алфавита» Роберта Логана[Logan 1986], которая объясняет в лингвистических терминах, почему логика и наука были разработаны на Западе, в то время как в течение веков Китай имел опережающую технологию. В более современном исследовании о влиянии естественного языка на информатику Дж. Маршалл Унгер[Unger 1987] описывает влияние японского языка на известный проект Пятого поколения компьютеров.

Всеми признанное наблюдение, что язык эскимосов имеет много слов для обозначения типов снега, было развенчано Джоффри Паллумом в его сборнике статей по лингвистике[Pullum 1991]. В статье вAtlantic Monthly  «Похвала снегу» (январь1995) Каллен Мерфи указывал, что набор слов, используемый для обсуждения «снежной» тематики людьми, говорящимипо-английски, по крайней мере столь же разнообразен, как и термины эскимосов. При этом, естественно, имеются в виду люди, для которых различия в типах снега существенны(преимущественно это ученые, которые проводят исследования в данной области).

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

Мой рассказ является слегка неточным в отношении принципа Чёрча и машин Тьюринга.

Чёрч фактически делал свое утверждение относительно рекурсивных функций [Church 1936], которые впоследствии оказались эквивалентными вычислениям,

проводимым с помощью машин Тьюринга [Turing 1936]. В той форме, в которой мы его формулируем здесь, этот принцип был описан Клини, и им же было дано то название, под которым принцип теперь известен. Роджерс приводит хорошую сводку аргументов в защиту эквивалентности различных моделей вычислений[Rogers 1967].

Если вы помните, именно шведский ботаник Карл Линней разработал идеи родов, видов и т. д. Это является прототипом схемы иерархической организации, иллюстрирующей наследование, поскольку абстрактная классификация описывает характеристики, свойственные всем классификациям. Большинство иерархий наследования следуют модели Линнея.

Критика процедур как методики абстрагирования (поскольку они не способны обеспечить надлежащий механизм маскировки данных) была впервые проведена Вилльямом Вульфом и Мери Шоу[Wulf 1973] при анализе многочисленных проблем, связанных с использованием глобальных переменных. Эта аргументация была впоследствии расширена Дэвидом Хансоном[Hanson 1981].

 

Подобно многим словам, которые нашли себе место в общепринятом жаргоне, термин « объектно-ориентированный» используется гораздо шире своего фактического значения. Тем самым на вопрос:  «Что такое объектно-ориентированное программирование?» очень непросто ответить. Бьорн Страуструп[Stroustrup 1988] не без юмора заметил, что большинство аргументов сводится к следующему силлогизму:

∙X — это хорошо.

∙Объектная ориентированность — это хорошо.

∙Следовательно, X является объектно-ориентированным.

Роджер Кинг аргументированно настаивал, что его кот является  объектно- ориентированным. Кроме прочих своих достоинств, кот демонстрирует характерное поведение, реагирует на сообщения, наделен унаследованными реакциями и управляет своим вполне независимым внутренним состоянием.

Многие авторы пытались дать строгое определение тех свойств языка программирования, которыми он должен обладать, чтобы называться объектно-ориентированным, — см., к примеру, анализ, проведенный Джозефиной Микалеф[Micallef 1988] или Питером Вегнером[Wegner 1986].

Вегнер, к примеру, различает языки, основанные на объектах, которые поддерживают только абстрагирование(такие, какAda), и объектно-ориентированныеязыки, которые поддерживают наследование.

Другие авторы — среди них наиболее заметен Брэд Кокс[Cox 1990] — определяют термин ООП значительно шире. Согласно Коксу объектно-ориентированное программирование представляет собой метод или цель(objective) программирования путем сборки приложений из уже имеющихся компонент, а не конкретную технологию, которую мы можем использовать, чтобы достичь этой цели. Вместо выпячивания различий между подходами мы должны объединить воедино любые средства, которые

оказываются многообещающими на пути к новой Индустриальной Революции в программировании. Книга Кокса по ООП[Cox 1986], хотя и написана на заре развития объектно-ориентированного программирования, и в силу этого отчасти устаревшая в отношении деталей, тем не менее является одним из наиболее читаемых манифестов объектно-ориентированногодвижения.

Упражнения

1.В  объектно-ориентированнойиерархии наследования каждый следующий уровень является более специализированной формой предыдущего. Приведите пример иерархии из повседневной жизни с этим свойством. Некоторые из иерархий, обнаруживаемые в реальной жизни, не являются иерархиями наследования. Укажите пример иерархии без свойства наследования.

2.Посмотрите значение слова парадигма по крайней мере в трех словарях. Соотнесите эти определения с языками программирования.

3.Возьмите задачу из реального мира (аналогичную пересылке цветов, рассмотренной ранее) и опишите ее решение в терминах агентов(объектов) и обязанностей.

4.Если вы знакомы с двумя (или более) различными языками программирования, приведите пример, когда один язык направляет мысль программиста к определенному решению, а другой— стимулирует альтернативное решение.

 

5.Если вы знакомы с двумя (или более) естественными языками, опишите ситуацию, когда один язык направляет говорящего в одном направлении, в то время как другой язык приводит к иному ходу мысли.

Глава 2 : Объектноориентированное проектирование

Когда программисты спрашивают друг друга:  «Чем же, в конце концов, является объектно-ориентированное программирование?», ответ чаще всего подчеркивает синтаксические свойства таких языков, какC++ илиObject Pascal, по сравнению с их более ранними, не объектно-ориентированнымиверсиями, то естьC илиPascal. Тем самым обсуждение обычно переходит на такие предметы, как классы и наследование, пересылка сообщений, виртуальные и статические методы. Но при этом опускают наиболее важный момент в объектно-ориентированном программировании, который не имеет ничего общего с вопросами синтаксиса.

Работа на  объектно-ориентированномязыке(то есть на языке, который поддерживает наследование, пересылку сообщений и классы) не является ни необходимым, ни достаточным условием для того, чтобы заниматься объектно-ориентированнымпрограммированием. Как мы подчеркнули в главе1, наиболее важный аспект в ООП— техника проектирования, основанная на выделении и распределении обязанностей. Она

была названа проектированием на основе обязанностей или проектированием на основе ответственности (responsibility-driven design) [Wirfs-Brock 1989b, Wirfs-Brock 1990].

2.1. Ответственность подразумевает невмешательство

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

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

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

 

отсекать эти связи или по крайней мере сделать их настолько слабыми, насколько это возможно.

С первого взгляда идея кажется не более сложной, чем понятия маскировки информации и модульности, которые важны при программировании в целом, в том числе и при использовании традиционных языков. Но проектирование, основанное на распределении ответственности, поднимает маскировку данных с уровня техники до уровня искусства.

Принцип маскировки информации становится жизненно важным при переходе от программирования  «в малом» к программированию «в большом».

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

2.2. Программирование  «в малом» и  «в большом»

О разработке индивидуального проекта часто говорят как о программировании  «в малом», а о реализации большого проекта как о программировании «в большом».

Для программирования  «в малом» характерны следующие признаки:

∙Код разрабатывается единственным программистом или, возможно, небольшой группой программистов. Отдельно взятый индивидуум может понять все аспекты проекта, от и до.

∙Основная проблема при разработке состоит в проектировании программы и написании алгоритмов для решения поставленной задачи.

Сдругой стороны, программирование «в большом» наделяет программный проект следующими свойствами:

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

∙Основная проблема в процессе разработки программного обеспечения — управление проектом и обмен информацией между группами и внутри групп.

В то время как начинающий студент обычно знакомится с программированием  «в малом», особенности многих объектно-ориентированныхязыков наилучшим образом понимаются при встрече с проблемами, типичными для программирования «в большом». Тем самым

 

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

2.3. Почему надо начинать с функционирования?

Из-зачего процесс проектирования начинают с анализа функционирования или поведения системы? Простой ответ состоит в том, что поведение системы обычно известно задолго до остальных ее свойств.

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

Мы проиллюстрируем проектирование на основе обязанностей (илиRDD-

проектирование — Responsibility-Driven-Design) на учебном примере.

2.4. Учебный пример: проектирование на основе обязанностей

Представьте себе, что вы являетесь главным архитектором программных систем в ведущей компьютерной фирме. В один прекрасный день ваш начальник появляется в офисе с идеей, которая, как он надеется, будет очередным успехом компании. Вам поручают разработать систему под названиемInteractive Intelligent Kitchen Helper (Интерактивный разумный кухонный помощник) (рис. 2.1)

Рис. 2.1. Внешний вид программы  «Интерактивный разумный кухонный помощник»

Задача, поставленная перед вашей командой программистов, сформулирована в нескольких скупых словах(написанных начем-то, что оказывается использованной обеденной салфеткой, причем почерком, принадлежащим вашему начальнику).

 

2.4.1. Интерактивный разумный кухонный помощник

Программа  «Интерактивный разумный кухонный помощник» (Interactive Intelligent Kitchen Helper, IIKH) предназначена для персональных компьютеров. Ее цель— заменить собой набор карточек с рецептами, который можно встретить почти в каждой кухне. Но помимо ведения базы данных рецептов, IIKH помогает в планировании питания на длительный период— например, на неделю вперед. Пользователь программыIIKH садится за компьютер, просматривает базу данных рецептов и в диалоговом режиме определяет меню на весь требуемый период.

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

Краеугольным камнем в ООП является характеристика программного обеспечения в терминах поведения, то есть в терминах действий, которые должны быть выполнены. Мы

увидим воплощение в жизнь этого принципа на многих уровнях процесса разработки IIKH. Первоначально команда попытается охарактеризовать на очень высоком уровне абстрагирования поведение приложения в целом. Затем она займется описанием поведения различных программных подсистем. И только тогда, когда все аспекты поведения будут выделены и описаны, программисты-разработчикиприступят к этапу кодирования. В следующих разделах мы будем отслеживать этапы работы команды программистов при создании данного приложения.

2.4.2. Работа по сценарию

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

2.4.3. Идентификация компонент

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

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

 

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

∙компонента должна иметь небольшой набор четко определенных обязанностей;

∙компонента должна взаимодействовать с другими компонентами настолько слабо, насколько это возможно.

Позднее мы поговорим о второй особенности подробнее. Сейчас мы просто занимаемся определением обязанностей компонент.

2.5. CRC-карточка— способ записи обязанностей

Чтобы выявить отдельные компоненты и определить их обязанности, команда программистов прорабатывает сценарий системы. То есть воспроизводится запуск приложения, как если бы оно было уже готово. Любое действие, которое может произойти, приписывается некоторой компоненте в качестве ее обязанности.

В качестве составной части этого процесса полезно изображать компоненты с помощью небольших индексных карточек. На лицевой стороне карточки написаны имя компоненты, ее обязанности и имена других компонент, с которыми она должна взаимодействовать. Такие карточки иногда называютсяCRC-карточкамиот словComponent, Responsibility, Collaborator (компонента, обязанность, сотрудники) [Beck 1989]. По мере того как для компонент выявляются обязанности, они записываются на лицевой сторонеCRC- карточки.

2.5.1. Дайте компонентам физический образ

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

 

Преимущество CRC-карточекв том, что они широко доступны, недороги и с них можно стирать информацию. Это стимулирует экспериментирование, поскольку альтернативные проекты могут быть испробованы, изучены и отброшены с минимальными затратами.

Физическое разделение карточек стимулирует интуитивное понимание важности логического разделения компонент, что помогает сделать упор на связности внутри модулей и зацеплении между модулями(которые вкратце будут описаны ниже).

Небольшой размер индексной карточки служит хорошей оценкой примерной сложности отдельного фрагмента — компоненты, которой приписывается больше задач, чем может поместиться на ее карточке, вероятно, является излишне сложной, и должно быть найдено более простое решение. Может быть, следует пересмотреть разделение обязанностей или разбить компоненту на две.

2.5.2. Цикл  «что/кто»

Как мы заметили в начале нашего обсуждения, выделение компонент производится во время процесса мысленного представления работы системы. Часто это происходит как цикл вопросов «что/кто». Во-первых, команда программистов определяет: что требуется делать? Это немедленно приводит к вопросу: кто будет выполнять действие? Теперь программная система в значительной мере становится похожа на некую организацию, скажем, карточный клуб. Действия, которые должны быть выполнены, приписываются некоторой компоненте в качестве ее обязанностей.

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

2.5.3. Документирование

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

Руководство пользователя описывает взаимодействие с системой с точки зрения пользователя. Это— отличное средство проверки того, что концепция командыпрограммистов-разработчиковсоответствует мнению клиента. Поскольку решения, принятые в процессе проработки сценария, соответствуют действиям, которые потребуются от пользователя при использовании программы, то написание руководства пользователя естественным образом увязывается с процессом проработки сценария.

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

 

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

Хотя в равной мере важно документировать программу на уровне модулей, слишком

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

CRC-карточкиявляются одним из видов проектной документации, но многие другие важные решения не отражены в них. Аргументы за и против каждой важной альтернативы при проектировании должны записываться, равно как и факторы, которые повлияли на конечное решение. Должен вестись протокол или дневник хода проекта. Как руководство пользователя, так и проектная документация уточняются и изменяются в процессе работы в точном соответствии с тем, как модифицируется собственно программа.

2.6. Компоненты и поведение

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

Первоначально планируется только пять действий:

1.Просмотреть базы данных с рецептами, но без ссылок накакой-токонкретный план питания.

2.Добавить новый рецепт в базу данных.

3.Редактировать или добавить комментарии к существующему рецепту.

4.Пересмотреть существующий план в отношении некоторых продуктов.

5.Создать новый план питания.

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

 

 

Рис. 2.2. CRC-карточкадля класса заставкиGreeter

первоначальный вид CRC-карточкидля компонентыGreeter.

В широком смысле обязанность компоненты, работающей с базой данных, — просто поддерживать записи с рецептами.

Мы уже выделили три аспекта этой компоненты: Recipe Database должна обеспечивать просмотр библиотеки существующих рецептов, редактирование рецептов, включение новых рецептов в базу данных.

2.6.1. Отложенные решения

В конце концов придется решить, как пользователь станет просматривать базу данных. Например, должен ли он сначала входить в список категорий таких, как «супы»,  «салаты»,  «горячие блюда»,  «десерты»?

С другой стороны, может ли пользователь задавать ключевые слова для ограничения области поиска, включая список ингредиентов( «миндаль»,  «клубника»,  «сыр»)? Или же использовать список предварительно заданных ключевых слов( «любимые пирожные Боба»)? Следует ли применять полосы прокрутки(scroll bars) или имитировать закладки в виртуальной книжке? Размышлять об этих предметах доставляет удовольствие, но важно то, что нет необходимости принимать конкретные решения на данном этапе проектирования(см. раздел2.6.2.  «Готовность к изменениям»). Поскольку они влияют

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

2.6.2. Готовность к изменениям

Как было сказано, единственное, что является постоянным в жизни, — неизбежность изменений. То же самое справедливо для программного обеспечения. Независимо от того

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

 

∙Главная цель состоит в том, что изменения должны затрагивать как можно меньше компонент. Даже принципиальные новшества во внешнем виде или функционировании приложения должны затронуть один или два фрагмента кода.

∙Старайтесь предсказать наиболее вероятные источники изменений и позвольте им влиять на возможно меньшее количество компонент программы. Наиболее общими причинами изменений являются интерфейс, форматы обмена информацией, вид выходных данных.

∙Старайтесь изолировать и уменьшить зависимость программного обеспечения от аппаратуры. Например, интерфейс просмотра рецептов в вашем приложении может частично зависеть от аппаратного обеспечения системы, на которой работает программа. Последующие версии должны переноситься на различные платформы. Хороший проект должен предвидеть подобное изменение.

∙Уменьшение количества связей между фрагментами программы снижает взаимозависимость между ними и увеличивает вероятность того, что каждую компоненту удастся изменить с минимальным воздействием на другие.

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

2.6.3. Продолжение работы со сценарием

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

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

Вернемся к блоку Greeter. Планирование меню, как вы помните, было поручено программной компонентеPlan Manager. Пользователь должен иметь возможность сохранить существующий план. Следовательно, компонентаPlan Manager может запускаться либо в результате открытия уже существующего плана, либо при создании нового. В последнем случае пользователя необходимо попросить ввести временные интервалы(список дат) для нового плана. Каждая дата ассоциируется с отдельной

 

компонентой типа Date. Пользователь может выбрать конкретную дату для детального исследования— в этом случае управление передается соответствующей компонентеDate. КомпонентаPlan Manager должна уметь распечатывать меню питания на планируемый период. Наконец, пользователь может попросить компонентуPlan Manager сгенерировать список продуктов на указанный период.

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

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

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

Ответственность за обработку подобных ситуаций следует распределить между компонентами.

Изучив различные сценарии, команда разработчиков в конечном счете решает, что все

действия могут быть надлежащим образом распределены между шестью компонентами

(рис. 2.3).Компонента Greeterвзаимодействует только с Plan Managerи Recipe Database.

Компонента Plan Manager  «зацепляется» только сDate, а та в свою очередь— сMeal. КомпонентаMeal обращается кRecipe Manager и через посредство этого объекта к конкретным рецептам.

Рис. 2.3. Взаимосвязь между компонентами программы IIKH

2.6.4. Диаграммы взаимодействия

Схема, изображенная на рис. 2.3, хорошо подходит для отображения статических связей между компонентами. Но она не годится для описания динамического взаимодействия во время выполнения программы. Для этих целей используются диаграммы взаимодействия.

 

На рис. 2.4 показана часть диаграммы взаимодействия для программыIIKH. Время движется сверху вниз. Каждая компонента представлена вертикальной линией.

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

Благодаря наличию оси времени диаграмма взаимодействия лучше описывает последовательность событий в процессе работы программы. Поэтому диаграммы

взаимодействия являются полезным средством документирования для сложных программных систем.

Рис. 2.4. Пример диаграммы взаимодействия

2.7. Компоненты программы

В этом разделе мы исследуем компоненты программы более подробно. За этим внешне простым понятием прячется много нетривиальных аспектов(что, впрочем, справедливо почти для всех понятий за исключением совсем элементарных).

2.7.1. Поведение и состояние

Мы уже видели, что компоненты характеризуются своим поведением, то есть тем, что они должны делать. Но компоненты также хранят определенную информацию. Возьмем, к примеру, компоненту-прототипRecipe из программыIIKH. Можно представить ее себе как пару «поведение—состояние».

∙Поведение компоненты — это набор действий, ею осуществляемых. Полное описание поведения компоненты иногда называют протоколом. Например, в протоколе, поддерживаемом компонентойRecipe, значится, что она осуществляет редактирование инструкций по приготовлению блюд, отображает их на экране, распечатывает рецепты.

∙Говоря о состоянии компоненты, имеют в виду ее внутреннее содержание. ДляRecipe состояние включает в себя ингредиенты и инструкции по приготовлению блюд. Состояние не является статическим и может изменяться с течением времени. Например, редактируя текст, пользователь изменяет состояние рецепта.

Не все компоненты обязаны иметь состояние. Например, у компонентыGreeter, вероятно, не будет внутренних данных, поскольку ей ни к чему помнитькакую-либоинформацию. Однако большинство компонент характеризуется и поведением, и состоянием.

2.7.2. Экземпляры и классы

 

Разделив понятия о состоянии и поведении, мы можем теперь затронуть тему, которую ранее избегали. Вероятно, в реальном приложении будет много рецептов. Однако все они будут вести себя одинаково. Отличается только состояние: список ингредиентов и инструкций по приготовлению. На ранних стадиях разработки нас должно интересовать поведение, общее для всех рецептов. Детали, специфические для отдельного рецепта, не важны.

Термин класс используется для описания множества объектов с похожим поведением. Мы увидим в последующих главах, что класс применяется как синтаксический механизм почти во всех объектно-ориентированныхязыках. Конкретные представители класса называются экземплярами. Заметим, что поведение ассоциировано с классом, а не с индивидуальными представителями. То есть все экземпляры класса воспринимают одни и те же команды и выполняют их сходным способом. С другой стороны, состояние является индивидуальным. Мы видим это на примере различных экземпляров классаRecipe. Все они могут выполнять одни и те же действия(редактирование, вывод на экран, печать), но используют различные данные. Мы рассмотрим концепцию класса более подробно в главе3.

2.7.3. Зацепление и связность

Двумя важными понятиями при разработке программ являются зацепление (coupling) и связность(cohesion). Связность— это мера того, насколько отдельная компонента образует логически законченную, осмысленную единицу. Высокая связность достигается объединением в одной компоненте соотносящихся(в том или ином смысле) друг с другом функций. Наиболее часто функции оказываются связанными друг с другом при необходимости иметь доступ к общим данным. Именно это объединяет разные части компонентыRecipe.

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

модификации и мешают дальнейшей разработке или повторному использованию в других программах.

В частности, зацепление возникает, если одна программная компонента должна иметь доступ к данным(состоянию) другой компоненты. Следует избегать подобных ситуаций. Возложите обязанность осуществлять доступ к данным на компоненту, которая ими владеет. Например, в случае с нашим проектом кажется, что ответственность за редактирование рецептов должна лежать на компонентеRecipe Database, поскольку именно в ней впервые возникает в этом необходимость. Но тогда объектRecipe Database должен напрямую манипулировать состоянием отдельных рецептов(их внутренними данными: списком ингредиентов и инструкциями по приготовлению). Лучше избежать столь тесного зацепления, передав обязанность редактирования непосредственно рецепту.

Более подробно о связности и зацеплении, а также о соответствующей технике программирования рассказывается в главе17.

2.7.4. Интерфейс и реализация модуля — принципы Парнаса

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

 

Например предположим, что шесть компонент приложенияIIKH создаются шестью программистами. Программист, разрабатывающий компонентуMeal, должен обеспечить просмотр базы данных с рецептами и выбор отдельного рецепта при составлении блюда. Для этого компонентаMeal просто вызывает функциюbrowse, привязанную к компонентеRecipe Database. Функцияbrowse возвращает отдельный рецептRecipe из базы данных.

Все это справедливо вне зависимости от того, как конкретно реализован внутриRecipe Database просмотр базы данных.

Мы прячем подробности реализации за фасадом интерфейса. Происходит маскировка информации. Говорят, что компонента инкапсулирует поведение, если она умеет выполнять некоторые действия, но подробности, как именно это делается, остаются скрытыми. Это естественным образом приводит к двум различным представлениям о программной системе. Вид со стороны интерфейса— это лицевая сторона; ее видят другие программисты. В интерфейсной части описывается, что умеет делать компонента. Вид со стороны реализации— это «изнанка», видимая только тем, кто работает над конкретной компонентой. Здесь определяется, как компонента выполняет задание.

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

Интерес к многократному использованию программных компонент общего назначения в разных проектах возрастает. Для осуществления подобного, связи между различными частями системы должны быть минимальны и прозрачны.

Как мы уже отмечали в предыдущей главе, эти идеи были сформулированы специалистом по информатике Дэвидом Парнасом в виде правил, часто называемых принципами Парнаса:

∙Разработчик программы должен предоставить пользователю всю информацию, которая нужна для эффективного использования приложения, и ничего кроме этого.

∙Разработчик программного обеспечения должен знать только требуемое поведение компоненты и ничего кроме этого.

Следствие принципа отделения интерфейса от реализации состоит в том, что программист может экспериментировать с различными алгоритмами, не затрагивая остальные компоненты программы.

2.8. Формализация интерфейса

Продолжим разработку программы IIKH. На следующих нескольких этапах уточняется описание компонент. Сначала формализуются способы взаимодействия.

Следует определить, как будет реализована каждая из компонент. Компонента, характеризуемая только поведением(не имеющая внутреннего состояния), может быть

 

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

2.8.1. Выбор имен

Имена, связанные с различными действиями, должны тщательно подбираться. Шекспир сказал, что переименование не меняет сути объекта1 , но определенно не все имена будут вызывать в воображении слушателя одинаковые мысленные образы.

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

Были предложены следующие положения общего характера, регулирующие этот процесс

[Keller 1990]:

∙Используйте имена, которые можно произнести вслух. Основное правило: если вы не можете громко прочитать имя, забудьте о нем.

∙Применяйте заглавные буквы или символы подчеркивания для того, чтобы отметить начало нового слова в составном имени: CardReader илиCard_reader вместо нечитаемогоcardreader.

∙Тщательно проверяйте сокращения. Сокращение, ясное для одного человека, может быть загадочным для другого. Обозначает ли имяTermProcess последний процесс в цепочке(terminal process), или нечто, что прекращает выполнение процесса(terminate process), или же процесс, связанный с терминалом компьютера?

∙Избегайте многозначности имен. Имяempty для функции— обозначает ли оно проверку того, что некоторый объект пуст, или же она удаляет все значения из объекта(делает его пустым)?

∙Не используйте цифры в именах. Их легко спутать с буквами(0 какO, 1 какl, 2 как

Z, 5 как S).

∙Присваивайте функциям, которые возвращают логические(булевские) значения, такие имена, чтобы было ясно, как интерпретироватьtrue иfalse. Например, PrinterIsReady ясно показывает, что значениеtrue соответствует принтеру в рабочем состоянии, в то время какPrinterStatus является гораздо менее точным.

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

 

Как только для всех действий выбраны имена, CRC-карточкадля каждой

компоненты переписывается заново с указанием имен функций и списка формальных аргументов. ПримерCRC-карточкидля компонентыDate приведен на рис. 2.5. Что осталось не установленным, так это то, как именно каждая компонента будет выполнять указанные действия.

1  «Что значит имя? Роза пахнет розой, хоть розой назови ее, хоть нет. Ромео под любым названьем был бы тем верхом совершенств, какой он есть». — Вильям Шекспир,  «Ромео и Джульетта», действиеII, сцена2 (пер. Бориса Пастернака).

Необходимо еще раз  «прокрутить» сценарий более детально, чтобы гарантировать, что все

действия учтены и вся необходимая информация имеется и доступна для соответствующих компонент.

Рис. 2.5. Обновленная CRC-карточкадля компонентыDate

2.9. Выбор представления данных

На данном этапе, если только это не было сделано раньше, происходит разделение команды разработчиков на группы, каждая из которых отвечает за конкретные компоненты программы. Задача теперь состоит в переходе от описания компоненты к конкретному коду. Главное здесь— проектирование структур данных, которые будут использоваться каждой из подсистем для хранения внутренней информации, необходимой для выполнения предписанных обязанностей.

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

Структуры данных должны точно соответствовать рассматриваемой задаче.

Неправильный выбор структуры может привести к сложным и неэффективным программам.

На этом же этапе описание поведения компонент должно быть преобразовано в алгоритмы. Реализованные функции затем сопоставляются с потребностями компоненты,

 

являющейся клиентом данного фрагмента, чтобы гарантировать, что все ее запросы

оказываются выполненными и что все необходимые для ее работы данные являются доступными.

2.10. Реализация компонент

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

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

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

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

Важной частью анализа и кодирования на этом этапе является полная характеризация и документирование необходимых предварительных условий, которые требуются программной компоненте для выполнения задания. Также следует проверить, правильно ли работает программная компонента, если вызвать ее с правильными входными значениями. Это подтвердит корректность алгоритмов, использованных при реализации компоненты.

2.11. Интеграция компонент

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

Например, при разработке программыIIKH было бы разумным начать интегрирование с компонентыGreeter. Чтобы протестировать ее в изоляции от остальных блоков программы, потребуются заглушки для управляющего кода базы данных с рецептамиRecipe Database и блока управления планированием питанияMeal Plan. Заглушки просто должны выдавать информационные сообщения и возвращать управление. Таким образом, команда разработчиков компонентыGreeter сможет протестировать различные аспекты

 

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

Затем заглушки заменяются более серьезным кодом. Например, вместо заглушки для компонентыRecipe Database можно вставить реальную подсистему, сохранив заглушки для остальных фрагментов. Тестирование продолжается до тех пор, пока не станет ясно, что система работает правильно. Этот процесс называют тестированием системы в целом.

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

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

2.12. Сопровождение и развитие

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

∙В переданном продукте могут быть обнаружены ошибки. Они должны быть исправлены либо через «заплатки» (patches) к существующей версии, либо в новой версии.

∙Изменение требований — возможно, из-зановых государственных постановлений или стандартизации.

∙Переход на другое аппаратное обеспечение. Например, в результате переноса системы на другие платформы или поддержки новых устройства ввода(световое перо или сенсорный экран). Может измениться технология вывода: скажем, вы перешли от текстового интерфейса к графическому.

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

∙Потребность в улучшенной документации.

Хороший проект предусматривает неизбежность изменений и подготавливает их с самого начала.

Упражнения

1.Опишите распределение обязанностей в организации, которая включает по крайней мере шесть членов. Рассмотрите учебное заведение(студенты, преподаватели, директор, гардеробщик), фирму(совет директоров, президент, рабочий) и клуб(президент, вице-президент, рядовой член). Опишите обязанности каждого члена организации и его сотрудников(если они есть).

 

2.Создайте с помощью диаграммы взаимодействия сценарий для организации из упражнения 1.

3.Для типичной карточной игры опишите программную систему, которая будет взаимодействовать с пользователем в качестве противоположного партнера. Типичные компоненты должны включать игровой стол и колоду карт.

4.Опишите программную систему для управления ATM (Automatic Teller Machine). Поскольку словоTeller достаточно многозначно(рассказчик, счетчик голосов при выборах, кассир в банке, диктор радиолокационной станции ПВО и т. д.), то у вас имеется большая свобода в выборе предназначения этой машины. Приведите диаграммы взаимодействия для различных сценариев использования этой машины.

Глава 3 : Классы и методы

Хотя термины, которые используются в объектно-ориентированныхязыках, отличаются, понятия классов, экземпляров, пересылки сообщений, методов и наследования являются общими. Эти термины были введены в главе1. Как уже отмечалось, использование различных терминов для одних и тех же понятий широко распространено в  объектно- ориентированных языках. Мы будем применять единую и, как мы надеемся, ясную терминологию для всех языков программирования. В вводных разделах будем отмечать специфику конкретных языков и различные синонимы для наших терминов. Обращайтесь к разделу»Глоссарий» за разъяснением незнакомых понятий.

В этой главе речь идет о статических атрибутах классов; в главе4 мы рассмотрим их динамическое использование. Здесь же мы проиллюстрируем механизмы объявления класса и определения методов. В главе4 объясняется, как создаются экземпляры класса и как им передаются сообщения. Мы отложим анализ механизмов наследования до главы7.

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

3.1. Инкапсуляция

Вглаве1 мы заметили, что объектно-ориентированное программирование и, в особенности, объекты могут рассматриваться со многих точек зрения. В этой главе мы будем представлять себе объекты как абстрактные типы данных.

Впрограммировании, основанном на абстракции данных, информация сознательно прячется в небольшой части программы. В частности, каждый объект из набора абстрактных типов данных, разрабатываемого программистом, имеет два»лица». Это аналогично дихотомии принципов Парнаса, которые обсуждались в главе2. С внешней точки зрения(клиента или пользователя) абстрактный тип данных представляет собой всего лишь совокупность операций, которые определяют поведение абстракций. С

 

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

Например, для абстрактного типа данныхstack пользователь видит только описание допустимых операций— скажем, push, pop, top. С другой стороны, программисту, реализующемуstack, необходимо манипулировать с конкретными структурами данных(рис. 3.1). Конкретные детали инкапсулированы в более абстрактный объект.

Рис. 3.1. Интерфейс и реализация для типа stack

Мы использовали термин экземпляр для обозначения представителя класса.

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

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

3.2. Разновидности классов

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

∙управление данными;

∙источники данных или посредники в передаче данных;

∙классы для просмотра данных;

∙вспомогательные, или упрощающие проектирование, классы.

Этот список не является исчерпывающим, однако он вполне подходит для учебных целей.

Большинство  объектно-ориентированныхприложений включают как классы вышеперечисленных категорий, так и другие. Если оказывается, что класс»разрывается» между двумя категориями, то зачастую его можно разбить на два класса.

Классы-администраторыданныхData Managers, часто получающие именаData илиState, — это классы, основной обязанностью которых является поддержка данных или информации о состояниичего-либо. Например, для абстрактной модели игры в карты основная задача классаCard состоит в том, чтобы хранить масть и ранг(достоинство) карты. Классы-администраторыданных обычно являются фундаментальными строительными блоками проекта, а их прототипами в спецификации проекта являются существительные.

 

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

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

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

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

К вспомогательным классам (Facilitator иHelper) разумно отнести те классы, которые не содержат полезной информации, но облегчают выполнение сложных заданий. Например, при отображении игральной карты мы используем вспомогательный класс, рисующий линии и текст на устройстве вывода. Другой служебный класс может, например, обслуживать связный список карт(колоду).

3.3. Пример: игра в карты

Мы используем программную абстракцию типичной игры в карты, чтобы познакомить вас с различными объектно-ориентированнымиязыками программирования, которые рассматриваются в этой книге. В следующей главе мы используем разработанный здесь классCard для написания пасьянса»косынка». КлассCard, подобно настоящим игральным картам, мало что знает о своем предполагаемом использовании и может применяться в карточной игре любого типа.

Рис. 3.2. CRC-карточкадля классаCard

 

На рис. 3.2 показанаCRC-карточка, которая описывает поведение игральной карты. Обязанности классаCard очень ограничены. В своей основе он является просто администратором данных, который хранит и возвращает значения масти и ранга, а также рисует карту.

Как мы отмечали в главе 2, CRC-карточкимного раз уточняются и перерисовываются, медленно эволюционируя от естественного языка к программному коду. Как мы помним, на следующем этапе каждый метод снабжается именем и списком аргументов. Не исключено, что описание вылезет за карточку, и тогда их придется скреплять скрепками(имеет смысл вообще отказаться от карточек, заменив ихчем-товроде отчетов).

CRC-карточка, изображенная на рис. 3.3, соответствует следующему этапу. Заметьте, что даже если обязанность состоит всего лишь в возврате значения(например, признака»картинка вверх»), мы все равно определяем функцию для посредничества в выполнении запроса. Имеются как практические, так и теоретические соображения в пользу этого. Мы вернемся к ним в главе17.

Как было предложено ранее, можно выделить и записать на оборотной сторонеCRC- карточки значения данных, которые должны содержаться в каждом экземпляре класса игральной карты. Следующий этап состоит в переводе поведения и состояния, описанных наCRC-карточке, в выполняемый код. Мы рассмотрим этот этап после того, как исследуем дихотомию между объявлением и определением.

Рис. 3.3. Уточнение CRC-карточкидля классаCard

3.4. Интерфейс и реализация

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

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

∙Методам должна быть доступна вся информация, необходимая для выполнения их обязанностей, и никакая другая.

 

Принципы Парнаса делят мир объекта на две части. Имеется внешний образ, наблюдаемый пользователем объекта, — мы будем называть это представление об объекте интерфейсом(interface), поскольку оно описывает, как объект взаимодействует с внешним миром. Обратная сторона объекта связана с его реализацией(implementation). Пользователю разрешен доступ только к тому, что описано в интерфейсной части. Реализация определяет, как достигается выполнение обязанностей, заявленных в интерфейсной части.

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

3.5. Классы и методы в ООП

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

3.5.1. Классы и методы в языке Object Pascal

По крайней мере два различных языка носят имя Object Pascal. Исходным является язык, созданный Ларри Теслером из компанииApple Computer [Tesler 1985]. Язык был построен на основе модулей из языкаApple Pascal. Второй вариант языкаObject Pascal первоначально называлсяTurbo Pascal [Turbo 1988, O’Brian 1989]. Его разработала и распространяла компанияBorland International. Первый упомянутый язык очень часто встречается на компьютерахMacintosh, второй большей частью связан сIBM PC. Язык, созданный компаниейBorland, вновь привлек внимание кObject Pascal. Сейчас этот язык используется в качестве фундамента в средеDelphi для разработкиWindows-приложений[Borland 1995]. В него были введены новые свойства, отсутствовавшие в исходном языкеTurbo Pascal. В данной книге мы постараемся описать обе версии языка, отмечая особо, где и в чем они различаются.

В языке Object Pascal модуль называется библиотекой процедур(unit). В отличие от языковC++ иObjective-C библиотека процедур содержится в едином файле, а не разбивается на два. Тем не менее библиотека процедур состоит из интерфейса(interface) и реализации(implementation). Библиотека процедур может подключать другие библиотеки. Этот процесс делает доступными свойства, описанные в разделе интерфейса подключаемой библиотеки.

Часть библтотеки для класса Card в языкеObject Pascal версии фирмыApple показана в листинге3.1. Разделinterface аналогичен описаниям функций вPascal. Он может содержать подразделы, обозначаемые ключевыми словамиconst, type иvar. Здесь же задаются необъектные типы данных(такие, как перечисляемые типыsuits иcolors).

 

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

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

Листинг 3.1. Интерфейсный раздел библиотеки для языка Object Pascal фирмыApple

unit card; interface type

suits = (Heart, Club, Diamond, Spade); colors = (Red, Black);

Card = object

(* поля данных *) suitValue : suits; rankValue : integer;

faceUp *) : boolean;
(* инициализация
procedure setRankAndSuit (c : integer; s : suits);
(* рабочие функции *)
function color : colors;
procedure draw (win : windows; x, y : integer);
function faceUp : boolean;
procedure flip; : integer;
function rank
function suit : suits;
end;

implementation

end.

Листинг 3.2. Интерфейсный раздел библиотеки для языка Deplhi Pascal

implementation const

CardWidth = 65;

CardHeight = 75;

function Card.color : colors; begin

case suit of Diamond: color:= Red; Heart: color:= Red;

Spade: color:= Black; Club: color:= Black; end;

end;

 

end.

3.5.2. Классы и методы в языке Smalltalk

Описание языка Smalltalk почти неразрывно связано с пользовательским интерфейсом средыSmalltalk. Таким образом, объяснение того, как в языкеSmalltalk создаются новые классы, должно обязательно начинаться с описания программы просмотра или броузераSmalltalk. Не только собственно броузер является достаточно сложным, но и детали его реализации отличаются для различных систем. Поэтому наше обсуждение необходимым образом будет поверхностным. Читатель, заинтересованный в более подробной информации, должен обратиться к руководству по той версии языкаSmalltalk, которую он использует[Goldberg 1984, LaLonde 1990b, Korienek 1993, Smith 1995].

Для пользователя броузер представляет собой большое окно, разделенное на пять окошек— четыре маленьких и одно большое(рис. 3.4). Каждое из верхних окон имеет полосы прокрутки. Нижнее окно используется для высвечивания и редактирования информации. Броузер управляется посредством мыши, которая должна иметь три кнопки: левая кнопка используется для операций выбора и редактирования, средняя и правая кнопки вызывают меню с операциями.

Классы в языке Smalltalk сгруппированы в категории. В первом окне прокручивается список всех категорий, известных системеSmalltalk. Хотя можно создать новую категорию, для наших целей достаточно выбрать существующую категорию и сконструировать относящийся к ней новый класс. Выбор элемента»Graphics-Primitives» в первом окне приведет к выполнению двух действий: во втором окне отобразится список всех классов, относящихся к данной категории, а в большом окне редактирования появится текст сообщения, вызываемого при создании новых классов.

После редактирования с использованием мыши пользователь может заменить это сообщение на:

Object subclass: #Card instanceVariableNames: ‘suit rank’ classVariableNames: » poolDictionaries: »

category: ‘Graphics-Primitives’

В данный момент мы будем рассматривать это сообщение просто в качестве иллюстрации того, чтоCard создается как подклассObject.

Каждый экземпляр класса Card содержит два поля данных. Как и в языкеDelphi Pascal, все классы должны быть подклассами уже существующих классов. КлассObject является наиболее общим порождающим классом.

Заметим, что имена полей данных не связаны скаким-либоконкретным типом данных. ЯзыкSmalltalk не имеет операторов объявления типа, и переменные могут принимать произвольные значения. Мы поговорим о принципиальной разнице между языками с типами данных и языками без таковых позднее, при обсуждении пересылки сообщений. В нашем классе нет переменных класса ипеременных-словарей(pool variables). Эти понятия относятся к следующему уровню сложности. Переменные класса будут обсуждаться в последующих главах. Переменные-словаривыходят за рамки настоящей книги.

 

Знак # перед словомCard идентифицирует его как символ. Наиболее важное свойство символов— однозначное соответствие между именем и значением. То есть именованные символы могут иметь различные значения, но все символы с одним именем соответствуют одному и тому же значению. Тем самым символы обычно используются как ключи или заменители категорий.

Закончив с определением характеристик нового класса, пользователь выбирает командуaccept из меню операций. Теперь новый класс введен в систему, и третье окно высвечивает разрешенные операции(первоначально список пуст).

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

Чтобы создать новый метод, пользователь редактирует шаблон и выбирает командуaccept из меню операций, когда редактирование закончено. Ниже показан метод, инициализирующий масть и ранг игральной карты.

setSuit: s rank: r

» устанавливает значения переменных экземпляра suit и rank » suit := s.

rank := r

В языке Smalltalk аргументы разделяются ключевыми словами, которые легко распознаются, поскольку оканчиваются двоеточием(идентификаторы не могут оканчиваться двоеточием). Тем самым именем метода, определенного выше, являетсяsetSuit:rank:. Метод имеет два аргумента, которые известны ему как идентификаторыs иr. В некоторых версиях языкаSmalltalk оператор присваивания записывается как стрелка, в большинстве других версий используется более традиционное обозначение:=. Наконец, можно заметить, что точка применяется в качестве разделителя операторов, и для последнего оператора ее можно опустить.

Доступ к переменным экземпляра не из методов класса в языке Smalltalk запрещен. Следовательно, мы должны определить явные функции доступа(accessor functions). Методsuit, показанный ниже, возвращает текущее значение переменной экземпляра с тем же именем:

suit

» вернуть значение масти для данной карты » suit

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

Целые числа от 1 до13 — это значения, представляющие ранг карты(то есть значение поляrank). Мы будем использовать символы(в смыслеSmallTalk) для представления

 

масти карты. Соответственно методcolor (цвет карты) тоже будет возвращать символ в качестве результата. Следующий код описывает этот метод:

color
» вернуть цвет данной карты « [ #red ]
(suit = #diamond) ifTrue:
(suit = #club) ifTrue: [ #black ]
(suit = #spade) ifTrue: [ #black ]
(suit = #heart) ifTrue: [ #red ]

Обратите внимание, что условные операторы в языкеSmalltalk записываются так, как если бы они были сообщениями, пересылаемыми условной части(на самом деле так оно и есть). Квадратные скобки образуют то, что вSmalltalk называетсяblocks, их можно рассматривать как конструкцию, аналогичную блокам в языкеPascal (пара командbegin, end). (В действительности все сложнее. Фактически блок сам по себе является объектом, который пересылается в качестве аргумента вместе с сообщениемifTrue к булевскому объекту. НачинающимSmalltalk-программистамлучше проигнорировать подробности.)

3.5.3. Классы и методы в языке Objective-C

Язык программирования Objective-C — это объектно-ориентированноерасширение директивного языкаC. В качестве такового он наследует большую часть структур и методов использованияC. В частности, реализация модулей основана на стандартном соглашении языкаC о разделении файлов на две категории: интерфейсные файлы(обычно с расширением».h») и файлы реализации(в языкеC они обычно имеют расширение».c», а вObjective-C —».m»). Предполагается, что пользователю класса(первая категория людей, перечисляемая в дихотомии Парнаса) требуется просмотреть только интерфейсные файлы.

Интерфейсный файл, подобный тому, что используется для нашей абстракции игральных карт(листинг3.4), служит двум целям. Для программиста он является удобным средством документирования назначения и функционирования класса. Для системы он передает информацию о типах данных и требованиях к оперативной памяти. Иногда эти два применения оказываются в конфликте друг с другом. Например, в языкеObjective-C, как и в языкеSmalltalk,

Листинг 3.4. Интерфейсный файл класса Card на языкеObjective-C

/*

описание интерфейса класса Card язык программирования: Objective-Cавтор: Тимоти Бадд, 1995 */

# import <objc/Object.h>

/* определить символьные константы для мастей */

# define Heart 0
# define Club 1
# define Diamond 2
# define Spade 3

/* определить символьные константы для цветов */

# define Red 0
# define Black 1
/* интерфейс класса Card */
@ interface Card : Object
{ suit;
int
int rank;

 

int faceup;
}
/* методы класса Card */
— (void) suit: (int) s rank: (int) c;
— (int) color;
— (int) rank;
— (int) suit;
— (void) flip;
— (void) drawAt: (int) and: (int);
@ end

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

Несколько первых строк интерфейсного файла содержат код, общий для языковC иObjective-C. Директиваimport аналогична директивеinclude из языкаC, за исключением того, что она гарантирует, что файл подключается только один раз. В данном случае импортируемый файл— это описание интерфейса классаObject. Директиваdefine задает некоторые символьные константы, которые мы будем использовать для обозначения мастей и цветов.

Символ @ обозначает начало кода, специфического дляObjective-C. В данном случае код описывает интерфейс классаCard. Разрешается записывать интерфейсы для нескольких классов в одном интерфейсном файле, хотя обычно каждый класс имеет отдельный файл. В языкеObjective-C, так же как и в языкахSmalltalk иDelphi Pascal, каждый класс должен являться подклассом уже существующего класса; классObject является наиболее общим порождающим классом.

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

Как и для указателей в языке C, переменная, объявленная с типом данныхid, может содержать либо допустимое значение, либо специальное значениеNull.

Строки, следующие за описанием данных, описывают методы, которые связаны с данным классом. Описание каждого метода начинается с символа»-» (дефис) в первой колонке, за которым может следовать выражение, аналогичное приведению типов данных вC. Оно показывает тип значения, возвращаемого методом. Тип объекта(id) предполагается по умолчанию, если не указано ничего другого. Тем самым методsuit (заметьте, что методы могут иметь те же имена, что и поля данных) возвращает значение типаinteger. Методflip описан как имеющий типvoid. В языкеC это является индикатором того, что возвращаемое значение отсутствует(то есть метод является процедурой, а не функцией). Точнее будет сказать, что возвращаемое значение игнорируется. Как и раньше, табуляция, комментарии и алфавитное упорядочивание делают описание более понятным.

 

Методы, которым требуются аргументы(вроде функции перемещения карты или метода, проверяющего попадание точки внутрь области, ограниченной полем карты), записываются в стиле языкаSmalltalk с ключевыми словами, разделяющими список аргументов. Однако в отличие от языкаSmalltalk каждый аргумент должен сопровождаться описанием типа данных, причем при отсутствии такого описания подразумевается типid. Указание типа дается такой же синтаксической конструкцией, какая используется для описания типа данных результата функции.

Файл реализации (листинг3.5) начинается с импорта интерфейсного файла для нашей абстракции игральной карты. Код языкаObjective-C может свободно смешиваться с кодом обычногоC. Например, в листинге3.5 две строчки, определяющие символьные константы для длины и ширины игральной карты, используют синтаксисC.

Директива implementation определяет фактический код для методов, связанных с классом. Как имя родительского класса, так и определения переменных экземпляра в областиimplementation иногда опускают— они могут быть взяты из описания интерфейса.

Листинг 3.5. Файл реализации класса Card на языкеObjective-C

/*

файл реализации для класса Card язык программирования: Objective-C

автор: Тимоти Бадд, 1995
*/
# import «card.h» 68
# define cardWidth
# define cardHeight 75
@ implementation Card
— (int) color
{

return suit % 2;

}

— (int) rank

{

return rank;

}

— (void) suit: (int) s rank: (int) c

{

suit = s; rank = c; faceup = 0;

}

//… кое-чтоопущено

@end

Нет необходимости, чтобы методы в областиimplementation следовали в том же порядке, как они были заданы в интерфейсной части. Чтобы упростить поиск конкретного метода, их часто перечисляют в алфавитном порядке. Заголовки методов повторяют интерфейсный файл, но только теперь за ними следует тело метода. Как и в языкеC, тело функции заключено в фигурные скобки.

3.5.4. Классы и методы в языке C++

Язык C++, подобноObjecive-C, является объектно-ориентированнымрасширением директивного языка программированияC. Как и вC, полезно различать интерфейсные файлы(имеющие расширение».h») и файлы реализации(расширение зависит от системы).

 

Как и в языке Objective-C, интерфейсный файл(описывающий, например, абстракцию игральной карты) может содержать описания более чем одного класса, хотя обычно это происходит, только если классы тесно связаны. Поскольку языкиC иC++ не поддерживают ключевого словаimport (в отличии отObjective-C), для достижения того же эффекта используется условное подключение файла. Когда файлcard.h считывается впервые, символCARDH (предполагается, что он не встречается в других местах) является неопределенным, и тем самым срабатывает условный операторifndef (если не определено), так как значениеCARDH действительно не определено. Значит, файлcard.h будет считан. При всех последующих попытках считать этот файл символ будет известен, и загрузка файла будет пропущена.

# ifndef CARDH // файл должен читаться лишь один раз

#define CARDH

. . .

#endif

Описание класса начинается с ключевого слова class (листинг3.6). ВC++ описание класса во многом напоминает структуру в языкеC, за исключением того, что вместе с полями данных стоят заголовки процедур. Ключевое словоprivate: предшествует фрагментам кода, доступ к котором разрешен только из самого класса. Ключевое словоpublic: обозначает область интерфейса— то есть то, что видно извне класса. Как и в языкеObjective-C, описание переменных экземпляра в областиprivate дается в интерфейсном файле только ради компилятора(чтобы он мог определить размер памяти, требуемой для объекта), а для пользователя данного класса эти поля остаются недоступными(что является нарушением первого принципа Парнаса).

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

Функция card(suit, int) в описании класса является уникальной во многих отношениях— не только потому, что ее имя совпадает с именем класса, но и потому, что у нее нет возвращаемого значения. Эта функция называется конструктором, она используется при инициализации создаваемых экземпляров класса. Мы обсудим конструкторы более подробно в главе4.

Ключевое слово void, как и в языкеObjective-C, показывает отсутствие типа. Когда оно используется как тип возвращаемого значения, это означает, что метод применяется как процедура ради побочного эффекта, а не для вычисления результата.

Методы draw иhalfdraw иллюстрируют описание типов параметров как составной части объявления функции. Этот стиль декларирования называется прототипом функции. Теперь он является частьюANSI стандарта языковC иC++. Заметьте, что прототип аналогичен списку аргументов, хотя аргументы представлены как типы данных и их имена являются необязательными.

Аргумент с типом данных window, обрабатываемый функциейdraw, передается через ссылку. На это указывает& в списке аргументов. Большие структуры, вроде описания окон(тип данныхwindow в нашем примере), часто передаются через ссылку.

Листинг 3.6. Описание класса сard на языкеC++

enum suits {diamond, club, heart, spade};

 

enum colors {red, black};

//абстракция игральной карты

//используется в пасьянсе

//язык программирования: C++

//автор: Тимоти Бадд, 1995

class card{ public:

//конструктор

card (suits, int);
// доступ к атрибутам карты ();
colors color
bool faceUp ();
int rank ();
suits suit ();

// выполняемые действия

void void draw (window &);
halfdraw (window &, int x, int y);
void flip ();
private: faceup;
bool r;
suits int // ранг
s; // масть
};

Поскольку методы рассматриваются просто как поля специального вида, принадлежащие объекту и неразличимые от полей данных, метод и поле данных не могут иметь общего имени. Тем самым переменнаяs хранит значение, представляющее собой масть карты, в то время как методsuit возвращает это значение. Аналогично идентификаторыr иrank нужны для хранения и для возврата ранга карты.

Файл реализации для данного класса должен обеспечить работу методов, описанных в интерфейсном файле. Начало файла реализации для нашей абстракции игральной карты показано ниже.

//

//файл реализации

//для абстракции игральной карты

# include «card.h» card::card (suits sv, int rv)

{

s = sv; // инициализировать масть
r = rv; // инициализировать ранг

faceup = true; // начальное положение — картинкой вверх

}

int card::rank()

{

return r;

}

Тело метода записывается как стандартная функция языка C, но имени метода предшествует имя класса и два двоеточия. На переменные экземпляра(поля данных класса) можно ссылаться внутри метода как на обычные переменные. Комбинация имени класса и имени метода образует полное имя, она может рассматриваться как аналог имени и фамилии при идентификации личности.

Чтобы вдохновить программистов использовать такие принципы разработки программ как абстрагирование и инкапсуляция, язык программированияC++ предоставляет им возможность определять встраиваемые функции. Для того кто к ней обращается,

 

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

inline int Card::rank()

{

return r;

}

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

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

Листинг 3.7. Описание класса сard сinline-методами, языкC++

//абстракция игральной карты

//язык программирования: C++

//автор: Тимоти Бадд, 1995 class card

{

public:

//конструкторы

card (suits, int); card ();

card (const card & c);

// доступ к атрибутам карты

int rank() {return r;}
suits suit()
{return s;}
colors color();
bool faceUp() {return faceup;}
//выполняемые действия
void draw (window & w, int x, int y);
void halfdraw (window & w, int x, int y);
void flip()
{faceup = ! faceup;}
private:
bool faceup; // ранг
int r;
suits s; // масть
};

Как следует из ее названия, встраиваемая функция будет встроена в код. Таким образом, вызова функции(с точки зрения машинного кода) не произойдет. С другой стороны, часто возникает множество копий тела функции, так что встраивание следует использовать для

 

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

При интенсивном использовании встраиваемых функций вполне реально, что файл реализации окажется короче файла с интерфейсом.

Встраиваемые функции также можно определять, задавая тело функции непосредственно внутри определения класса(см. листинг3.7). Однако это приводит к тому, что

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

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

3.5.5. Классы и методы в языке Java

Трудно сказать, следует ли описывать языкJava как диалектC++. Хотя сначала кажется, что эти два языка имеют много общего, внутренние различия достаточно значительны, что оправдывает дляJava статус совершенно нового языка. С одной стороны, языкJava не имеет указателей, ссылок, структур, объединений, оператораgoto, функций(есть методы), перегрузки операторов. С другой стороны, он поддерживает строки как примитивный тип данных(что не делаетC++) и использует»сборку мусора» для управления памятью.

Хотя сам по себе Java является языком программирования общего назначения, недавний интерес к нему связан с его использованием в качестве средства разработки дляWorld Wide Web. В нашем изложении мы будем игнорировать этот аспект и сконцентрируемся на свойствахJava как одного из языков программирования.

Описание класса на языке Java (пример приведен в листинге3.8) очень похоже на определение класса в языкеC++ за исключением следующих отличий:

∙Отсутствуют препроцессор, глобальные переменные, перечисляемые типы данных.

Символьные константы могут быть созданы путем описания и инициализации локальных переменных с использованием ключевого слова final. Такие»терминальные» значения не могут впоследствии изменяться и тем самым оказываются эквивалентными символьным константам.

Листинг 3.8. Стандартное описание класса на языке Java

class Card

{

// статические значения цветов и мастей

final public int red = 0;
final public int black = 1;
final public int spade = 0;
final public int heart = 1;

 

final public int diamond = 2;
final public int club = 3;
// поля данных faceup;
private boolean
private int r;
private int s;
// конструктор

Card (int sv, int rv)

{s = sv; r = rv; faceup = false; } // доступ к атрибутам карты

public int rank ()

{return r; }

public int suit()

{return s; } public int color ()

{if ( suit() == heart ЅЅ suit() == diamond ) return red;

return black; } public boolean faceUp() { return faceup; }

// выполняемые действия

public void draw (Graphics g, int x, int y)

{

/* … пропущено … */

}

public void flip ()

{ faceup = ! faceup; }

};

∙Реализация методов приводится непосредственно внутри определения класса, а негде-либов другом месте. (Это разрешено в языкеC++ в качестве опции, но является обязательным для языкаJava.)

∙Вместо разбиения описания класса на private иpublic эти ключевые слова присоединяются в явном виде к каждой переменной или методу.

∙Логический тип данных именуется boolean вместоbool, используемого в языке С++.

∙За исключением конструкторов (которые, как и в языкеC++, распознаются в силу того факта, что их имена совпадают с названием класса) все методы должны иметь возвращаемое значение.

Вглаве 8 мы рассмотрим учебный пример, целиком написанный на языкеJava.

Упражнения

1.Предположим, вам требуется программа на традиционном(не  объектно- ориентированном) языке программирования вроде Паскаля илиC. Как бы вы смоделировали классы и методы?

2.В языках Smalltalk иObjective-C методы, имеющие несколько аргументов, описываются с использованием ключевых слов, отделяющих каждый аргумент. В языкеC++ список аргументов идет сразу за именем метода. Опишите преимущества и недостатки, свойственные каждому подходу, — в частности, объясните влияние на читаемость и степень понимания текста программы.

3.Цифровой счетчик — это переменная с ограниченным диапазоном, которая сбрасывается, когда ее целочисленное значение достигает определенного максимума. Примеры: цифровые часы и счетчик километража. Опишите класс для такого счетчика. Обеспечьте возможность установления максимальных и

 

Объектно-ориентированное программирование: 1 комментарий

  1. Привет! Это комментарий.
    Чтобы начать модерировать, редактировать и удалять комментарии, перейдите на экран «Комментарии» в консоли.
    Аватары авторов комментариев загружаются с сервиса Gravatar.

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *