ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

2. ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

2.1. Основные теоретические положения

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

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

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

2.2. Объектная декомпозиция

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

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

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

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

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

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

Пример 9.1. Разработать программу исследования элементарных функций, которая для функций y=sin x, y=cos x, y=tg x, у=ln х, у=ех выполняет следующие действия:

строит таблицу значений функции на заданном отрезке с заданным
шагом;

определяет корни функции на заданном отрезке;

определяет максимум и минимум функции на заданном отрезке.

В основе объектной декомпозиции также лежит граф состояний интерфейса (см. рис. 5.6 - 5.7). Будем считать, что каждое состояние интерфейса - это состояние некоторого функционального элемента системы, т. е. объекта. Состояний интерфейса пять, соответственно получаем пять объектов. Назовем эти объекты следующим образом: Главное меню, Меню операций, Табулятор, Определитель корней, Определитель экстремумов. Эти объекты передают управление друг другу, генерируя сообщение Активизировать. Результат объектной декомпозиции изображают в виде диаграммы объектов (рис. 9.1).

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

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

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

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

2.3. Классы и объекты-переменные

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

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

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


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

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

Согласно общим правилам языка программирования объект-переменная должен быть:

создан - для него должна быть выделена память;

инициализирован - полям объекта должны быть присвоены значения;

уничтожен - память, выделенная под объект, должна быть освобождена.

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

2.4. Методы построения классов

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

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

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

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

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

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

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

В Borland Pascal реализовано только простое наследование, при котором класс может иметь только одного родителя. В теории программирования определено также множественное наследование, предполагающее наличие у класса двух и более родителей. Такой вариант наследования реализован, например, в C++.

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

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

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

На диаграмме классов наполнение будем изображать аналогично композиции

, но соединяющую линию будем рисовать пунктиром (рис. 9.5).

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

Переопределение методов - частный случай реализации полиморфизма в программировании.

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

Различают:

• чистый полиморфизм - возможность различной интерпретации кода функции в зависимости от типа аргументов; используется в языках высокого уровня абстракции, например, в языке LISP или SMALLTALK;

• перегрузку (полиморфные имена) функций — возможность определения нескольких функций с одним именем - одно и то же имя функции может многократно использоваться в разных местах программы; выбор нужной функции может определяться типами аргументов, областью видимости (внутри модуля, файла, класса и т.д.); если выбор определяется типом аргументов, то перегрузка называется параметрической; например, язык C++ позволяет разработчику выполнять параметрическую перегрузку функций вне классов, а в Borland Pascal возможно определение функций с одинаковыми именами в различных модулях или в модуле и основной программе;

переопределение методов - в ООП возможность различных определений методов в классе-потомке и классе-родителе; конкретный метод определяется классом объекта, для которого он вызывается;

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

При переопределении методов различают простой и сложный полиморфизм.

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

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

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

При этом возможны следующие варианты.

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

Рассмотрим два примера.

Пример 9.2. Разработать класс для реализации объекта «Текст», который должен:

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

определять количество слов в тексте;

дабавлять слова после слов с указанными номерами;

удалять заданные слова из текста;

менять местами слова с заданными номерами;

заменять заданное слово на другое заданное во всем тексте;

переопределять атрибуты слова с заданным номером.

Итак, реализуемый объект должен оперировать с некоторыми внутренними объектами «Словами», для которых можно определить собственное состояние и поведение. Естественно создать специальный класс TWord для реализации «Слов». Класс TText для реализации «Текста» может быть построен как с использованием композиции, так и с использованием наполнения.

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

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

Пример 9.3. Разработать классы для реализации объектов Табулятор, Определитель корней и Определитель экстремумов из примера 9.1.

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

Любой объект, получив управление, должен ввести диапазон изменения аргумента [а, Ь], решить подзадачу, вывести результаты, а затем вернуть управление меню операций. Общее поведение и поля объектов опишем в классе TMetod. Основной метод этого класса Run должен реализовывать общее поведение и обеспечивать возможность изменения элементов этого поведения (решения конкретных подзадач) для объектов классов, которые будут от него наследоваться. Решение конкретной подзадачи реализуем как внутренний метод Task, вызываемый из метода Run. Этот внутренний метод для класса TMetod определять не будем (абстрактный метод).

Классы для реализации разрабатываемых объектов наследуем от TMetod, переопределяя метод Task и добавляя недостающие поля (рис. 9.7).

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

2.5. Этапы реализации объектно-ориентированного подхода

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

Рассмотрим эти этапы.

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

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

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

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

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

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

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

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

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

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

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