5. Объектно-ориентированное программирование 5.1. Проблемы проектирования сложных программных систем. 5.2. Объектная модель и ее эволюция. 5.3. Реализация.

Презентация:



Advertisements
Похожие презентации
Основы информатики Классы Заикин Олег Сергеевич zaikin.all24.org
Advertisements

Методология объектно- ориентированного программирования.
OOП Инна Исаева. Подпрограмма – это большая программа, разделённая на меньшие части. В программе одна из подпрограмм является главной. Её задача состоит.
Полиморфизм. Полиморфизм – это свойство системы использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта.
Полиморфизм Полиморфизм (polymorphism) - последний из трех "китов", на которых держится объектно-ориентированное программирование Слово это можно перевести.
Практическое занятие 6. Функции. Большинство языков программирования используют понятия функции и процедуры. C++ формально не поддерживает понятие процедуры,
Должны существовать простые объяснения природных процессов, так как природа не действует из каприза или по произволу. А. Эйнштейн Сложность разработки.
Языки и методы программирования Преподаватель – доцент каф. ИТиМПИ Кузнецова Е.М. Лекция 7.
Алгоритмический подход – главное алгоритм решения задачи ( в основном, используется для вычислительных задач ); Структурное программирование – декомпозиция,
Основные виды ресурсов и возможности их разделения.
Лекция 5 Способы конструирования программ. Основы доказательства правильности.
В. Дихтяр ИНФОРМАЦИОННЫЕ ТЕХНОЛОГИИ (для бакалавров) Российский университет дружбы народов Институт гостиничного бизнеса и туризма Раздел 1.Разработка.
Распределенная обработка информации Разработано: Е.Г. Лаврушиной.
Преобразования типов В языке C/C++ имеется несколько операций преобразования типов. Они используются в случае, если переменная одного типа должна рассматриваться.
Обработка исключительных ситуаций Исключительная ситуация (исключение) – это ошибка, возникающая во время выполнения программы. Например, ошибка работы.
Объектно-ориентированное программирование С++. Лекция 6 Карпов В.Э.
Архитектура и обеспечение систем базы данных. СУБД.
Лекция 2. ИСТОЧНИКИ ОШИБОК В ПРОГРАММНЫХ СРЕДСТВАХ.
В. И. Дихтяр ИНФОРМАТИКА Российский университет дружбы народов Институт гостиничного бизнеса и туризма Раздел 3Моделирование объектов и процессов и его.
Объектно-ориентированный подход в языке C#. Класс в языке C# - ссылочный тип, определенный пользователем. Для классов ЯП C# допустимо только единичное.
Транксрипт:

5. Объектно-ориентированное программирование 5.1. Проблемы проектирования сложных программных систем Объектная модель и ее эволюция Реализация принципов абстрагирования и инкапсуляции в С Состояние и поведение объектов классов Отношения между классами.

5.1. Проблемы проектирования сложных программных систем Материал подготовлен по книге Гради Буч Объектно-ориентированный анализ и проектирование с примерами приложений на С++. Второе издание. Rational Санта-Клара, Калифорния, перевод с английского под редакцией И. Романовского и Ф. Андреева

5.1. Проблемы проектирования сложных программных систем Врач, строитель и программистка спорили о том, чья профессия древнее. Врач заметил: "В Библии сказано, что Бог сотворил Еву из ребра Адама. Такая операция может быть проведена только хирургом, поэтому я по праву могу утверждать, что моя профессия самая древняя в мире". Тут вмешался строитель и сказал: "Но еще раньше в Книге Бытия сказано, что Бог сотворил из хаоса небо и землю. Это было первое и, несомненно, наиболее выдающееся строительство. Поэтому, дорогой доктор, вы не правы. Моя профессия самая древняя в мире". Программистка при этих словах откинулась в кресле и с улыбкой произнесла: "А кто же по-вашему сотворил хаос?"

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

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

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

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

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

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

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

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

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

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

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

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

5.1. Проблемы проектирования сложных программных систем Пять признаков сложной системы «Сложные системы часто являются иерархическими и состоят из взаимозависимых подсистем, которые в свою очередь также могут быть разделены на подсистемы, и т.д., вплоть до самого низкого уровням». Тот факт, что многие сложные системы имеют почти разложимую иерархическую структуру, является главным фактором, позволяющим нам понять, описать и даже "увидеть" такие системы и их части. В самом деле, скорее всего, мы можем понять лишь те системы, которые имеют иерархическую структуру. Архитектура сложных систем складывается и из компонентов, и из иерархических отношений этих компонентов. Все системы имеют подсистемы, и все системы являются частями более крупных систем. Особенности системы обусловлены отношениями между ее частями, а не частями как таковыми.

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

5.1. Проблемы проектирования сложных программных систем «Иерархические системы обычно состоят из немногих типов подсистем, по-разному скомбинированных и организованных» Иными словам и, разные сложные системы содержат одинаковые структурные части. Эти части могут использовать общие более мелкие компоненты, такие как клетки, или более крупные структуры, типа сосудистых систем, имеющиеся и у растений, и у животных. «Любая работающая сложная система является результатом развития работавшей более простой системы... Сложная система, спроектированная "с нуля", никогда не заработает. Следует начинать с работающей простой системы» В процессе развития системы объекты, первоначально рассматривавшиеся как сложные, становятся элементарными, и из них строятся более сложные системы. Более того, невозможно сразу правильно создать элементарные объекты: с ними надо сначала повозиться, чтобы больше узнать о реальном поведении системы, и затем уже совершенствовать их.

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

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

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

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

5.2. Объектная модель и ее эволюция Объектно-ориентированное программирование (OOP)- это методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определенного класса, а классы образуют иерархию наследования. В данном определении можно выделить три части: 1) OOP использует в качестве базовых элементов объекты, а не алгоритмы; 2) каждый объект является экземпляром какого-либо определенного класса; 3) классы организованы иерархически. Программа будет объектно-ориентированной только при соблюдении всех трех указанных требований. В частности, программирование, не основанное на иерархических отношениях, не относится к OOP, а называется программированием на основе абстрактных типов данных. Объектно-ориентированное программирование основывается на объектно- ориентированном проектировании и объектно-ориентированном анализе.

5.2. Объектная модель и ее эволюция Объектно-ориентированное проектирование (OOD, object- oriented design) - это методология проектирования, соединяющая в себе процесс объектной декомпозиции и приемы представления логической и физической, а также статической и динамической моделей проектируемой системы. В данном определении содержатся две важные части: объектно- ориентированное проектирование 1) основывается на объектно- ориентированной декомпозиции; 2) использует многообразие приемов представления моделей, отражающих логическую (классы и объекты) и физическую (модули и процессы) структуру системы, а также ее статические и динамические аспекты.

5.2. Объектная модель и ее эволюция Объектно-ориентированный анализ (OOA, object-oriented analysis) - это методология, при которой требования к системе воспринимаются с точки зрения классов и объектов, выявленных в предметной области. Как соотносятся ООА, OOD и OOP? На результатах ООА формируются модели, на которых основывается OOD; OOD в свою очередь создает фундамент для окончательной реализации системы с использованием методологии OOP.

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

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

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

5.2. Объектная модель и ее эволюция

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

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

5.2. Объектная модель и ее эволюция Абстрактный датчик температуры на C++: // Температура по Фаренгейту typedef float Temperature; // Число, однозначно определяющее положение датчика typedef unsigned int Location; class TemperatureSensor { public: TemperatureSensor (Location); ~TemperatureSensor(); void calibrate(Temperature actualTemperature); Temperature currentTemperature() const; private:... };

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

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

