«Т. Бадд. Объектно-ориентированное программирование. Введение. Введение и общий замысел. Глава 1 дает неформальное определение базовых I. концепций объектно-ориентированного программирования. Глава 2 вводит принцип ...»
Т. Бадд.
Объектно-ориентированное
программирование.
Введение.
Введение и общий замысел. Глава 1 дает неформальное определение базовых
I.
концепций объектно-ориентированного программирования. Глава 2 вводит
принцип разработки на основе обязанностей. Эти две главы являются
фундаментальными, и их следует изучить подробно. В частности, я настоятельно
рекомендую выполнить по крайней мере одно упражнение с CRC-карточками из главы 2. Техника CRC-карточек, по моему мнению, является одной из лучших для определения функциональности, ответственности и инкапсуляции при базовой разработке проекта.
Классы, методы и сообщения Главы 3 и 4 определяют синтаксис, используемый в II.
языках Smalltalk, C++, Java, Objective-C и Object Pascal для задания классов, методов и посылки сообщений. Глава 3 заостряет внимание на статических свойствах (классах и методах), в то время как глава 4 описывает динамические аспекты (создание объектов и пересылку сообщений). Главы 5 и 6 развивают эти идеи. Здесь же начинаются обучающие примеры — образцы программ, разработанных в объектно-ориентированной манере и иллюстрирующих различные черты объектной техники.
Наследование и повторное использование кода Главы 7, 8 и 9 вводят концепцию III.
наследования и объясняют ее применение для обеспечения повторного использования кода. Пример из главы 8, написанный на языке Java, иллюстрирует также применение стандартного прикладного программного интерфейса (API — application program interface). В главе 9 противопоставляются наследование и композиция в качестве альтернативных техник обеспечения повторного использования кода.
Более подробно о наследовании. В главах с 10 по 13 концепция наследования IV.
анализируется более детально. Введение наследования оказывает влияние на почти все аспекты языка программирования, которое зачастую не сразу очевидно для начинающего. В главе 10 обсуждается поиск методов и их связывание с сообщениями. Там же иллюстрируется тот факт, что подклассы и подтипы — это не одно и то же. В главе 11 обсуждается семантика переопределения методов и отмечаются две совершенно различные интерпретации этого понятия. В главе продолжается тема переопределения и исследуются некоторые следствия наследования применительно к механизмам управления памятью, присваивания и сравнения. Наконец, в главе 13 изучается множественное наследование.
Полиморфизм. В значительной степени мощь объектно-ориентированного V.
программирования проистекает из применения различных форм полиморфизма. В главе 14 читатель знакомится с основными механизмами полиморфизма в объектно-ориентированных языках и двумя показательными обучающими примерами. Первый пример в главе 15 рассматривает создание библиотек общего назначения. Конкретная библиотека, а именно недавно разработанная стандартная библиотека шаблонов (STL — Standard Template Library) для языка С++, обсуждается в главе 16.
Разработка программного обеспечения. В главе 17 обсуждается ряд стандартных VI.
тем компьютерной инженерии в контексте объектно-ориентированного программирования. Глава 18 знакомит с несколькими относительно новыми концепциями — средой разработки приложений и шаблонами разработки. Оба подхода основаны на использовании наборов классов. Наконец, в главе приводится конкретный пример среды разработки.
Продвинутое изучение.Концепция классов при внимательном рассмотрении не VII.
столь проста, как нас пытались убедить в главе 3. В главе 20 рассмотрены более глубокие аспекты объектно-ориентированного программирования. Там же обсуждаются делегирование (являющееся примером объектно-ориентированного программирования без классов) и понятие метакласса (на уровне собственно языка программирования). В главе 21 в общих чертах описаны разнообразные техники реализации, применяющиеся при создании объектно-ориентированных языков.
В десятинедельном курсе, который я читаю в университете штата Орегон, приблизительно одну неделю я посвящаю каждому из основных направлений, описанных выше. В то же самое время студенты работают над не слишком большим проектом. Конкретный объектно-ориентированный язык разработки они выбирают сами. Семестр заканчивается представлением дизайна проекта и его реализацией.
Первое издание книги я закончил главой «Дополнительная информация». К сожалению, объектно-ориентированное программирование развивается так быстро, что любая дополнительная информация почти сразу устаревает. Поэтому я не включил во второе издание главу с таким названием. Вместо этого я попытаюсь поддерживать страничку Web с последними сведениями.
Как получить исходные тексты Исходные тексты обучающих примеров, представленных в книге, можно получить анонимно, обратившись через ftp по адресу ftp.cs.orst.edu, каталог /pub/budd/oopintro. В том же каталоге можно будет найти дополнительную информацию, например список ошибок, обнаруженных в книге, упражнения, копии «прозрачек», которые я использую в своем курсе. Все это можно также увидеть через World Wide Web на моих личных домашних страницах по адресу http://www.cs.orst.edu/~budd/oopintro. Вопросы вы можете посылать электронной почтой по адресу [email protected] или обычной почтой: 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. Язык и мышление Человеческие существа не общаются непосредственно с объективным миром и с обществом в том смысле, как это обычно понимается. Они в значительной мере зависят от того конкретного языка, который стал их средой общения. Это совершенная иллюзия — полагать, что кто-то может согласовать себя с сущностью реальности без использования языка и что язык — всего лишь случайное средство решения конкретных задач общения или мышления. Суть вопроса в том, что «реальный мир» в значительной степени неосознанно строится на языковых привычках группы людей... Мы видим, слышим и испытываем остальные ощущения так, как мы это делаем, в значительной степени потому, что языковые обычаи нашего общества предрасполагают к определенному выбору способа интерпретации.
Цитата подчеркивает тот факт, что язык, на котором мы говорим, непосредственно влияет на способ восприятия мира. Это справедливо не только для естественных языков, подобных тем, что изучались в начале двадцатого века американскими лингвистами Эдвардом Сапиром и Ли Ворфом, но также и для искусственных языков, наподобие тех, что мы используем в программировании.
1.2.1. Эскимосы и снег Примером, почти повсеместно цитируемым (хотя зачастую ошибочно — см.
[Pillum 1991]) в качестве иллюстрации того, как язык влияет на мышление, является тот «факт», что в эскимосских (или юитских) языках имеется множество слов для описания различных типов снежного покрова — мокрого, плотного, подмерзшего и т. д. Это-то как раз не является удивительным. Любое сообщество с общими интересами естественным образом разрабатывает специализированный словарь необходимых понятий.
Что действительно важно — не слишком абсолютизировать вывод, который мы можем сделать из этого простого наблюдения. Главное не в том, что глаз эскимосов в каком-то существенном аспекте отличается от моего собственного или что эскимосы могут видеть вещи, которые я не способен различать. С течением времени, с помощью тренировки, я бы стал ничуть не хуже различать разнообразные типы снежного покрова. Однако язык, на котором я говорю (а именно английский), не вынуждает меня заниматься этим, и тем самым указанные способности не являются для меня естественными.
Таким образом, различные языки (например, эвенкийский) могут привести (но не обязательно требуют этого) к тому, чтобы смотреть на мир с разных сторон.
Чтобы эффективно использовать ООП, требуется глядеть на мир иным способом. Само по себе применение объектно-ориентированного языка программирования (такого, как C++) не вынуждает стать объектно-ориентированным программистом. Использование объектно-ориентированного языка упрощает разработку объектно-ориентированных приложений, но, как было остроумно замечено, «программа фортрановского типа может быть написана на любом языке».
1.2.2.Пример из области программирования Связь между языком и мышлением для естественных языков, о которой мы говорили, является еще более заметной для искусственных компьютерных языков. Язык программирования, в терминах которого разработчик думает о проблеме, вносит особые оттенки и, вообще говоря, изменяет даже сам алгоритм.
Приведем пример, иллюстрирующий связь между компьютерным языком и способом решения задачи. Некоторое время назад один студент, работающий в области генетических исследований, столкнулся с необходимостью анализа последовательностей ДНК. Проблема могла быть сведена к относительно простой задаче. Молекула ДНК представляется в виде вектора из N целочисленных значений, где N очень велико (порядка десятков тысяч). Нужно было проверить, не является ли какой-либо участок длины M (M — фиксированная константа порядка 5–10) повторяющимся в последовательности ДНК.
ACTCGGATCTTGCATTTCGGCAATTGGACCCTGACTTGGCCA...
Программист, не долго думая, написал простую и прямолинейную программу на Fortran — нечто вроде 20 IF (X(I+K-1).NE.X(J+K-1)) FOUND=.FALSE.Он был неприятно разочарован, когда пробные запуски программы показали, что она потребует многих часов для завершения работы. Студент обсудил эту проблему со студенткой, которая оказалась профессионалом в программировании на языке APL. Она сказала, что могла бы попробовать написать программу для решения этой задачи. Студент был в сомнении: Fortran известен как один из наиболее «эффективных» компилируемых языков, а APL реализовывался с помощью интерпретатора. Таким образом, тот факт, что APL-программист способен составить алгоритм, который требует для работы минуты, а не часы, был воспринят с определенной дозой недоверия.
APL-программистка переформулировала задачу. Вместо того чтобы работать с вектором из N элементов, она представила данные в виде матрицы, имеющей приблизительно N строк и M столбцов:
TGGA C C
GGAC C C
Затем студентка отсортировала матрицу по строкам. Если какой-то фрагмент оказывается повторяющимся, то в отсортированной матрице две соседние строки должны оказаться идентичными.
T G G A C C
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.
Создание математического формализма вычислимости было связано с необходимостью определить понятие алгоритма. Пока исследования в этой области шли успешно, каждая новая формализованная последовательность вычислений получала имя «алгоритм» просто по определению. Когда же математики столкнулись с задачами, для которых пришлось доказывать отсутствие алгоритма, потребовалось формальное определение. В настоящий момент принято считать, что алгоритмом является последовательность действий, которая может быть сведена к программе, выполняемой с помощью машины Тьюринга. Или, в эквивалентной форме: последовательность действий, которая может быть сведена к программе для машины Поста, или конечного автомата Маркова, или же к последовательности рекурсивных функций Клини и Чёрча, является алгоритмом. Доказано, что все эти формальные системы вычислимости являются эквивалентными. Тем самым принцип Чёрча является аксиомой, не требующей доказательства, которая формализует понятие алгоритма («эффективной процедуры») и в силу статуса аксиомы опровергающего контрпримера иметь не может. — Примеч. перев.
Признание принципа Чёрча имеет важное и глубокое следствие для языков программирования. Машины Тьюринга являются изумительно простыми механизмами.
От языка программирования требуется немного, чтобы смоделировать такое устройство.
В 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. показывает такую иерархию классов для Фло. Эта же самая иерархия включ ает в себя мою жену Бет, собаку Флеш, Фила — утконоса, живущего в зоопарке, а также цветы, которые я послал своей бабушке.
Рис. 1.2. Иерархическое дерево классов, представляющих различные материальные Поскольку Фло — человек, та информация о ней, которой я обладаю, применима также, к примеру, к моей жене Бет. Те данные, которыми я располагаю в силу принадлежности последней к классу млекопитающих, имеют также отношение к моей собаке Флеш.
Информация об объектах как о вещах материальных имеет смысл в отношени Фло, и ее цветов. Мы выражаем все это в виде идеи наследования:
Классы могут быть организованы в иерархическую структуру с наследованием свойств.
Дочерний класс (или подкласс) наследует атрибуты родительского класса (или надкласса), расположенного выше в иерархическом дереве 1. Абстрактный родительский класс — это класс, не имеющий экземпляров (его примером может служить Mammal на рис. 1.2).
Он используется только для порождения подклассов.
1.4.5. Связывание и переопределение методов Утконос Фил представляет собой проблему для нашей простой структуры. Я знаю, что млекопитающие являются живородящими, но Фил определенно является млекопитающим, хотя он (точнее, его подруга Филлис) кладет яйца. Чтобы включить его в нашу схему, мы должны найти технику для представления исключений из общего правила.
Мы сделаем это, допустив правило, что информация, содержащаяся в подклассе, может Здесь придется попросить читателя вернуться к рис. 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 являются теперь зарезервированными и не должны встречаться в других частях программы (разве что с целью вызова процедур), даже если эти части не имеют ничего общего с подпрограммами, обслуживающими стек.
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 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 в году сделал многое для популяризации концепций, разработанных Кеем и его командой из группы 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 является объектно-ориентированным.
Роджер Кинг аргументированно настаивал, что его кот является объектноориентированным. Кроме прочих своих достоинств, кот демонстрирует характерное поведение, реагирует на сообщения, наделен унаследованными реакциями и управляет своим вполне независимым внутренним состоянием.
Многие авторы пытались дать строгое определение тех свойств языка программирования, которыми он должен обладать, чтобы называться объектно-ориентированным, — см., к примеру, анализ, проведенный Джозефиной Микалеф [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 сгенерировать список продуктов на указанный период.