Объектно-ориентированное программирование

Общая характеристика объектно-ориентированного подхода в современном программировании. Рассмотрение концепций и понятия объектно-ориентированного подхода, а также их выражения на унифицированном языке моделирования UML и языке программирования С++.

Рубрика Программирование, компьютеры и кибернетика
Вид курс лекций
Язык русский
Дата добавления 01.10.2014
Размер файла 256,9 K

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

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

Размещено на http://www.allbest.ru/

Конспект лекций

Объектно-ориентированное программирование

Новосибирск

2001

Оглавление

Введение

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.

Пример. Рассмотрим стек, реализованный с использованием массива фиксированной длины.

int stack[100]; // не более ста элементов в стеке

int top=-1; // номер доступного элемента

void push (int el) {

if(top == 99) throw (1); // проверить на переполнение

else stack[++top] = el; // поместить элемент в стек

}

int pop ( ) {

if(top == -1) throw (0); // проверить на пустоту

else return stack[top--]; // извлечь элемент из стека

}

main ( ) {

int i = 0, k;

. . .

try{ // пробный блок

push (i);

. . .

k = pop ( );

. . .

}

catch(int error){. . .} // если error = 0, то стек пуст;

// если error = 1, то стек полон

}

2.2 Инкапсуляция

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

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

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

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

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

Интерфейс стека - это его операции pop и push, а реализация - это конкретное представление стека.

Пример. Перепишем реализацию стека, рассмотренную в предыдущем пункте, с использованием структуры.

struct Stack {

int s[100];

int top;

void push(int el);

int pop( );

};

Функции pop и push изменяют значения членов класса. Однако изменить значения элементов могут и другие функции. При этом такие изменения могут быть внесены и по ошибке. Следовательно, имеет смысл ограничить доступ к данным объектов типа Stack.

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

class Stack {

private:

int s[100];

int top;

public:

void push(int el);

int pop( );

bool isFull ( ) const;

bool isEmpty ( ) const;

};

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

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

Мы описали Stack как класс, а не как структуру. Принципиального отличия здесь нет, поскольку структура в С++ является классом, члены которого, однако, по умолчанию открыты. Члены класса, описанного ключевым словом class, по умолчанию являются закрытыми.

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

Таким образом, введение ограничения доступа к элементам класса на практике реализует понятие инкапсуляции.

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

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

2.3 Модульность

Модулем называют набор связанных процедур вместе с данными, которые они обрабатывают.

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

В языке C++ модулями являются файлы, которые компилируются отдельно один от другого и затем объединяются в один исполняемый файл при помощи редактора связей.

Пример. В качестве примера рассмотрим модульную структуру программы, использующей стек.

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

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

Итак, интерфейс стека будет помещен в файл stack.h.

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

void push(int el);

int pop ( );

Код пользователя будет находиться, например, в файле user.c:

#include "stack.h" // включить интерфейс

main (void)

{

push (1);

if (pop ( ) != 1) ...; // ???

. . .

}

Файл, содержащий реализацию модуля Stack, может называться, например, stack.c:

#include "stack.h" // включить интерфейс

int stack [100]; //реализация

int top;

void push (int el){. . .}

int pop ( ){. . .}

Тексты user.c и stack.c совместно используют информацию об интерфейсе, содержащуюся в stack.h. Во всем другом эти два файла независимы и могут быть раздельно откомпилированы. Графическое изображение упомянутых фрагментов программы представлено на рис. 2.1.

Рис. 2.1 Структура модулей программы, использующей стек

Если стек представлен в виде объекта типа Stack, введенного с использованием понятия класс, то информация о данных, агрегированных в этот новый тип, (а не только о предоставляемом им интерфейсе) также должна быть доступна при компиляции пользовательского кода на языке С++.

Предположим, что компилятор встречает объявление объекта

Stack My_stack;

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

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

В результате файл stack.h должен содержать описание структуры или класса Stack, приведенное в п. 2.2.

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

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

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

Рассмотрим приемы и правила, которые позволяют составлять модули наиболее эффективным образом:

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

- структура модуля должна быть достаточно простой для восприятия;

- реализация каждого модуля не должна зависеть от реализации других модулей;

- должны быть приняты меры для облегчения процесса внесения изменений там, где они наиболее вероятны.

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

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

- в качестве межмодульных можно использовать только те элементы, вероятность изменения которых мала;

- все структуры данных должны быть обособлены в модуле; доступ к ним будет возможен для всех процедур этого модуля и закрыт для всех других;

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

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

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

Могут сказываться и требования секретности: одна часть кода может быть несекретной, а другая - секретной, тогда последняя выполняется в виде отдельного модуля (модулей).

В результате всего сказанного сформулируем следующее определение модульности:

Модульность - это свойство системы, которая была разложена на внутренне связные, но слабо связанные между собой модули.

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

Подсистема - это агрегат, содержащий другие модули и другие подсистемы. Каждый модуль в системе должен располагаться в одной подсистеме или находиться на самом верхнем уровне.

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

2.4 Иерархичность

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

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

Иерархия - это упорядочение абстракций, расположение их по уровням.

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

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

