Глава     3

Классы и методы


Разделы

Содержание

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

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

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

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

Разделы

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

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

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

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

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

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

Разделы

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

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

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

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

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

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

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

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

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

Разделы

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


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

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

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

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

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


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

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

Разделы

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

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

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

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

Разделы

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

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

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

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

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

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

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

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

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

interface

type

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

  Card = object

      (* поля данных *)

      suitValue	: suits;
      rankValue	: integer;
      faceUp 	: boolean;

  (* инициализация *)

  procedure setRankAndSuit (c : integer; s : suits);

  (* рабочие функции *)

  function  color  : colors;
  procedure draw (win : windows; x, y : integer);
  function  faceUp : boolean;
  procedure flip;
  function  rank   : integer;
  function  suit   : suits;

end;

implementation

    ...

end.

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

const

  CardWidth  = 65;
  CardHeight = 75;

  function Card.color : colors;

  begin

    case suit of

    Diamond: color:= Red;
    Heart:   color:= Red;
    Spade:   color:= Black;
    Club:    color:= Black;

    end;

  end;

...

end.

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

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

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

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

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

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

       Object subclass: #Card
       instanceVariableNames: 'suit rank'
       classVariableNames: ''
       poolDictionaries: ''
       category: 'Graphics-Primitives'

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

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

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

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

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

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

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

setSuit: s rank: r

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

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

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

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

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

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

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

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

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

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

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

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

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

# import <objc/Object.h>

/*  определить символьные константы для мастей */
# define     Heart			0
# define     Club     	1
# define     Diamond  	2
# define     Spade    	3

/* определить символьные константы для цветов */
# define     Red      	0
# define     Black    	1

/* интерфейс класса Card */
@ interface Card : Object
{
        int  suit;
        int  rank;
        int  faceup;
}

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

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

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

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

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

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

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

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

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

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

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

# import "card.h"
# define     cardWidth   68
# define     cardHeight  75

@ implementation Card
- (int)     color
{
    return suit % 2;
}

- (int)     rank
{
    return rank;
}

- (void)    suit: (int) s rank: (int) c
{
    suit = s;
    rank = c;
    faceup = 0;
}
    // ... кое-что опущено
@ end

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

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

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

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

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

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

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

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

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

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

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

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

Листинг 3.6.
Описание класса сard на языке C++
enum suits {diamond, club, heart, spade};
enum colors {red, black};

//абстракция игральной карты
//используется в пасьянсе
//язык программирования: C++
//автор: Тимоти Бадд, 1995

class card
{
public:

    // конструктор
    card(suits, int);

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

    // выполняемые действия
    void    draw(window &);
    void    halfdraw(window &, int x, int y);
    void    flip();

private:

    bool    faceup;
    int     r;          // ранг
    suits   s;          // масть
};

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

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

//
//  файл реализации
//  для абстракции игральной карты
//
# include "card.h"

card::card (suits sv, int rv)
{
    s = sv;         // инициализировать масть
    r = rv;         // инициализировать ранг
    faceup = true;  // начальное положение — картинкой вверх
}

int card::rank()
{
    return r;
}

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

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

inline int Card::rank()
{
   return r;
}

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

Листинг 3.7.
Описание класса сard с inline-методами, язык C++
//абстракция игральной карты
//язык программирования: C++
//автор: Тимоти Бадд, 1995

class card
{
public:

    // конструкторы
    card(suits, int);
    card();
    card(const card & c);

    // доступ к атрибутам карты
    int     rank(){return r;}
    suits   suit(){return s;}
    colors  color();
    bool    faceUp(){return faceup;}

    //выполняемые действия
    void    draw (window & w, int x, int y);
    void    halfdraw (window & w, int x, int y);
    void    flip(){faceup = ! faceup;}

private:

    bool    faceup;
    int     r;          // ранг
    suits   s;          // масть
};

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

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

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

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

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

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

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

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

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

Листинг 3.8.
Стандартное описание класса на языке Java
class Card
{
    // статические значения цветов и мастей
    final public int red        = 0;
    final public int black      = 1;
    final public int spade      = 0;
    final public int heart      = 1;
    final public int diamond    = 2;
    final public int club       = 3;

    // поля данных
    private boolean faceup;
    private int     r;
    private int     s;

    // конструктор
    Card(int sv, int rv)
     { s = sv; r = rv; faceup = false; }

    // доступ к атрибутам карты
    public int rank(){ return r; }
    public int suit(){ return s; }
    public int color()
     {
      if ( suit() == heart ЅЅ suit() == diamond )
       return red;

      return black; 
     }

    public boolean faceUp()
     { return faceup; }

    // выполняемые действия
    public void draw (Graphics g, int x, int y)
     {
      /* ... пропущено ... */
     }

    public void flip ()
     { faceup = ! faceup; }
};

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

Упражнения

Разделы

  1. Предположим, вам требуется программа на традиционном (не объектно-ориентированном) языке программирования вроде Паскаля или C. Как бы вы смоделировали классы и методы?
  2. В языках Smalltalk и Objective-C методы, имеющие несколько аргументов, описываются с использованием ключевых слов, отделяющих каждый аргумент. В языке C++ список аргументов идет сразу за именем метода. Опишите преимущества и недостатки, свойственные каждому подходу, — в частности, объясните влияние на читаемость и степень понимания текста программы.
  3. Цифровой счетчик — это переменная с ограниченным диапазоном, которая сбрасывается, когда ее целочисленное значение достигает определенного максимума. Примеры: цифровые часы и счетчик километража. Опишите класс для такого счетчика. Обеспечьте возможность установления максимальных и минимальных значений, увеличения значений счетчика на единицу, возвращения текущих значений.
  4. Определите класс для комплексных чисел. Напишите методы для сложения, вычитания и умножения комплексных чисел.
  5. Определите класс для дробей — рациональных чисел, являющихся отношением двух целых чисел. Напишите методы для сложения, вычитания, умножения и деления дробей. Как вы приводите дроби к наименьшему знаменателю?
  6. Рассмотрим следующие две комбинации класса и функции с использованием языка C++. Объясните разницу в применении функции addi с точки зрения пользователя.

    class example1
    {
    public:
    
        int i;
    };
    
    int addi(example1 &x, int j)
    {
        x.i = x.i + j;
        return x.i;
    }
    
    class example2
    {
    public:
            int i;
    
            int addi(int j)
             { i = i+j; return i; }
    };


Классы и методы

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