ОГЛАВЛЕНИЕ
ВВЕДЕНИЕ
1. СЛОЖНОСТЬ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ
2. ОБЪЕКТНАЯ МОДЕЛЬ
2.1. Абстрагирование
2.2. Инкапсуляция
2.3. Модульность
2.4. Иерархичность
2.5. Типизация
2.6. Параллелизм
2.7. Сохраняемость
3. ОБЪЕКТЫ
3.1. Состояние
3.2. Поведение
3.3. Идентичность
3.4. Отношения между объектами
4. КЛАССЫ
4.1. Ассоциация
4.2. Агрегация
4.3. Обобщение
4.3.1. Наследственная иерархия
4.3.2. Наследование и типизация
4.3.3. Множественное наследование
4.4. Зависимость
4.5. Инстанцирование
4.6. Переменные и операции класса
4.7. Интерфейсы
4.8. Группирование классов
5. ОБЪЕКТНО-ОРИЕНТИРОВАННЫЙ АНАЛИЗ
6. ОСНОВНЫЕ КОНСТРУКЦИИ ЯЗЫКА UML
6.1. Диаграмма классов
6.2. Диаграмма объектов
6.3. Диаграммы взаимодействий
6.2. Диаграмма состояний
6.5. Диаграмма деятельности
ЛИТЕРАТУРА
ВВЕДЕНИЕ
Объектно-ориентированный подход в последнее десятилетие стал одним из наиболее интенсивно развивающихся направлений в программировании и наиболее популярным средством разработки программного обеспечения.
Начало развитию объектно-ориентированного подхода положил язык Simula 67, который был разработан в конце 60-х гг. в Норвегии. Несмотря на то, что язык намного опередил свое время, современники (программисты 60-х гг.) оказались не готовы воспринять ценности языка Simula 67, и он не выдержал конкуренции с другими языками программирования (прежде всего, с языком Fortran).
Но достоинства языка Simula 67 были замечены некоторыми программистами, и в 70-е гг. было разработано большое число экспериментальных объектно-ориентированных языков программирования. В результате исследования этих языков были разработаны современные объектно-ориентированные языки программирования: C++, Ada, Smalltalk и др.
Наиболее распространенным объектно-ориентированным языком программирования является язык C++ [1, 6, 8]. Он возник на базе соединения языков С и Simula. С++ был разработан в начале 80-х Бьерном Страуструпом, сотрудником компании AT&T. Все эти годы язык интенсивно развивался, и, наконец, в августе 1998 г. был принят международный стандарт языка С++.
Разработка новых объектно-ориентированных языков программирования продолжается и в настоящее время. Например, с 1995 г. стал широко распространяться объектно-ориентированный язык программирования Java, ориентированный на сети компьютеров и, прежде всего, на Internet. В настоящее время компанией Microsoft разрабатывается новый объектно-ориентированный язык C# (C Sharp), который во многом базируется на языке С++ и также ориентирован на разработку Internet-приложений.
Вместе с развитием объектно-ориентированного программирования стали развиваться и объектно-ориентированные методы разработки программного обеспечения, охватывающие стадии анализа и проектирования. Среди общепризнанных объектно-ориентированных подходов к анализу и проектированию следует выделить методы Г. Буча [3, 4], Д. Рамбо, А. Джекобсона, Шлеера-Меллора и Коуда-Йордона. В результате объединения усилий первых трех авторов появился на свет унифицированный язык моделирования UML [2, 5, 7, 9], который в 1997 г. был принят в качестве стандарта консорциумом Object Management Group и получил широкое распространение в сфере производства программного обеспечения.
Основные идеи объектно-ориентированного подхода опираются на следующие положения:
– программа представляет собой модель некоторого реального процесса, части реального мира; модель содержит не все признаки и свойства представляемой ею части реального мира, а только те, которые существенны для разрабатываемой программной системы;
– модель реального мира или его части может быть описана как совокупность взаимодействующих между собой объектов;
– объект описывается набором атрибутов (свойств), значения которых определяют состояние объекта, и набором операций (действий), которые может выполнять объект;
– взаимодействие между объектами осуществляется посылкой специальных сообщений от одного объекта к другому; сообщение, полученное объектом, может потребовать выполнения определенных действий, например изменения состояния объекта;
– объекты, описанные одним и тем же набором атрибутов и способные выполнять один и тот же набор операций, представляют собой класс однотипных объектов.
С точки зрения языка программирования класс объектов можно рассматривать как тип данных, а отдельные объекты – как данные этого типа. Определение программистом собственных классов объектов должно позволить описывать конкретную задачу в терминах ее предметной области (при соответствующем выборе имен типов и имен объектов, их атрибутов и выполняемых действий).
Объектно-ориентированный подход дает следующие основные преимущества:
– уменьшение сложности программного обеспечения;
– повышение его надежности;
– обеспечение возможности модификации отдельных компонент программ без изменения остальных компонент;
– обеспечение возможности повторного использования отдельных компонент программного обеспечения.
Систематическое применение объектно-ориентированного подхода позволяет разрабатывать хорошо структурированные, надежные в эксплуатации, достаточно просто модифицируемые программные системы. Этим объясняется интерес программистов к объектно-ориентированному подходу и объектно-ориентированным языкам программирования.
Целью данного курса лекций является введение в объектно-ориентированный подход к разработке программного обеспечения. В рамках курса рассмотрены концепции и понятия объектно-ориентированного подхода (на основе [4]), а также их выражение на унифицированном языке моделирования UML (на основе [5]) и языке программирования С++.
1. СЛОЖНОСТЬ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ
Объектно-ориентированный подход возник в первую очередь в ответ на растущую сложность программного обеспечения. На заре компьютерной эры возможности компьютеров были ограничены и было очень трудно написать большую программу. В 60–70-е гг. эффективность применения компьютеров резко возросла, и стало все больше создаваться прикладных программ повышенной сложности. Наибольшее распространение в это время получило структурное проектирование по методу сверху вниз. Однако через некоторое время оказалось, что структурный подход не работает, если объем программы превышает приблизительно 100 тыс. строк. Как результат – выход проектов за рамки установленных сроков и бюджетов и, более того, их несоответствие начальным требованиям. Для решения этих проблем и стали применять объектно-ориентированный подход.
Справедливости ради отметим, что объектно-ориентированный подход является достаточно универсальным инструментом. Он может применяться и для разработки программ малой и средней сложности. Однако, именно для сложных систем использование объектно-ориентированного подхода является критичным. Таким образом, основной областью использования объектно-ориентированного подхода и основным объектом нашего интереса являются сложные промышленные программные продукты.
Подобные системы могут применяться для решения самых разных задач. В качестве примеров можно привести:
– системы с обратной связью (интеллектуальные, самообучающиеся системы), которые активно взаимодействуют или управляются событиями физического мира и для которых ресурсы времени и памяти ограничены;
– задачи поддержания целостности информации объемом в сотни тысяч записей при параллельном доступе к ней с обновлениями и запросами;
– системы управления и контроля над реальными процессами (например, диспетчеризация воздушного и железнодорожного транспорта).
Системы подобного типа обычно имеют большое время жизни, и большое количество пользователей оказывается в зависимости от их нормального функционирования. Глобальные системы национального или даже мирового масштаба являются яркими примерами таких систем (системы управления ядерными объектами, Интернет).
Сложность является существенной чертой промышленной программы: один разработчик практически не в состоянии охватить все аспекты такой системы. Фактически сложность промышленных программ превышает возможности интеллекта одного человека.
Сложность, присущая программному обеспечению, определяется следующими основными причинами:
– сложностью реального мира;
– сложностью управления процессом разработки;
– гибкостью программного обеспечения;
– сложностью описания поведения систем.
Сложность реального мира
Проблемы, которые мы пытаемся решить с помощью разрабатываемого программного обеспечения, часто неизбежно содержат сложные элементы, к которым предъявляется множество различных и нередко противоположных требований.
Но даже в простых проблемах сложность может возникнуть из-за языковой и понятийной "нестыковки" между заказчиком системы и ее разработчиком: пользователи обычно с трудом могут внятно объяснить разработчикам, что на самом деле нужно сделать. Часто пользователь лишь смутно представляет, что ему нужно от будущей программной системы. С другой стороны, разработчик, являясь экспертом лишь в своей области знаний, недостаточно квалифицирован в предметной области.
Дополнительные сложности возникают в результате изменения требований к программной системе уже в процессе разработки. В основном требования корректируются из-за того, что само осуществление программного проекта часто изменяет проблему. Использование системы, после того как она разработана и установлена, заставляет пользователей лучше понять и отчетливей сформулировать то, что им действительно нужно. В то же время этот процесс повышает квалификацию разработчиков в предметной области и позволяет им задавать более осмысленные вопросы, которые проясняют темные места в проектируемой системе.
Сложность управления процессом разработки
Большое число требований к системе неизбежно приводит либо к созданию нового программного продукта значительных размеров, либо к модификации существующего, что также не делает его проще. Сегодня обычными стали системы размером в десятки тысяч и даже миллионы строк на языках высокого уровня. Ни один человек не в состоянии понять и разработать полностью такую систему. Поэтому возникает необходимость разбиения системы на подсистемы и модули и коллективная разработка систем.
В идеале для успеха разработки команда разработчиков должна быть как можно меньше. Но какой бы она ни была, всегда возникнут трудности, связанные с координацией работ над проектом, и проблема взаимопонимания требований и спецификаций системы.
Гибкость программного обеспечения
Программирование обладает максимальной гибкостью среди технических наук. Программист, как и писатель, работает со словом, и всеми базовыми элементами, необходимыми для создания программ, он может обеспечить себя сам, зачастую пренебрегая уже существующими разработками. Такая гибкость – чрезвычайно привлекательное, но опасное качество: пользователь, осознав эту возможность, постоянно изменяет свои требования; разработчик увлекается украшательством своей системы во вред основному ее назначению. Поэтому программные разработки остаются очень кропотливым и "бесконечным" делом, а программные системы потенциально незавершенными.
Сложность описания поведения системы
Сложные программные системы содержат сотни и тысячи переменных, текущие значения которых в каждый момент времени описывают состояние программы. Кроме того, они имеют большое количество точек ветвления, которые определяют множество зависящих от ситуации путей решения задачи. Все это разработчик должен продумать, зафиксировать в программах, протестировать и отладить.
Любая сложная система, в том числе и сложная программная система, обладает следующими общими признаками:
1. Сложные системы часто являются иерархическими и состоят из взаимозависимых подсистем, которые, в свою очередь, также могут быть разделены на подсистемы и т.д.
Сложная система состоит не просто из отдельных компонент, между ними имеются определенные иерархические отношения.
Например, большинство персональных компьютеров состоит из одних и тех же основных элементов: системного блока, монитора, клавиатуры и манипулятора "мышь". Мы можем взять любую из этих частей и разложить ее, в свою очередь, на составляющие. Системный блок, например, содержит материнскую плату, платы оперативной памяти, центральный процессор, жесткий диск и т.д.
Продолжая, мы можем разложить на составляющие центральный процессор. Он состоит из регистров и схем управления, которые сами состоят из еще более простых деталей: диодов, транзисторов и т.д. Возникает вопрос, что же считать простейшим элементом системы? Ответ дает второй признак.
2. Выбор того, какие компоненты в данной системе считаются элементарными, относительно произволен и в большой степени оставляется на усмотрение исследователя.
Низший уровень для одного наблюдателя может оказаться достаточно высоким для другого. Если пользователю достаточно выделить системный блок, монитор и клавиатуру, то для разработчика компьютера этого явно недостаточно.
3. Внутрикомпонентная связь обычно сильнее, чем связь между компонентами.
Это обстоятельство позволяет отделять высокочастотные взаимодействия внутри компонентов от низкочастотных взаимодействий между компонентами и дает возможность относительно изолированно изучать каждую компоненту.
4. Иерархические системы обычно состоят из немногих типов подсистем, по-разному скомбинированных и организованных.
Иными словами, разные сложные системы содержат одинаковые структурные части. Эти части, в свою очередь, могут использовать общие более мелкие компоненты. Например, и у растений, и у животных имеются крупные подсистемы типа сосудистых систем, и клетки как более мелкие компоненты.
5. Любая работающая сложная система является результатом развития работавшей более простой системы.
В качестве примера назовем теорию эволюции живой природы.
Сложная система, спроектированная с нуля, вряд ли заработает. Следует начать с работающей простой системы.
В процессе развития системы объекты, первоначально рассматривавшиеся как сложные, становятся элементарными, и из них строятся более сложные системы.
2. ОБЪЕКТНАЯ МОДЕЛЬ
Объектно-ориентированный подход основывается на совокупности ряда принципов, называемой объектной моделью. Главными принципами являются
– абстрагирование;
– инкапсуляция;
– модульность;
– иерархичность.
Эти принципы являются главными в том смысле, что без них модель не будет объектно-ориентированной. Кроме главных, назовем еще три дополнительных принципа:
– типизация;
– параллелизм;
– сохраняемость.
Называя их дополнительными, мы имеем в виду, что они полезны в объектной модели, но не обязательны.
2.1. Абстрагирование
Люди развили чрезвычайно эффективную технологию преодоления сложности. Мы абстрагируемся от нее. Если мы не в состоянии полностью воссоздать сложный объект, то приходится игнорировать не слишком важные детали. В результате мы имеем дело с обобщенной, идеализированной моделью объекта.
Например, изучая процесс фотосинтеза у растений, мы концентрируем внимание на химических реакциях в определенных клетках листа и не обращаем внимание на остальные части – черенки, жилки и т.д.
Абстракция выделяет существенные характеристики некоторого объекта, отличающие его от всех других видов объектов, и, таким образом, четко определяет его концептуальные границы с точки зрения наблюдателя.
Абстрагирование концентрирует внимание на внешних особенностях объекта и позволяет отделить самые существенные особенности поведения от несущественных. Такое разделение смысла и реализации называют барьером абстракции. Установление того или иного барьера абстракции порождает множество различных абстракций для одного и того же предмета или явления реального мира. Абстрагируясь в большей или меньшей степени от различных аспектов проявления реальности, мы находимся на разных уровнях абстракции.
Для примера рассмотрим системный блок компьютера. Пользователю, использующему компьютер для набора текста, не важно, из каких частей состоит этот блок. Для него это – коробка белого цвета с кнопками и емкостью для дискеты. Он абстрагируется от таких понятий, как "процессор" или "оперативная память". С другой стороны, у программиста, пишущего программы в машинных кодах, барьер абстракции лежит намного ниже. Ему необходимо знать устройство процессора и команды, понимаемые им.
Является полезным еще один дополнительный принцип, называемый принципом наименьшего удивления. Согласно ему абстракция должна охватывать все поведение объекта, но не больше и не меньше, и не привносить сюрпризов или побочных эффектов, лежащих вне ее сферы применимости.
Например, нам необходимо использовать структуру данных, аналогичную стеку (с доступом, осуществляемым по правилу "первым вошел, последним вышел"), однако требуется проверять наличие в "стеке" некоторого элемента. Если мы назовем эту структуру данных стеком и предложим постороннему программисту, он очень удивится, заметив "лишнюю" операцию.
Все абстракции обладают как статическими, так и динамическими свойствами. Например, файл как объект требует определенного объема памяти на конкретном устройстве, имеет имя и содержимое. Эти атрибуты являются статическими свойствами. Конкретные же значения каждого из перечисленных свойств динамичны и изменяются в процессе использования объекта: файл можно увеличить или уменьшить, изменить его имя и содержимое.
Будем называть клиентом любой объект, использующий ресурсы другого объекта, называемого сервером. Мы будем характеризовать поведение объекта услугами, которые он оказывает другим объектам, и операциями, которые он выполняет над другими объектами. Этот подход концентрирует внимание на внешних проявлениях объекта и реализует так называемую контрактную модель программирования. Эта модель заключается в следующем: внешнее проявление объекта рассматривается с точки зрения его контракта с другими объектами, в соответствии с этим должно быть выполнено и его внутреннее устройство (часто – во взаимодействии с другими объектами). Контракт фиксирует все обязательства, которые объект-сервер имеет перед объектом-клиентом. Другими словами, этот контракт определяет ответственность объекта – то поведение, за которое он отвечает.
Каждая операция, предусмотренная контрактом, однозначно определяется ее сигнатурой – списком типов формальных параметров и типом возвращаемого значения. Полный набор операций, которые клиент может осуществлять над другим объектом, вместе с правильным порядком, в котором эти операции вызываются, называется протоколом. Протокол отражает все возможные способы, которыми объект может действовать или подвергаться воздействию. Тем самым протокол полностью определяет внешнее поведение абстракции.
Пример. В тепличном хозяйстве, использующем гидропонику, растения выращиваются на питательном растворе без песка, гравия и другой почвы. Управление режимом работы парниковой установки – очень ответственное дело. Оно зависит как от вида выращиваемых культур, так и от стадии выращивания. Нужно контролировать целый ряд факторов: температуру, влажность, освещение, кислотность и концентрацию питательных веществ. В больших хозяйствах для решения этой задачи часто используют автоматические системы, которые контролируют и регулируют указанные факторы. Цель автоматизации состоит здесь в том, чтобы при минимальном вмешательстве человека добиться соблюдения режима выращивания.
Одна из ключевых абстракций в данной задаче – датчик. Известно несколько разновидностей датчиков. Все, что влияет на урожай, должно быть измерено. Таким образом, нужны датчики температуры воды, температуры воздуха, влажности, кислотности, освещения и концентрации питательных веществ.
С внешней точки зрения датчик температуры – это объект, который способен измерять температуру там, где он расположен. Температура – это числовой параметр, имеющий ограниченный диапазон значений и определенную точность и означающий число градусов по Цельсию.
Местоположение датчика – это некоторое однозначно определенное место в теплице, температуру в котором необходимо знать. Таких мест, вероятно, немного. Для датчика температуры при этом существенно не само местоположение, а только то, что данный датчик расположен именно в данном месте.
Рассмотрим элементы реализации нашей абстракции на языке С++.
typedef float Temperature; // Температура по Цельсию
typedef unsigned int Location; // Число, однозначно определяющее
// положение датчика
Здесь два оператора определения типов Temperature и Location вводят удобные псевдонимы для простейших типов, и это позволяет нам выражать свои абстракции на языке предметной области. Temperature – это числовой тип данных в формате с плавающей точкой для записи температур. Значения типа Location обозначают места, где могут располагаться температурные датчики.
Рассмотрим обязанности датчика температуры. Датчик должен знать значение температуры в своем местонахождении и сообщать ее по запросу. Клиент по отношению к датчику может выполнить такие действия: калибровать датчик и получать от него значение текущей температуры. Таким образом, объект "Датчик температуры" имеет две операции: "Калибровать" и "Текущая температура".
struct TemperatureSensor {
Temperature curTemperature; // текущая температура в
// местонахождении датчика
Location loc; // местонахождение датчика
void calibrate (Temperature actualTemperature); // калибровать
Temperature currentTemperature ( ); // текущая температура
};
Данным описанием вводится новый тип TemperatureSensor. Важным здесь является то, что, во-первых, данные и функции, изменяющие их, объединены вместе в одном описании и, во-вторых, мы не работаем непосредственно с данными, а изменяем их посредством соответствующих функций.
Объекты данного типа вводятся так же, как и переменные стандартных типов:
TemperatureSensor TSensors[100]; // массив из ста объектов типа
// TemperatureSensor
Функции, объявленные внутри описания, называются функциями-членами. Их можно вызывать только для переменной соответствующего типа. Например, калибровать датчик можно так:
TSensors[3] . calibrate (0.); // калибруется датчик номер 3
Поскольку имя объекта, для которого вызывается функция-член, неявно ей передается, в списках аргументов функций отсутствует аргумент типа TemperatureSensor, задающий конкретный датчик, над которым производятся действия. К этому объекту внутри функции можно явно обратиться по указателю this. Например, в теле функции calibrate можно написать один из двух эквивалентных операторов
curTemperature = actualTemperature;
this -> curTemperature = actualTemperature;
Центральной идеей абстракции является понятие инварианта. Инвариант – это некоторое логическое условие, значение которого (истина или ложь) должно сохраняться. Для каждой операции объекта можно задать предусловия (т.е. инварианты, предполагаемые операцией) и постусловия (т.е. инварианты, которым удовлетворяет операция).
Рассмотрим инварианты, связанные с операцией currentTemperature. Предусловие включает предположение, что датчик установлен в правильном месте в теплице, а постусловие – что датчик возвращает значение температуры в градусах Цельсия.
Изменение инварианта нарушает контракт, связанный с абстракцией. Если нарушено предусловие, то клиент не соблюдает свои обязательства и сервер не может выполнить задачу правильно. Если нарушено постусловие, то свои обязательства нарушил сервер, и клиент не может ему больше доверять.
Для проверки условий язык С++ предоставляет специальные средства в библиотеке assert.h.
В случае нарушения какого-либо условия возбуждается исключительная ситуация. Объекты могут возбуждать исключения, чтобы запретить дальнейшее выполнение операции и предупредить о проблеме другие объекты, которые в свою очередь могут принять на себя перехват исключения и справиться с проблемой.
С++ имеет специальный механизм обработки исключений, чувствительный к контексту. Контекстом для возбуждения исключения является блок try (пробный блок). Если при выполнении операторов, находящихся внутри блока try, происходит исключительная ситуация, то управление передается обработчикам исключений, которые задаются ключевым словом catch и находятся ниже блока try. Синтаксически обработчик catch выглядит подобно описанию функции с одним аргументом без указания типа возвращаемого значения. Для одного блока try может быть задано несколько обработчиков, отличающихся типом аргумента.
Исключение возбуждается посредством указания ключевого слова throw с необязательным аргументом-выражением. Исключение будет обработано посредством вызова того обработчика catch, тип параметра которого будет соответствовать типу аргумента throw. При наличии вложенных блоков try (например, из-за вложенности вызовов функций) будет использован обработчик самого глубокого блока. Если обработчика, соответствующего типу аргумента throw, на данном уровне не будет найдено, будет осуществлен выход из текущей функции и поиск в блоке try с меньшей глубиной вложенности и т.д. После обработки исключения управление передается на оператор, следующий за описаниями обработчиков catch.