Например, лазерный принтер является разновидностью принтеров (лазерный принтер является принтером), принтер Хьюлетт-Паккард 6L является разновидностью лазерных принтеров (принтер Хьюлетт-Паккард 6L является лазерным принтером). Понятие "принтер" обобщает свойства, присущие всем принтерам, а лазерный принтер - это просто особый тип принтера со свойствами, которые отличают его, например, от матричного или струйного принтера.

Важный элемент объектно-ориентированных систем и основной вид иерархии "является" - иерархия обобщения (наследования) (отношение родитель-потомок).

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

"Лакмусовой бумажкой" обобщения является обратная проверка. Если В не есть А, то В не стоит производить от А.

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

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

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

Пример. Одиночное наследование. Вернемся к иерархии "принтер - лазерный принтер" (лазерный принтер является разновидностью принтеров).

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

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

Множественным наследованием часто злоупотребляют. Например, сладкая вата - это частный случай сладости, но никак не ваты. Следует применять ту же "лакмусовую бумажку": если В не есть А, то ему не стоит наследовать от А.

Иерархия "имеет" вводит отношение агрегации (целое/часть). В иерархии "имеет" некоторая абстракция находится на более высоком уровне, чем любая из использовавшихся при ее реализации.

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

Пример. Компьютер имеет системный блок (системный блок является частью компьютера). Системный блок компьютера одновременно имеет (агрегирует) материнскую плату, платы оперативной памяти, центральный процессор и множество других компонент. Заметим, что от замены процессора на более мощный, от добавления нескольких плат оперативной памяти или второго жесткого диска системный блок не становится другим системным блоком. Если же мы разбираем системный блок, мы уничтожает его как объект, однако его компоненты остаются и могут быть использованы в других системных блоках. Другими словами, системный блок и его компоненты имеют свои отдельные и независимые сроки жизни.

2.5 Типизация

Типизация - это способ защититься от использования объектов одного класса (типа) вместо другого, или, по крайней мере, управлять таким использованием.

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

Рассмотрим следующий фрагмент:

typedef char* Pchar;

Pchar p1, p2;

char *p3 = p1;

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

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

Важным понятием объектно-ориентированного подхода в целом и языка С++ в частности является полиморфизм.

Полиморфизм - это способ присваивать различные значения (смыслы) одному и тому же сообщению. Смысл зависит от типа обрабатываемых данных.

Имеется несколько типов полиморфизма.

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

int i = 1;

double a, b = 4.5;

a = b + i;

В данном примере значение переменной i будет преобразовано к типу double и результат сложения также будет иметь тип double. Заметим, что значение i в памяти останется неизменным, преобразуется только временная копия i, используемая при вычислении значения выражения.

Перегрузка. Функция или оператор вызывается на основе сигнатуры. Например,

double a;

a = 1/2; // целочисленное деление, a = 0

a = 1./2.; // деление вещественных чисел, a = 0.5

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

class complex {

double re, im;

public:

. . .

complex operator+(complex);

complex operator*(complex);

};

Мы определили простую реализацию понятия комплексного числа: число представляется парой чисел с плавающей точкой двойной точности, вычисления осуществляются посредством операций + и *. Теперь, определив переменные b и c типа complex, можно записать b+c, что означает (по определению) b.operator+(c). В результате появляется возможность записывать комплексные выражения в форме, близкой к общепринятой.

Другие типы полиморфизма - включение и параметрический полиморфизм - мы рассмотрим в п. 4.3 и 4.5 соответственно.

Для осуществления явных преобразований переменных одного типа к другому типу в С++ имеются специальные операторы приведения.

Оператор static_cast используется для преобразования родственных типов и позволяет провести преобразование типа корректно, переносимо и обратимо. Например,

int i;

double a, b;

. . .

а = static_cast < double > (i);

а = static_cast < double > (static_cast < int > (b) + 1);

Оператор reinterpret_cast позволяет провести явное преобразование между несвязанными (неродственными) типами. Например,

i = reinterpret_cast < int > (&x);// системно-зависимое

Использование модификатора const приводит к тому, что значение переменной нельзя изменить. Если данное ограничение необходимо обойти, используется оператор const_cast.

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

2.6 Параллелизм

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

Процесс (поток управления) - это фундаментальная единица действия в системе. Каждая программа имеет по крайней мере один поток управления, в параллельной системе таких потоков много.

Век одних потоков недолог, а другие живут в течение всего сеанса работы системы.

Параллелизм главное внимание уделяет абстрагированию и синхронизации процессов.

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

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

Параллелизм - это свойство, отличающее активные объекты от неактивных.

2.7 Сохраняемость

Любой программный объект существует в памяти и живет во времени.

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

- промежуточные результаты вычисления выражений;

- локальные переменные в вызове процедур;

- глобальные переменные и динамически создаваемые данные;

- данные, сохраняющиеся между сеансами выполнения программы;

- данные, сохраняемые при переходе на новую версию программы;

- данные, которые вообще переживают программу.

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

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

До сих пор мы говорили о сохранении объектов во времени. В большинстве систем объектам при их создании отводится место в памяти, которое не изменяется и в котором объект находится всю свою жизнь.

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

Это касается систем, распределенных в пространстве.

В результате получим следующее определение.

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

3. Объекты

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

3.1 Состояние

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

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

...

Подобные документы

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.