5.2. Объектная модель и ее эволюция

Инкапсуляция - это процесс отделения друг от друга элементов объекта, определяющих его устройство и поведение; инкапсуляция служит для того, чтобы изолировать контрактные обязательства абстракции от их реализации. Пример: нагреватель, поддерживающий заданную температуру в помещении. Операции: включение, выключение и запрос состояния. class Heater { public: Heater(Location); ~Heater(); void turnOn(); void tum0ff(); Boolean isOn() const; private: }; Вот и все, что посторонним надо знать о классе Heater.

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

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

5.2. Объектная модель и ее эволюция В большинстве языков, поддерживающих принцип модульности как самостоятельную концепцию, интерфейс модуля отделен от его реализации. Таким образом, модульность и инкапсуляция ходят рука об руку. В разных языках программирования модульность поддерживается по-разному. Например, в C++ модулями являются раздельно компилируемые файлы. Для C/C++ традиционным является помещение интерфейсной части модулей в отдельные файлы с расширением.h (так называемые файлы-заголовки). Реализация, то есть текст модуля, хранится в файлах с расширением.с (.срр). Связь между файлами объявляется директивой макропроцессора #include. Такой подход строится исключительно на соглашении и не является строгим требованием самого языка. В языке Object Pascal принцип модульности формализован несколько строже. В этом языке определен особый синтаксис для интерфейсной части и реализации модуля (unit).

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

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

5.2. Объектная модель и ее эволюция На выбор разбиения на модули могут влиять и некоторые внешние обстоятельства. При коллективной разработке программ распределение работы осуществляется, как правило, по модульному принципу и правильное разделение проекта минимизирует связи между участниками. При этом более опытные программисты обычно отвечают за интерфейс модулей, а менее опытные - за реализацию. На более крупном уровне такие же соотношения справедливы для отношений между субподрядчиками. Абстракции можно распределить так, чтобы быстро установить интерфейсы модулей по соглашению между компаниями, участвующими в работе. Изменения в интерфейсе вызывают много крика и зубовного скрежета, не говоря уже об огромном расходе бумаги, - все эти факторы делают интерфейс крайне консервативным. Что касается документирования проекта, то оно строится, как правило, также по модульному принципу - модуль служит единицей описания и администрирования. Десять модулей вместо одного потребуют в десять раз больше описаний, и поэтому, к сожалению, иногда требования по документированию влияют на декомпозицию проекта (в большинстве случаев негативно). Могут сказываться и требования секретности: часть кода может быть несекретной, а другая - секретной; последняя тогда выполняется в виде отдельного модуля (модулей).

5.2. Объектная модель и ее эволюция

Иерархия Абстракция - вещь полезная, но всегда, кроме самых простых ситуаций, число абстракций в системе намного превышает наши умственные возможности. Инкапсуляция позволяет в какой-то степени устранить это препятствие, убрав из поля зрения внутреннее содержание абстракций. Модульность также упрощает задачу, объединяя логически связанные абстракции в группы. Но этого оказывается недостаточно. Значительное упрощение в понимании сложных задач достигается за счет образования из абстракций иерархической структуры. Определим иерархию следующим образом: Иерархия - это упорядочение абстракций, расположение их по уровням. Основными видами иерархических структур применительно к сложным системам являются структура классов (иерархия "is-a") и структура объектов (иерархия "part of").

5.2. Объектная модель и ее эволюция Пример иерархии: наследование. Важным элементом объектно- ориентированных систем и основным видом иерархии "is-a" является упоминавшаяся выше концепция наследования. Наследование означает такое отношение между классами (отношение родитель/потомок), когда один класс заимствует структурную или функциональную часть одного или нескольких других классов (соответственно, одиночное и множественное наследование). Иными словами, наследование создает такую иерархию абстракций, в которой подклассы наследуют строение от одного или нескольких суперклассов. Часто подкласс достраивает или переписывает компоненты вышестоящего класса. Семантически, наследование описывает отношение типа "is-a". Например, медведь есть млекопитающее, дом есть недвижимость и "быстрая сортировка" есть сортирующий алгоритм. Таким образом, наследование порождает иерархию "обобщение-специализация", в которой подкласс представляет собой специализированный частный случай своего суперкласса. "Лакмусовая бумажка" наследования - обратная проверка; так, если B не есть A, то B не стоит производить от A.

5.2. Объектная модель и ее эволюция Абстракции образуют иерархию …

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

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

5.2. Объектная модель и ее эволюция На C++ план выращивания будет выглядеть следующим образом. Сначала введем новые типы данных, приближая наши абстракции к словарю предметной области (день, час, освещение, кислотность, концентрация): // Число, обозначающее день года typedef unsigned int Day; // Число, обозначающее час дня typedef unsigned int Hour; // Булевский тип enum Lights {OFF, ON}; // Число, обозначающее показатель кислотности в диапазоне от 1 до 14 typedef float pH; // Число, обозначающее концентрацию в процентах: от 0 до 100 typedef float Concentration;

5.2. Объектная модель и ее эволюция // Структура, определяющая условия в теплице struct Condition { Temperature temperature; Lights lighting; pH acidity; Concentration concentration; };

5.2. Объектная модель и ее эволюция Наконец, вот и план выращивания: class GrowingPlan { public: GrowingPlan (char *name); virtual ~GrowingPlan(); void clear(); virtual void establish(Day, Hour, const Condition&); const char* name() const; const Condition& desiredConditions(Day, Hour) const; protected:... };

5.2. Объектная модель и ее эволюция Мы предусмотрели что каждый план имеет имя, и его можно устанавливать и запрашивать. Операция establish описана как virtual для того, чтобы подклассы могли ее переопределять. В открытую (public) часть описания вынесены конструктор и деструктор объекта (определяющие процедуры его порождения и уничтожения), две процедуры модификации (очистка всего плана clear и определение элементов плана establish) и два селектора- определителя состояния (функции name и desiredCondition). Мы опустили в описании закрытую часть класса, заменив ее многоточием, поскольку сейчас нам важны внешние ответственности, а не внутреннее представление класса.

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

5.2. Объектная модель и ее эволюция // Тип Урожай typedef unsigned int Yield; class FruitGrowingPlan : public GrowingPlan { public: FruitGrowingPlan(char* name); virtual ~FruitGrowingPlan(); virtual void establish(Day, Hour, Condition&); void scheduleHarvest(Day, Hour); Boolean isHarvested() const; unsigned daysUntilHarvest() const; Yield estimatedYield() const; protected: Boolean repHarvested; Yield repYield; … }

5.2. Объектная модель и ее эволюция План выращивания фруктов FruitGrowingPlan является разновидностью плана выращивания GrowingPlan. В него добавлены параметры repHarvested и repYield, определены четыре новые функции и переопределена функция establish. Теперь мы могли бы продолжить специализацию - например, определить на базе "фруктового" плана "яблочный" класс AppleGrowingPlan В предыдущем примере рассматривалось одиночное наследование, когда подкласс FruitGrowingPlan был создан только из одного суперкласса GrowingPlan. В ряде случаев полезно реализовать наследование от нескольких суперклассов (множественное наследование). Предположим, что нужно определить класс, представляющий разновидности растений.

