Глава     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-M
    
    		FOUND=.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  G  G  A  C  C	
    
    G  G  A  C  C  C	

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

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

    Проверка этого условия оказывается тривиальной задачей. Причина, по которой APL-программа оказалась быстрее, не имела ничего общего со скоростью работы APL по сравнению с Fortran. Главным было то, что программа на Fortran использовала алгоритм со сложностью O(M*N2), в то время как алгоритм сортировки 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, и, в частности, для Фло.

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

    [INSERT HERE]

    дуумов с различными линиями наследования. Классы представляются в виде иерархической древовидной структуры, в которой более абстрактные классы (такие, как Material Object или Animal) располагаются в корне дерева, а более специализированные классы и в конечном итоге индивидуумы располагаются на его конце, в ветвях. Рисунок 1.2 показывает такую иерархию классов для Фло. Эта же самая иерархия включает в себя мою жену Бет, собаку Флеш, Фила — утконоса, живущего в зоопарке, а также цветы, которые я послал своей бабушке.


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

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

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

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

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

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

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

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

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

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

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

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

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

    1. Все является объектом.
    2. Вычисления осуществляются путем взаимодействия (обмена данными) между объектами, при котором один объект требует, чтобы другой объект выполнил некое действие. Объекты взаимодействуют, посылая и получая сообщения. Сообщение — это запрос на выполнение действия, дополненный набором аргументов, которые могут понадобиться при выполнении действия.
    3. Каждый объект имеет независимую память, которая состоит из других объектов.
    4. Каждый объект является представителем класса, который выражает общие свойства объектов (таких, как целые числа или списки).
    5. В классе задается поведение (функциональность) объекта. Тем самым все объекты, которые являются экземплярами одного класса, могут выполнять одни и те же действия.
    6. Классы организованы в единую древовидную структуру с общим корнем, называемую иерархией наследования. Память и поведение, связанное с экземплярами определенного класса, автоматически доступны любому классу, расположенному ниже в иерархическом дереве.

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

    Разделы

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

    Хотя антропоморфные описания, подобные тем, что цитировались выше в тексте Ингалса, могут шокировать людей, фактически они являются отражением огромной выразительной силы метафор. Журналисты используют метафоры каждый день, подобно тому, как это сделано в нижеследующем фрагменте из 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] не без юмора заметил, что большинство аргументов сводится к следующему силлогизму:

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

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

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

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

    Упражнения

    Разделы

    1. В объектно-ориентированной иерархии наследования каждый следующий уровень является более специализированной формой предыдущего. Приведите пример иерархии из повседневной жизни с этим свойством. Некоторые из иерархий, обнаруживаемые в реальной жизни, не являются иерархиями наследования. Укажите пример иерархии без свойства наследования.
    2. Посмотрите значение слова парадигма по крайней мере в трех словарях. Соотнесите эти определения с языками программирования.
    3. Возьмите задачу из реального мира (аналогичную пересылке цветов, рассмотренной ранее) и опишите ее решение в терминах агентов (объектов) и обязанностей.
    4. Если вы знакомы с двумя (или более) различными языками программирования, приведите пример, когда один язык направляет мысль программиста к определенному решению, а другой — стимулирует альтернативное решение.
    5. Если вы знакомы с двумя (или более) естественными языками, опишите ситуацию, когда один язык направляет говорящего в одном направлении, в то время как другой язык приводит к иному ходу мысли.
    6. Приведите аргументы за и против той точки зрения, что работа с компьютером в своей основе является моделированием. Возможно, вам захочется прочесть статью Алана Кея в Scientific American [Kay 1977].

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

    Сайт создан в системе uCoz