5.2. Объектная модель и ее эволюция class Plant { public: Plant(char* name, char* species); virtual ~Plant(); void setDatePlanted(Day); virtual establishGrowingConditions(const Condition&); const char* name() const; const char* species() const; Day datePlantedt() const; protected: char* repName; char* repSpecies; Day repPlanted; private:... };

5.2. Объектная модель и ее эволюция Каждый экземпляр класса Plant будет содержать имя, вид и дату посадки. Кроме того, для каждого вида растений можно задавать особые оптимальные условия выращивания. Мы хотим, чтобы эта функция переопределялась подклассами, поэтому она объявлена виртуальной при реализации в C++. Три параметра объявлены как защищенные, то есть они будут доступны и классу, и подклассам (закрытая часть спецификации доступна только самому классу). Изучая предметную область, мы приходим к выводу, что различные группы культивируемых растений - цветы, фрукты и овощи, - имеют свои особые свойства, существенные для технологии их выращивания. Например, для цветов важно знать времена цветения и созревания семян. Аналогично, время сбора урожая важно для абстракций фруктов и овощей. Создадим два новых класса - цветы (Flower) и фрукты-овощи (FruitVegetable); они оба наследуют от класса Plant. Однако некоторые цветочные растения имеют плоды! Для этой абстракции придется создать третий класс, FlowerFruitVegetable, который будет наследовать от классов Flower и FruitVegetable.

5.2. Объектная модель и ее эволюция Чтобы не было избыточности, в данном случае очень пригодится множественное наследование. Сначала опишем отдельно цветы и фрукты-овощи. class FlowerMixin { public: FlowerMixin(Day timeToFlower, Day timeToSeed); virtual ~FlowerMixin(); Day timeToFlower() const; Day timeToSeed() const;protected:... };

5.2. Объектная модель и ее эволюция class FruitVegetableMixin { public: FruitVegetableMixin(Day timeToHarvest); virtual ~FruitVegetableMixin(); Day timeToHarvest() const;protected:... }; Мы намеренно описали эти два класса без наследования. Они ни от кого не наследуют и специально предназначены для того, чтобы их подмешивали (откуда и имя Mixin) к другим классам.

5.2. Объектная модель и ее эволюция Например, опишем розу: class Rose : public Plant, public FlowerMixin {…}; А вот морковь: class Carrot : public Plant, public FruiteVegetableMixin {…}; В обоих случаях классы наследуют от двух суперклассов: экземпляры подкласса Rose включают структуру и поведение как из класса Plant, так и из класса FlowerMixin. И вот теперь определим вишню, у которой товаром являются как цветы, так и плоды: class Cherry : public Plant, public FlowerMixin, public FruitVegetableMixin {…};

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

5.2. Объектная модель и ее эволюция Агрегация. Если иерархия "is а" определяет отношение "обобщение/специализация", то отношение "part of" (часть) вводит иерархию агрегации. Вот пример. class Garden { public: Garden(); virtual ~Garden(); protected: Plant* repPlants[100]; GrowingPlan repPlan; }; Это - абстракция огорода, состоящая из массива растений и плана выращивания.

5.2. Объектная модель и ее эволюция Агрегация есть во всех языках, использующих структуры или записи, состоящие из разнотипных данных. Но в объектно- ориентированном программировании она обретает новую мощь: агрегация позволяет физически сгруппировать логически связанные структуры, а наследование с легкостью копирует эти общие группы в различные абстракции. В связи с агрегацией возникает проблема владения, или принадлежности объектов. В нашем абстрактном огороде одновременно растет много растений, и от удаления или замены одного из них огород не становится другим огородом. Если мы уничтожаем огород, растения остаются (их ведь можно пересадить). Другими словами, огород и растения имеют свои отдельные и независимые сроки жизни; мы достигли этого благодаря тому, что огород содержит не сами объекты Plant, а указатели на них. Напротив, мы решили, что объект GrowingPlan внутренне связан с объектом Garden и не существует независимо. План выращивания физически содержится в каждом экземпляре огорода и погибает вместе с ним.

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

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

5.2. Объектная модель и ее эволюция Конкретный язык программирования может иметь сильный или слабый механизм типизации, и даже не иметь вообще никакого, оставаясь объектно-ориентированным. Например, в Eiffel соблюдение правил использования типов контролируется непреклонно, - операция не может быть применена к объекту, если она не зарегистрирована в его классе или суперклассе. В сильно типизированных языках нарушение согласования типов может быть обнаружено во время трансляции программы. С другой стороны, в Smalltalk типов нет: во время исполнения любое сообщение можно послать любому объекту, и если класс объекта (или его надкласс) не понимает сообщение, то генерируется сообщение об ошибке. Нарушение согласования типов может не обнаружиться во время трансляции и обычно проявляется как ошибка исполнения. C++ тяготеет к сильной типизации, но в этом языке правила типизации можно игнорировать или подавить полностью.

5.2. Объектная модель и ее эволюция Рассмотрим абстракцию различных типов емкостей, которые могут использоваться в теплице. Вероятно, в ней есть емкости для воды и для минеральных удобрений; хотя первые предназначены для жидкостей, а вторые для сыпучих веществ, они имеют достаточно много общего, чтобы устроить иерархию классов. Начнем с типов. // Число, обозначающее уровень от 0 до 100 процентов typedef float Level; Операторы typedef в C++ не вводят новых типов. В частности, и Level и Concentration - на самом деле другие названия для float, и их можно свободно смешивать в вычислениях. В этом смысле C++ имеет слабую типизацию: значения примитивных типов, таких, как int или float неразличимы в пределах данного типа. Напротив, Ada и Object Pascal предоставляют сильную типизацию для примитивных типов. В Ada можно объявить самостоятельным типом интервал значений или подмножество с ограниченной точностью.

5.2. Объектная модель и ее эволюция

Построим теперь иерархию классов для емкостей: class StorageTank { public: StorageTank(); virtual ~StorageTank(); virtual void fill(); virtual void startDraining(); virtual void stopDraining(); Boolean isEmpty() const; Level level() const; protected:... }; Класс StorageTank - это базовый класс иерархии. Он обеспечивает структуру и поведение общие для всех емкостей: возможность их наполнять или опустошать.

5.2. Объектная модель и ее эволюция class WaterTank : public StorageTank { public: WaterTank(); virtual ~WaterTank(); virtual void fill(); virtual void startDraining(); virtual void stopDraining(); void startHeating(); void stopHeating(); Temperature currentTemperature() const; protected:... };

5.2. Объектная модель и ее эволюция class NutrientTank : public StorageTank { public: NutrientTank(); virtual ~NutrientTank(); virtual void startDrainingt(); virtual void stopDraining(); protected:... }; Классы WaterTank (емкость для воды) и NutrientTank (для удобрений) наследуют свойства StorageTank, частично переопределяют их и добавляют кое-что свое: например, класс WaterTank вводит новое поведение, связанное с температурой.

5.2. Объектная модель и ее эволюция Предположим, что мы имеем следующие описания: StorageTank s1, s2; WaterTank w; NutrientTank n; При проверке типов у классов, C++ типизирован гораздо строже. Под этим понимается, что выражения, содержащие вызовы операций, проверяются на согласование типов во время компиляции. Например, следующее правильно: Level l = s1.level(); w.startDrainingt(); n.stopDraining(); Действительно, такие методы есть в классах, к которым принадлежат соответствующие переменные.

5.2. Объектная модель и ее эволюция Напротив, следующее неправильно и вызовет ошибку компиляции: s1.startHeating(); // Неправильно n.stopHeating(); // Неправильно Таких функций нет ни в самих классах, ни в их суперклассах. Но следующее n.fill(); совершенно правильно: функции fill нет в определении NutrientTank, но она есть в вышестоящем классе. Итак, сильная типизация заставляет соблюдать правила использования абстракций, поэтому она тем полезнее, чем больше проект. Однако у нее есть и теневая сторона. А именно, даже небольшие изменения в интерфейсе класса требуют перекомпиляции всех его подклассов. Кроме того, не имея параметризованных классов сложно создать собрание разнородных объектов (придется использовать класс-контейнер, содержащий указатели на void, то есть на объекты произвольного типа, что небезопасно).

5.2. Объектная модель и ее эволюция В языках с сильной типизацией гарантируется, что все выражения будут согласованы по типу. Что это значит, лучше пояснить на примере. Следующие присваивания допустимы: s1 = s2; s1 = w; Первое присваивание допустимо, поскольку переменные имеют один и тот же класс, а второе - поскольку присваивание идет снизу вверх по типам. Однако во втором случае происходит потеря информации (известная в C++ как "проблема срезки"), так как класс переменной w, WaterTank, семантически богаче, чем класс переменной s1, то есть StorageTank. Следующие присваивания неправильны: w = s1; // Неправильно w = n; // Неправильно В первом случае неправильность в том, что присваивание идет сверху вниз по иерархии, а во втором классы даже не находятся в состоянии подчиненности.

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

5.2. Объектная модель и ее эволюция Статическое и динамическое связывание. Сильная и статическая типизация - разные вещи. Строгая типизация следит за соответствием типов, а статическая типизация (иначе называемая статическим или ранним связыванием) определяет время, когда имена связываются с типами. Статическая связь означает, что типы всех переменных и выражений известны во время компиляции; динамическое связывание (называемое также поздним связыванием) означает, что типы неизвестны до момента выполнения программы. Пример: свободная, то есть не входящая в определение какого-либо класса, функция void balanceLevels(StorageTank& s1, StorageTank& s2);

5.2. Объектная модель и ее эволюция Вызов этой функции с экземплярами класса StorageTank или любых его подклассов в качестве параметров будет согласован по типам, поскольку тип каждого фактического параметра происходит в иерархии наследования от базового класса StorageTank. При реализации этой функции мы можем иметь что-нибудь вроде: if (s1.level()> s2.level()) s2.fill(); В чем особенность семантики при использовании селектора level? Он определен только в классе StorageTank, поэтому, независимо от классов объектов, обозначаемых переменными в момент выполнения, будет использована одна и та же унаследованная ими функция. Вызов этой функции статически связан при компиляции - мы точно знаем, какая операция будет запущена.

5.2. Объектная модель и ее эволюция Иное дело fill. Этот селектор определен в StorageTank и переопределен в WaterTank, поэтому его придется связывать динамически. Если при выполнении переменная s2 будет класса WaterTank, то функция будет взята из этого класса, а если - NutrientTank, то из StorageTank. В C++ есть специальный синтаксис для явного указания источника; в нашем примере вызов fill будет разрешен, соответственно, как WaterTank::fill или StorageTank::fill. Это особенность называется полиморфизмом: одно и то же имя может означать объекты разных типов, но, имея общего предка, все они имеют и общее подмножество операций, которые можно над ними выполнять. Противоположность полиморфизму называется мономорфизмом; он характерен для языков с сильной типизацией и статическим связыванием (Ada). Полиморфизм возникает там, где взаимодействуют наследование и динамическое связывание.

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

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

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

5.2. Объектная модель и ее эволюция

В объектно-ориентированном проектировании есть три подхода к параллелизму: Во-первых, параллелизм - это внутреннее свойство некоторых языков программирования. Так, для языка Ada механизм параллельных процессов реализуется как задача. В Smalltalk есть класс process, которому наследуют все активные объекты. Во-вторых, можно использовать библиотеку классов, реализующих какую-нибудь разновидность "легкого" параллелизма. Например, библиотека AT&T для C++ содержит классы Shed, Timer, Task и т.д. Ее реализация зависит от платформы, хотя интерфейс достаточно хорошо переносим. При этом подходе механизмы параллельного выполнения не встраиваются в язык (и, значит, не влияют на системы без параллельности), но в то же время практически воспринимаются как встроенные. В-третьих, можно создать иллюзию многозадачности с помощью прерываний. Для этого надо кое-что знать об аппаратуре.

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

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

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

5.2. Объектная модель и ее эволюция Языки программирования, как правило, не поддерживают понятия сохраняемости; примечательным исключением является Smalltalk, в котором есть протоколы для сохранения объектов на диске и загрузки с диска. Однако, записывать объекты в неструктурированные файлы - это все-таки наивный подход, пригодный только для небольших систем. Как правило, сохраняемость достигается применением (немногочисленных) коммерческих OODB. Другой вариант - создать объектно- ориентированную оболочку для реляционных СУБД; это лучше, в частности, для тех, кто уже вложил средства в реляционную систему. Мы рассмотрим такую ситуацию в главе 10. Сохраняемость - это не только проблема сохранения данных. В OODB имеет смысл сохранять и классы, так, чтобы программы могли правильно интерпретировать данные. Это создает большие трудности по мере увеличения объема данных, особенно, если класс объекта вдруг потребовалось изменить.

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

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ Материал подготовлен по книгам: Б. Стауструп Программирование. Принципы и практика использования С++ Подбельский Язык С++.

5.3. Реализация принципов абстрагирования и инкапсуляции в С++

Итак: 1) Компоненты класса могут быть общедоступными (public) и собственными (private). Кроме того, они могут быть защищенными (protected). Защищенные компоненты доступны методам класса, а также методам классов-потомков (private-компоненты в классах потомках недоступны). 2) Методы класса могут определены внутри класса (тогда компилятор будет стараться сделать их подставляемыми) или вне его (тогда должно быть указано полное имя функции в формате имя_класса::имя_функции).

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ 3) В классах (class) по умолчанию компоненты закрыты (private), в структурах (struct) – открыты (public). Это сделано для совместимости со структурами языка С. В остальном никакой разнице между class и struct нет! Рекомендуется настоящие классы (имеющие собственное поведение) описывать как class, структуры (не имеющие собственного поведения) – как struct.

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ В дальнейшем для демонстрации будем использовать класс class Point { int x, y; // по умолчанию private public: int SetX(int); int SetY(int); int GetX(); int GetY(); }; int Point::SetX(int xi = 0) {if((xi > 800)||(xi

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ class Point { int x, y; // по умолчанию private public: int SetX(int); int SetY(int); int GetX() {return x;} int GetY() {return y;} }; int Point::SetX(int xi = 0) {if((xi > 800)||(xi 600)||(yi

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ Мы забыли про конструктор! class Point { int x, y; // по умолчанию private public: int SetX(int); int SetY(int); int GetX(); {return x;} int GetY(); {return y;} Point(int,int); }; Point::Point(int xi = 0,int yi = 0) { if((xi > 800)||(xi 600)||(yi

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ Компонентные данные создаются в памяти для каждого экземпляра (объекта). Компонентные функции (методы) получают память только один раз (все объекты класса пользуются одной единственной копией компонентных функций. При вызове метода для объекта методу скрытно передается указатель на этот объект. Этот указатель имеет фиксированное имя this и может использоваться явным образом. Point p1(1,2); Мы пишем:Компилятор формирует: int Point::GetX() int GetX(Point *this) {return x;} {return this->x;} Мы пишем:Компилятор формирует: int x = p1.GetX();int x = GetX(&p1); При вызове компилятор анализирует тип объекта p1 и выбирает функцию – метод класса Point (раннее, статическое связывание)

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ Если все экземпляры класса должны пользоваться одной и той же копией какого-либо данного, оно должно быть объявлено статическим (static). class Point { int x, y; // по умолчанию private public: int SetX(int); int SetY(int); int GetX(); {return x;} int GetY(); {return y;} Point(int,int); static int n; //счетчик объектов класса Point (точек) };

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ Доступ к статическому компоненту возможен после его инициализации, которая должна быть выполнена в глобальной области (вне функций) после определения класса: int Point::n = 0; После инициализации статический компонент получает память и доступ к нему может быть организован как обычным способом (через объект), так и с помощью квалифицированного имени (через класс): Point p1; p1. n = 1; Point p2; Point::n = 2; Однако наше решение со счетчиком точек неудачно: все кто угодно могут изменить его значение! Лучше поместить его в закрытую часть класса, переписать конструктор и ввести деструктор.

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ class Point { int x, y; // по умолчанию private static int n; //счетчик объектов класса Point (точек) public: int SetX(int); int SetY(int); int GetX(); {return x;} int GetY(); {return y;} Point(int,int); ~Point() {n--;} }; Point::Point(int xi = 0,int yi = 0) { if((xi > 800)||(xi 600)||(yi

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ - А если теперь кто-то захочет узнать, сколько имеется точек? - Какие проблемы? Введем новый интерфейс count: class Point { int x, y; // по умолчанию private static int n; //счетчик объектов класса Point (точек) public: int SetX(int); int SetY(int); int GetX(); {return x;} int GetY(); {return y;} Point(int,int); ~Point() {n--;} int count() {return n;} };

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ Наше решение имеет недостаток: для того чтобы узнать сколько имеется точек, нужно чтобы как минимум один объект-точка существовал и был нам известен. Тогда через него мы можем узнать общее количество: Point pn; … int n = pn.count(); А что делать, если заранее неизвестно, сколько и какие объекты существуют? Создавать новый объект или вести «внешний» счет? Решение: использовать статическую компонентную функцию (статический метод).

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ class Point { int x, y; // по умолчанию private static int n; //счетчик объектов класса Point (точек) public: int SetX(int); int SetY(int); int GetX(); {return x;} int GetY(); {return y;} Point(int,int); ~Point() {n--;} static int count() {return n;} };

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ Статические методы работают только со статическими данными и могут быть вызваны как для объектов так и без привязки к объекту. То есть можно так: Point p1; int n = p1.count(); А можно и так: int n = Point::count(); Таким образом, используя статические методы, можно получать «общую» информацию об объектах класса, не имея ни одной ссылки на эти объекты (и даже не имея самих объектов).

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ Дружественные функции и дружественные классы позволяют расширить интерфейс класса за счет нарушения принципа инкапсуляции для «привилегированных лиц». Дружественные функции, не являясь членами класса, имеют доступ ко всем его компонентам, в том числе private и protected. Прототип дружественной функции должен быть помещен в определение класса, предоставляющего ей доступ к своим компонентам, со спецификацией friend (не важно, в какой части класса: public, protected или private). Таким образом только сам класс (точнее, его разработчик) выбирает себе друзей.

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ class Point { … friend ScaleLine(Point &, Point &, int n); … }; ScaleLine(Point &pb, Point &pf, int n) //масштабирование линии { pb.x*=n; pb.y*=n; pf.x*=n; pf.y*=n; } - не очень удачное решение: функция не проверяет допустимость новых значение координат. Это, конечно, можно исправить, но вообще-то проверку должен делать сам класс Point…

5.3. Реализация принципов абстрагирования и инкапсуляции в С++ Объявление дружественным целого класса эквивалентно объявлению друзьями всех его функций: class Line; class Point { … friend class Line; … }; class Line { Point pb, pf; public: … Scale(int n) { pb.x*=n; pb.y*=n; pf.x*=n; pf.y*=n;} … };

5.4. Состояние и поведение объектов классов Определение объектов классов в основном аналогично определению переменных встроенных типов. Отличие заключается в том, что при создании объекта вызывается конструктор: Point::Point(int xi = 0,int yi = 0) { if((xi > 800)||(xi 600)||(yi

5.4. Состояние и поведение объектов классов Подобно встроенным типам можно объявлять указатели на объекты и массивы объектов: Point p1(3,5); // объект x = 3, y = 5 Point *pp1 = &p1; // указатель, настраивается на объект p1 Point p2(2,1); // объект x = 2, y = 1 Point mp[10] = {p1,p2} ; // массив, первые два элемента // – копии p1 и p2 Доступ к открытой части объекта осуществляется в основном посредством двух операторов:. (точка) - доступ к компоненту по имени объекта и имени компонента -> - доступ к компоненту через указатель на объект и имя компонента

5.4. Состояние и поведение объектов классов Point p1(3,5); Point *pp1 = &p1; Point p2(2,1); Point mp[10] = {p1,p2} ; cout

5.4. Состояние и поведение объектов классов Point p1(3,5); Point *pp1 = &p1; Point p2(2,1); Point mp[10] = {p1,p2} ; Объектам присуща идентичность: к одному и тому же объекту можно обратиться через разные ссылки, однако объект все равно остается одним и тем же. Так объект p1 может быть адресован по имени и через указатель: cout SetX(8); cout

5.4. Состояние и поведение объектов классов Жизненный цикл объектов классов подобен жизненному циклу любых других объектов памяти: объекты, созданные вне функций (статические, глобальные переменные) размещаются в сегменте данных и «живут», пока работает программа; объекты, созданные «внутри» функций (автоматические, локальные переменные) размещаются в стеке и «живут», пока работает функция. Исключение составляют объекты, определенные как static; объекты, созданные оператором new (динамические) размещаются в специальном сегменте (куче) и живут до их уничтожения оператором delete или до конца работы программы. Не забываем, что создание и уничтожение объектов класса сопровождается вызовами конструктора и деструктора!

5.5. Отношения между классами Отношения между классами позже переносятся на отношения между их объектами. Основные виды отношений: ассоциация; агрегация; использование; наследование; инстанцирование.

5.5. Отношения между классами Ассоциация – смысловая связь. Для выполнения собственных задач классы связаны с другими классами. Практически связи реализуются с помощью указателей. Агрегация – это отношения целого к части. Один класс (объект) может быть частью другого. Практически это означает, что он либо физически помещен в другой, либо с помощью указателя присутствует в нем. class Line { Point pb, pf; public: Line(Point &p_b, Point &p_f): pb(bp_b), pf(p_f) { } … };

5.5. Отношения между классами Использование – один класс использует другой для решения своих задач. Использование производится с помощью интерфейсных функций. Если один класс использует функции другого класса, то такое отношение часто описывается в терминах «клиент-сервер». Сервер должен быть виден клиенту. Видимость обеспечивается следующими способами: сервер глобален по отношению к клиенту (глобальный объект с «фиксированным» именем); сервер передан клиенту в качестве параметра операции; сервер является частью клиента (ассоциация или агрегация); сервер локально порождается клиентом в ходе выполнения какой-либо операции (сервер – автоматический или динамический объект).

5.5. Отношения между классами Наследование – это отношение общее - частное. Механизм наследования предполагает построение иерархии классов. Иерархия классов позволяет определять новые классы на основе уже имеющихся. Имеющиеся классы обычно называются базовыми, а новые – производными. Производные классы «получают в наследство» от базовых данные и методы и, кроме того, могут пополняться собственными компонентами. Определение производного класса обязательно содержит список всех базовых классов, из которых он непосредственно наследует: class DerivedClass: public BaseClass1, private BaseClass2 {…}; Здесь определяется класс DerivedClass, наследующий от классов BaseClass1 и BaseClass2. Ключевые слова public и private перед именами базовых классов выступают в качестве спецификаторов доступа. Спецификатор позволяет влиять на уровень доступа в производном классе к унаследованным компонентам базового класса.

5.5. Отношения между классами Доступ в базовом классе Спецификатор доступа Доступ в производном классе classstruct publicотсутствуетprivatepublic protectedотсутствуетprivatepublic privateотсутствуетнедоступны public protectedpublicprotected privatepublicнедоступны publicprotected privateprotectedнедоступны publicprivate protectedprivate недоступны

5.5. Отношения между классами

Особенности конструкторов и деструкторов при наследовании Конструкторы базовых классов всегда вызываются до конструкторов производных классов. Если конструктору базового класса необходимо передать параметры, это делается с помощью конструктора производного класса. class ColorPoint: public Point { unsigned char Color; // 256 цветов public: ColorPoint(int xi, int yi, unsigned char col): Point(xi,yi), Color(col) {} … }; Конструктор класс ColorPoint явно вызывает конструктор класса Point. Если этого не сделать, конструктор класса Point будет вызван неявно с параметрами по умолчанию 0,0.

5.5. Отношения между классами Деструкторы базовых классов автоматически вызываются при уничтожении объекта производного класса. Деструкторы вызываются в порядке, обратном порядку вызовов конструкторов. class Base1 {…}; class Base2 {…}; class Deriv: public Base1, public Base2 {… public: ~Deriv() {операции;} }; При уничтожении объекта класса Deriv деструктор выполняется так: 1. операции; 2. ~Base2(); 3. ~Base1();

5.5. Отношения между классами Виртуальные базовые классы При множественном наследовании может возникнуть проблема повторного наследования. class X { public : int A; …}; class Y: public X {…}; class Z: public X {…}; class D: public Y, public Z {…}; D Dobj; Во многих случаях повторное наследование нежелательно, так как увеличивается объем памяти для хранения объекта производного класса (D); возникают некоторые проблемы при обращении к компонентам базового класса (X) для объекта производного класса (D). Эти проблемы решаются с помощью указания «полных имен» компонентов: Dobj.D::Y::X::A = 1; Dobj.D::Z::X::A = 2; D Z Y XX

5.5. Отношения между классами Для решения проблемы класс X при наследовании следует сделать виртуальным class X { public : int A; …}; class Y: virtual public X {…}; class Z: virtual public X {…}; class D: public Y, public Z {…}; D Dobj; В объект Dobj входит только один экземпляр класса X, что уменьшает объем памяти и упрощает обращение. Физически это реализуется с использованием указателей. Классы Y и Z не содержат данных класса X, а только указатели на «отдельный» объект X. При включении данных классов Y и Z в объект класса D в последний попадает два указателя, настроенных на единственную версию X. D Z Y X

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

5.5. Отношения между классами //Формальный пример class BaseCL { … public: … void fun1(void); // обычный метод virtual void fun2(void);// виртуальный метод }; class DerivedCL: public BaseCL { … public: … void fun1(void); // обычный метод void fun2(void); // виртуальный метод };

5.5. Отношения между классами DerivedCL DCObject; //объект производного класса BaseCL * pBC; // указатель базового класса pBC = & DCObject; // 1. Настройка указателя pBC–>fun1(); // 2. Вызов BaseCL::fun1(void) - обычный pBC–>fun2(); // 3. Вызов DerivedCL::fun2(void) – полиморфный 1. Указатель базового класса в данном случае вполне можно настроить на объект производного класса, так как наследование открытое и, следовательно, производный класс обязуется выполнять все обязанности базового. 2. При вызове обычного метода анализируется только тип указателя и, следовательно, для объекта производного класса будет вызван метод базового класса (функции BaseCL::fun1 будет передан как this указатель на объект DCObject ). 3. По указателю типа базового класса, настроенному на объект производного класса, вызывается метод производного класса. Это полиморфный вызов.

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

5.5. Отношения между классами Абстрактные классы Допустим, нам необходимо одновременно вывести на экран несколько графических фигур, причем сколько и каких именно, заранее не известно. Фигуры должны быть представлены в программе объектами различных классов. Предположим, что каждый из этих классов имеет метод draw для рисования фигуры на экране. Чтобы вывести все фигуры на экран, мы должны сосредоточить графические объекты в одном массиве и потом в цикле вызвать для каждого объекта метод draw. Однако здесь возникает вопрос: элементы какого типа будет содержать наш массив, если графические объекты имеют различные типы? Подобные проблемы легко разрешаются с помощью использования так называемых абстрактных классов.

5.5. Отношения между классами Абстрактные классы вводятся для представления наиболее общих понятий, которые в дальнейшем предполагается конкретизировать. Эти понятия невозможно использовать непосредственно, но на их основе можно как на базе построить частные производные классы, пригодные для описания конкретных объектов. С точки зрения языка программирования, абстрактным классом называется класс, в котором есть хотя бы одна чистая (пустая) виртуальная функция, имеющая определение вида: virtual тип имя_функции(список параметров) = 0; Такая функция «ничего не делает» и недоступна для вызовов. Ее назначение – служить основой для подменяющих ее функций в производных классах.

5.5. Отношения между классами /* Абстрактный класс Figures, описывающий графические фигуры «вообще» */ class Figures { virtual void draw(void) = 0; }; /* Все классы графических объектов будут наследовать от класса Figures и переопределять метод draw, задавая конкретное его поведение */ … /*Теперь мы можем объявить массив указателей на объекты класса Figures, заполнить его адресами объектов производных классов (графических объектов) и вывести фигуры на экран */ Figures *GraphicFigures[10]; int n=0; // число объектов // заполнение массива указателями на графические объекты … // вывод на экран for(int i = 0; i draw();

Программа «Модель обслуживания» демонстрирует основные понятия объектно- ориентированного программирования, включая полиморфизм

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

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

Концепция Подходы к построению моделей Существуют три основных подхода к построению подобных моделей: сканирования активностей (описание действий и условий); процессно-ориентированный (описание процессов); событийный (описание событий). Наша реализация будет использовать событийный подход

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

Решение –Так сразу? – А почему бы и нет? На данном этапе, не вдаваясь в подробности задачи, уже можно определить одну из ключевых абстракций модели – генератор событий. Его задача – заполнять и хранить список будущих событий, извлекать из него событие, ближайшее по времени, и передавать его на обработку. Обработка будет различной в зависимости от типа события. Генератор событий не «знает» и не должен «знать» типы событий и при генерации любого из них должен действовать единообразно: вызывать компонентную функцию некоторого «класса обработчиков событий». Этот класс будет основой для всех классов, реально обрабатывающих события определенных типов, а функция-обработчик окажется виртуальной.

Решение Кто будет обрабатывать события? //Абстрактный класс обработчиков событий class EventsServers { public: virtual void ServeEvent()= 0; }; Каждый класс, наследующий от EventsServers, будет располагать собственной реализацией ServeEvent, подходящей для событий конкретного типа.

Решение Где будут «храниться» события? Генератор событий выбирает из списка будущих событий ближайшее по времени. Поэтому будем использовать очередь приоритетов, описываемую классом PQueue,который разработан ранее. Его определение помещено в файл pqueuelib.h. До подключения этого файла необходимо определить тип элементов очереди и задать максимальную ее длину.

Решение Как будут описываться события? Элементом очереди будет структура, содержащая запись о будущем событии. Она должна включать указатель на объект – обработчик события и время наступления события: //Структура записи о будущем событии struct EventRecord { EventsServers *pEventServer; unsigned EventTime; }; Значением указателя pEventsServer будет адрес объекта класса – потомка EventsServers, а генерация события заключаться в полиморфном вызове: pEventServer–>ServeEvent().

Решение Что быстрее? Извлечение элемента из очереди приоритетов требует сравнения элементов (в нашем случае – для определения ближайшего по времени события). Поэтому необходимо перегрузить оператор «

Решение Ну долго еще? Максимальная длина очереди приоритетов определяется максимальным количеством событий, планируемых в любой момент времени. Рассматривая генератор событий как «самостоятельную» абстракцию, без привязки к конкретной задаче, невозможно точно определить эту величину. (зададим ее некоторым достаточно большим числом – 20). Теперь, наконец, можно включить определение класса PQueue : // Определение класса PQueue typedef EventRecord PQDataType; //тип элементов очереди const int MaxPQSize = 20; // максимальная длина очереди #include "pqueuelib.h" // класс PQueue

Решение Класс «Генераторы событий» // Класс EventsGenerators class EventsGenerators { private: unsigned Time; //текущее время PQueue FutureEvents; //список будущих событий public: //Конструктор EventsGenerators(void); //Запланировать новое событие void PlanNewEvent(EventsServers*, unsigned ); // Вернуть время unsigned GetTime(void); //Главный процесс void Procces(void); };

Реализация Методы класса Events Generators. Конструктор, PlanNewEvent() EventsGenerators::EventsGenerators(void) { Time = 0; } void EventsGenerators::PlanNewEvent(EventsServers *EvServer, unsigned EvTime) { if (EvTime

Реализация Методы класса Events Generators. GetTime(), Procces () unsigned EventsGenerators::GetTime(void) { return Time; } void EventsGenerators::Procces(void) { EventRecord ServingEvent; while(!FutureEvents.PQEmpty()) { ServingEvent = FutureEvents.PQDelete(); Time = ServingEvent.EventTime; ServingEvent.pEventServer->ServeEvent(); }

Вновь постановка задачи… Входные величины Варьируемыми параметрами модели будут постоянные и случайные составляющие промежутков времени между приходами клиентов, времени обслуживания, а также максимальный размер очереди клиентов и число обслуживающих работников: // Время между приходами клиентов = 0...RandClientTime #define RandClientTime 30 // Время обслуживания = // ServingTime...ServingTime+RandServingTime #define ServingTime 30 #define RandServingTime 30 // Максимальный размер очереди #define MaxQSize 6 // Число обслуживающих работников #define NOS 2

Вновь постановка задачи… Результаты: информация о клиентах В Clients.txt будет отражаться информация обо всех клиентах, посетивших учреждение в течение рабочего дня. Эта информация упорядочивается в виде таблицы с заголовком вида: Name: Enter: Qlen: Serbeg: Server: Exit: Wait: Served: All: Позиции заголовка соответствуют следующему порядку внесения информации о клиентах в файл: номер клиента, время прихода, длина очереди на момент прихода, время начала обслуживания, номер обслуживающего работника, время ухода, длительность ожидания, длительность обслуживания, общее время нахождения в учреждении.

Вновь постановка задачи… Результаты: информация о сотрудниках Программа будет создавать несколько файлов, относящихся к работникам: Server1.txt, Server2. txt и т.д.. В этих файлах будет фиксироваться информация о занятости работника в течение рабочего дня и тех клиентах, которые были им обслужены. Эта информация также упорядочивается в виде таблицы с заголовком: Server 1: Number:Client:enter:exit:Service Time: Позиции заголовка: номер клиента по порядку поступления на обслуживание к данному работнику, номер клиента по порядку прихода в учреждение, время начала обслуживания, время окончания обслуживания, длительность обслуживания.

Решение События В нашей системе будут иметь место два вида событий: 1) приход очередного клиента; 2) окончание обслуживания работником одного из клиентов. Следовательно, реальная длина очереди приоритетов будет не более чем число работников (для каждого из которых планируется событие «окончание обслуживания») плюс 1 (событие «приход клиента»).

Решение Классы Анализ задачи позволяет выделить следующие классы: класс Clients (клиенты) – отвечает за хранение и вывод информации о клиенте. Объекты класса накапливают сведения о пребывании клиента в учреждении по мере его продвижения по цепочке «приход–ожидание–обслуживание–уход» и перед уничтожением фиксируют эти сведения в файле Clients.txt ; класс ClientsQueues (очереди клиентов) – отвечает за хранение и обновление информации об очереди клиентов, дожидающихся обслуживания. Заказывает и обрабатывает событие «приход клиента», создавая при этом новые объекты класса Clients, по запросу поставляет первого в очереди клиента на обслуживание;

Решение Классы, продолжение класс Servers (работники) – отвечает за хранение и обновление информации о работнике и его занятости в каждый момент времени. Заказывает и обрабатывает событие «окончание обслуживания»; класс Managers (управляющие) – отвечает за хранение информации о всех работниках, служит связующим звеном между ними и очередью клиентов. Итого: class Clients; class ClientsQueues; class Servers; class Managers;

Решение Класс Clients #include //Класс Clients class Clients { friend Servers; friend ClientsQueues; private: unsigned Name; //имя–номер unsigned QueueLength; //длина очереди на момент прихода unsigned Server; //номер сервера unsigned EnterTime, ServiceBeginTime, ExitTime; static unsigned NumberOfClients; //общее число клиентов static ofstream ClientsOutFile; //выходной поток–файл public: Clients(void); // конструктор ~Clients(); };

Решение Класс ClientsQueues Класc поддерживает структуру типа «Очередь» для клиентов. Поэтому он – наследник класса Queue. Длина очереди задается константой MaxQSize, которая определена выше. Тип элементов очереди – указатель на объект класса Clients : typedef Clients* QDataType; #include "queuelib.h" Методы класса Queue (вставить, удалить) будут вызываться только методами класса ClientsQueue и не должны быть доступны извне, поэтому класс Queue наследуется со спецификатором доступа private. Класс ClientsQueue открыто наследует поведение класса EventsServers, так как определяет обработку события «приход клиента», предоставляя собственный вариант виртуальной функции ServeEvent.

Решение Класс ClientsQueues, продолжение //Класс ClientsQueue class ClientsQueues: public EventsServers, private Queue { private: EventsGenerators *pEventsGenerator; Managers *pManager; public: ClientsQueues(EventsGenerators *, Managers *); // обработать событие «Приход клиента» void ServeEvent(); // Реакция на запрос: «дать клиент» Clients * GetClient(void); ~ClientsQueues(); };

Решение Класс Servers //Класс Servers class Servers: public EventsServers { private: unsigned Name; EventsGenerators *pEventsGenerator; Managers *pManager; ofstream *pServerOutFile; Clients * pClient; unsigned NumberOfServedClients; unsigned ClientEnterTime, ClientExitTime; void SaveInformation(); public: Servers(Managers* m, EventsGenerators * eg,unsigned ); int TakeClient(Clients *); // запрос «возьми клиента» // обработка события «окончание обслуживания» void ServeEvent(); };

Решение Класс Managers //Класс Managers class Managers { private: ClientsQueues *pClientsQueue; unsigned NumberOfServers; Servers * ServersRecord[10]; public: Managers(EventsGenerators *, unsigned ); int TakeClient(Clients *); Clients * GetClient(void); ~Managers(); };

Решение Взаимодействие объектов классов

Решение « И увидел Он, что это хорошо,..» После определения всех методов класса запуск процесса моделирования можно осуществить следующей главной функцией : void main(void) { // инициализация генератора случайных чисел srand(time(0)); EventsGenerators EG; // генератор событий Managers MN(&EG,NOS); // менеджер EG.Procces(); // запуск процесса } На этом процесс разработки можно считать законченным. Осталась сущая мелочь – реализация. Но это дело техники…

Реализация Сначала «статика» Два компонента класса Clients являются статическими, т.е. общими для всех экземпляров класса. Они должны быть инициализированы до создания объектов класса следующим образом: unsigned Clients::NumberOfClients = 0; ofstream Clients::ClientsOutFile; Для вывод в файл времени наступления какого–либо события необходимо преобразование вида // Макрос для вывода времени #define HourMinute(T) T/60

Реализация Методы класса Clients. Конструктор. Clients::Clients(void) { Name = ++NumberOfClients; Server = 0; if (Name==1) { ClientsOutFile.open("Clients.txt"); ClientsOutFile

Реализация Методы класса Clients. Деструктор. Clients::~Clients() { unsigned WaitTime = ServiceBeginTime - EnterTime; unsigned ServiceTime = ExitTime - ServiceBeginTime; unsigned AllTime = ExitTime - EnterTime; ClientsOutFile

Реализация Методы класса ClientsQueues. Конструктор и деструктор, GetClient(). ClientsQueues::ClientsQueues(EventsGenerators *pEvGen, Managers *pM) { pEventsGenerator = pEvGen; pManager = pM; unsigned Time = pEventsGenerator->GetTime(); pEventsGenerator->PlanNewEvent(this,Time + rand()%RandClientTime); } ClientsQueues::~ClientsQueues() { while (!QEmpty()) delete QDelete(); } Clients * ClientsQueues::GetClient(void) { if (!QEmpty()) return QDelete(); return NULL; }

Реализация Методы класса ClientsQueues. ServeEvent(). void ClientsQueues::ServeEvent() { unsigned Time = pEventsGenerator->GetTime(); if (Time>=480) return; if(!QFull()) { Clients * pNewClient = new Clients; pNewClient->EnterTime = Time; pNewClient->QueueLength = QLength(); if(QEmpty()) { if (!pManager->TakeClient(pNewClient)) QInsert(pNewClient); } else QInsert(pNewClient); } pEventsGenerator-> PlanNewEvent(this, Time + rand()%RandClientTime); }

Реализация Методы класса Servers. SaveInformation(). void Servers::SaveInformation() { unsigned ServiceTime = ClientExitTime - ClientEnterTime; *pServerOutFile

Реализация Методы класса Servers. Конструктор. Servers::Servers(Managers *pM, EventsGenerators *pEvGen, unsigned n) { Name = n; pManager = pM; pEventsGenerator = pEvGen; pClient = NULL; NumberOfServedClients =0; pServerOutFile = new(ofstream); char FileName[] = {'S','e','r','v','e','r', (char)Name+48,'.','t','x','t',0}; pServerOutFile->open(FileName); *pServerOutFile

Реализация Методы класса Servers. TakeClient(). int Servers::TakeClient(Clients * pCl) { unsigned Time = pEventsGenerator->GetTime(); if (Time>=480) return 0; if (!pClient) { pClient = pCl; pClient->ServiceBeginTime = Time; pClient->Server = Name; ClientEnterTime = Time; pEventsGenerator->PlanNewEvent(this, Time+ ServingTime + rand()%RandServingTime); return 1; } return 0; }

Реализация Методы класса Servers. ServeEvent(). void Servers::ServeEvent() { unsigned Time = pEventsGenerator->GetTime(); ClientExitTime = Time; pClient->ExitTime = Time; NumberOfServedClients++; SaveInformation(); delete pClient; if (Time>=480) return; pClient = pManager->GetClient(); if (pClient) { pClient->ServiceBeginTime = Time; pClient->Server = Name; ClientEnterTime = Time; pEventsGenerator->PlanNewEvent(this, Time+ ServingTime + rand()%RandServingTime); }

Реализация Методы класса Managers. Конструктор и деструктор. Managers::Managers(EventsGenerators *pEvGen, unsigned sn) { pClientsQueue = new ClientsQueues(pEvGen,this); NumberOfServers = sn; for (int i=0; i< NumberOfServers; i++) ServersRecord[i] = new Servers(this, pEvGen,i+1); } Managers::~Managers() { delete pClientsQueue; for (int i=0; i< NumberOfServers; i++) delete ServersRecord[i]; }

Реализация Методы класса Managers. TakeClient() и GetClient() int Managers::TakeClient(Clients * pCl) { for (int i=0; iTakeClient(pCl)) return 1; return 0; } Clients * Managers::GetClient(void) { return pClientsQueue->GetClient(); }

5.5. Отношения между классами Инстанцирование (instant – подставить) : подразумевает использование шаблонов («параметризированных», «обобщенных» классов) Часто на момент написания класса неизвестны типы его компонентов. Вспомним, например, класс Stack : class Stack { private: SDataType Stacklist[MaxStackSize]; … }; На момент написания класса тип элементов стека неизвестен. Мы были вынуждены дать ему имя SDataType. Перед включением класса в свою программу программист должен определить этот тип как синоним известного типа: typedef int SDataType; // элементы стека имеют тип int

5.5. Отношения между классами У такого подхода есть один существенный недостаток: так как тип данных определяется один раз и не может быть изменен, в программном модуле не может быть нескольких объектов с данными различных типов, – например, стека целых чисел и стека указателей на строки. Язык С++ предоставляет более мощный механизм для решения данной проблемы – использование шаблонов. Шаблон есть описание множества классов, имеющих родственное строение и поведение, а отличающихся только используемыми или обрабатываемыми типами. Таким образом, шаблон задает целое семейство классов. Общее описание шаблона имеет вид: template // Определение класса тип 1, тип 2 – имена типов – параметров шаблона. В определении класса все эти имена должны быть использованы. Описание параметров шаблона действительно только до конца определения класса, т.е. перед каждым шаблоном должно помещаться собственное описание типов, начинающееся с ключевого слова template. Различные шаблоны могут использовать одинаковые имена типов.

5.5. Отношения между классами Шаблон класса Stack мог бы иметь вид: template class Stack { private: SDataType Stacklist[MaxStackSize]; … }; Само инстанцирование подразумевает создание объектов параметризированных классов: Stack S1; //стек целых чисел Stack S2; //стек указателей на строки - при создании объекта необходимо указать тип параметра(ов) шаблона в угловых скобках!

5.5. Отношения между классами При написании шаблонов класса следует помнить, что все его методы также являются шаблонами: template class Stack { private: SDataType Stacklist[MaxStackSize]; … public: void Push(const SDataType& item); … }; template void Stack::Push(const SDataType& item) {…}