• 8.1. ИСТОРИЯ СОЗДАНИЯ ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОГРАММИРОВАНИЯ
  • 8.2. ВВЕДЕНИЕ В ОБЪЕКТНО-ОРИЕНТИРОВАННЫЙ ПОДХОД К РАЗРАБОТКЕ ПРОГРАММ
  • 8.3. СРАВНИТЕЛЬНЫЙ АНАЛИЗ ТЕХНОЛОГИЙ СТРУКТУРНОГО И ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОГРАММИРОВАНИЯ
  • 8.4. ОСНОВНЫЕ ПОНЯТИЯ ОБЪЕКТНО-ОРИЕНТИРОВАННОЙ ТЕХНОЛОГИИ
  • 8.5. ОСНОВНЫЕ ПОНЯТИЯ, ИСПОЛЬЗУЕМЫЕ В ОБЪЕКТНО-ОРИЕНТИРОВАННЫХ ЯЗЫКАХ
  • 8.6. ЭТАПЫ И МОДЕЛИ ОБЪЕКТНО-ОРИЕНТИРОВАННОЙ ТЕХНОЛОГИИ
  • 8.7. КАКИМИ БЫВАЮТ ОБЪЕКТЫ ПО УСТРОЙСТВУ
  • 8.8. ПРОЕКТНАЯ ПРОЦЕДУРА ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОЕКТИРОВАНИЯ ПО Б. СТРАУСТРУПУ
  • 8.9. ТЕХНОЛОГИЯ ПРОЕКТИРОВАНИЯ НА ОСНОВЕ ОБЯЗАННОСТЕЙ
  • 8.10. ПРИМЕР РЕТРОСПЕКТИВНОЙ РАЗРАБОТКИ ИЕРАРХИИ КЛАССОВ БИБЛИОТЕКИ ВИЗУАЛЬНЫХ КОМПОНЕНТ DELPHI И C++ BUILDER
  • 8.11. АЛЬТЕРНАТИВНЫЙ ПРОЕКТ ГРАФИЧЕСКОГО ИНТЕРФЕЙСА
  • 8.12. ПРОЕКТ АСУ ПРЕДПРИЯТИЯ
  • 8.13. ОБЗОР ОСОБЕННОСТЕЙ ПРОЕКТОВ ПРИКЛАДНЫХ СИСТЕМ
  • 8.14. ГИБРИДНЫЕ ТЕХНОЛОГИИ ПРОЕКТИРОВАНИЯ
  • Глава 8

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

    8.1. ИСТОРИЯ СОЗДАНИЯ ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОГРАММИРОВАНИЯ

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

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

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

    Как выяснилось, ООПр оказались пригодными не только для моделирования (Simula) и разработки графических приложений-(SmallTalk), но и для создания большинства других приложений, а их приближенность к человеческому мышлению и возможность многократного использования кода сделали их наиболее используемыми в программировании.

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

    8.2. ВВЕДЕНИЕ В ОБЪЕКТНО-ОРИЕНТИРОВАННЫЙ ПОДХОД К РАЗРАБОТКЕ ПРОГРАММ

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

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

    Модуль ООПр — файл описаний объектов и действий над ними.

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

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

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

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

    У кассира имеется некоторый определенный метод, или эвроритм, или последовательность операций (процедура), которые используют работники кассы для выполнения вашего запроса. Имеются у кассира и другие методы, например по сдаче денег, — инкассации.

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

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

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

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

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

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

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

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

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

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

    Классы могут быть организованы в иерархическую структуру с наследованием свойств. Класс-потомок наследует атрибуты родительского класса, расположенного ниже в иерархическом дереве (если дерево иерархии наследования растет вверх). Абстрактный родительский класс — это класс, не имеющий экземпляров объектов. Он используется только для порождения потомков. Класс "HomoSapience", скорее всего, будет абстрактным, поскольку для практического применения, например работодателю, экземпляры его объектов не интересны.

    Рис. 8.1. Понятие создания объекта как экземпляра класса ПТИЦЫ


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

    От класса "кассир железнодорожной кассы" могут быть получены экземпляры объектов: "кассир кассы № 1", "кассир кассы № 2", "кассир кассы № 3" и т. д.

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

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

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

    Рис. 8.3. Пример простого и множественного наследования


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

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

    Агрегированный объект — объект, составленный из подобъектов. Подобъекты называются частями агрегата, и агрегат отвечает за них. Например, в системах с множественным наследованием шахматная фигура ферзь может быть унаследована от слона и ладьи. В системах с отсутствующим множественным наследованием можно получить ферзя двумя способами. Согласно первому способу, можно создать класс "любая_фигура" и далее, в периоде выполнения, делегировать полномочия каждому объекту-экземпляру данного класса быть ладьей, слоном, ферзей, пешкой и т. д. По второму способу после получения классов "ладья" и "слон" их можно объединить композицией в класс "ферзь". Теперь объект класса "ферзь" можно использовать как объект "ферзь" или даже как объект "слон", для чего объекту "ферзь" делегируется выполнение полномочий слона. Более того, можно делегировать объекту "ферзь" полномочия стать объектами "король" или даже "пешка"! Для композиции требуется, чтобы объединяемые объекты имели четко определенные интерфейсы. И у наследования, и у композиции есть достоинства и недостатки.

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

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

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

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

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

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

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

    С помощью делегирования композицию можно сделать столь же мощным инструментом повторного использования, сколь и наследование. При делегировании в процесс обработки запроса вовлечено два объекта: получатель поручает выполнение операций другому объекту — уполномоченному. Примерно так же подкласс делегирует ответственность своему родительскому классу. Но унаследованная операция всегда может обратиться к объекту-получателю через переменную-член (в C++) или переменную self (в Smalltalk). Чтобы достичь того же эффекта для делегирования, получатель передает указатель на самого себя соответствующему объекту, чтобы при выполнении делегированной операции последний мог обратиться к непосредственному адресату запроса.

    Например, вместо того чтобы делать класс Window (окно) подклассом класса Rectangle (прямоугольник) — ведь окно является прямоугольником, — мы можем воспользоваться внутри Window поведением класса Rectangle, поместив в класс Window переменную экземпляра типа Rectangle и делегируя ей операции, специфичные для прямоугольников. Другими словами, окно не является прямоугольником, а содержит его. Теперь класс Window может явно перенаправлять запросы своему члену Rectangle, а не наследовать его операции.

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

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

    Таким образом, можно выделить следующие фундаментальные характеристики объектно-ориентированного мышления:

    Характеристика 1. Любой предмет или явление могут рассматриваться как объект.

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

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

    Характеристика 4. Вычисления осуществляются путем взаимодействия (обмена данными) между объектами, при котором один объект требует, чтобы другой объект выполнил некоторое действие (метод). Объекты взаимодействуют, посылая и получая сообщения. Сообщение — это запрос на выполнение действия, дополненный набором аргументов, которые могут понадобиться при выполнении действия. Объект — получатель сообщения — обрабатывает сообщения своими внутренними методами.

    Характеристика 5. Каждый объект является представителем класса, который выражает общие свойства объектов данного класса в виде одинаковых списков набора данных (полей) в своей памяти и внутренних методов, обрабатывающих сообщения. В классе методы задают поведение объекта. Тем самым все объекты, которые являются экземплярами одного класса, могут выполнять одни и те же действия.

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

    Характеристика 7. Благодаря полиморфизму — способности подставлять во время выполнения вместо одного объекта другой, с совместимым интерфейсом, в периоде выполнения одни и те же объекты могут разными методами исполнять одни и те же запросы сообщений.

    Характеристика 8. Композиция является предпочтительной альтернативой множественному наследованию и позволяет изменять состав объектов агрегата в процессе выполнения программы.

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

    8.3. СРАВНИТЕЛЬНЫЙ АНАЛИЗ ТЕХНОЛОГИЙ СТРУКТУРНОГО И ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОГРАММИРОВАНИЯ

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

    Информация по отдельным файлам представлена:

    1) именем файла;

    2) общим количеством строк файла (показывается текстовым редактором);

    3) количеством строк операторов описаний данных во всем файле;

    4) общим количеством комментариев в файле (выявляется контекстным поиском признака комментария в тексте файла);

    5) количеством строк отдельных комментариев в файле;

    6) количеством пустых строк в файле (выявляется визуальным анализом текста файла);

    7) количеством подпрограмм в файле (является контекстным поиском заголовков procedure и function в тексте файла);

    8) количеством операторов описания подпрограмм в файле;

    9) количеством строк кода, рассчитанных по формуле: количество строк кода = 2) — 3) — 5) — 6) — 8).

    Количество операторов описания подпрограмм в файле выявляется по принципу подсчета всех срок, например, в следующем примере выявлено четыре строки:

    function CellString (Col, Row: Word; var Color: Word;

    Formatting; Boolean): String;

    Begin

    End; {CellStrung}

    Для проведения объективного сравнительного анализа потребовался выбор функционально похожих программ:

    — Mcalc — рассмотренная ранее в гл. 2 и 7 демонстрационная программа, реализованная по технологии структурного программирования;

    — Tcalc — демонстрационная программа, реализованная по технологий объектно-ориентированного программирования — функционально полный аналог программы Mcalc.

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

    Таблица 8.1

    Результаты анализа текста программы MCalc

    Имя файла Всего строк Количество описательных операторов Комментарии Пустых строк Количество процедур Количество описательных операторов процедур Код
    Всего Строк
    Mcalc 143 8 11 7 5 2 6 117
    Mcdisply 357 54 47 15 49 18 64 175
    Mcinput 240 33 18 8 19 7 25 155
    Mclib 503 68 47 20 46 21 73 296
    Mcommand 873 88 63 19 54 24 86 626
    Mcparser 579 51 33 21 16 12 36 455
    Mcutil 413 62 46 16 45 18 75 215
    mcvars 124 96 9 5 19 0 0 0
    Итого: 3232 460 274 111 253 102 365 2043
    15,4% 3,7% 12,3% 68,6%

    Анализ демонстрационный программы TCalc "Borland Inc."

    Программа TCalc 1993 (Turbo Pascal 6.0) состоит из следующих файлов:

    tcalc.pas — файл основной программы;

    tcell.pas — файл работы с клетками;

    tcellsp.pas — файл дополнений работы с клетками (изменение значений);

    tchash.pas — файл дополнений работы с клетками (значения в клетках);

    tcinput.pas — файл подпрограмм ввода данных с клавиатуры;

    tclstr.pas — файл подпрограмм работы со строками;

    tcmenu.pas — файл подпрограмм, обслуживающих систему меню;

    tcparser.pas — файл интерпретатора арифметических выражений формул клеток;

    tcrun.pas — файл инициализации и запуска основных объектов;

    tcscreen.pas — файл подпрограмм работы с дисплеем;

    tcsheet.pas — файл подпрограмм, обслуживающих действия, выбранных посредством меню;

    tcutil.pas — файл вспомогательных подпрограмм;

    mcmvsmem.asm — ассемблерный файл подпрограмм запоминания в оперативной памяти информации экрана, а также восстановления ранее сохраненной информации экрана.

    Все файлы закодированы с соблюдением стандартов оформления.

    Хотя фирма "Borland Inc." занимается разработкой компиляторов, файл mcparser.pas также является заимствованным из UNIX YACC utility и лишь частично модифицирован. Остальные файлы являются оригинальными.

    Ассемблерный файл mcmvsmem.asm является искусственно добавленным. Цель его добавления — демонстрация возможности использования ассемблерных вставок. Результаты арифметического анализа текста программы представлены в табл. 8.2.

    Таблица 8.2

    Результаты анализа текста программы TCalc

    Имя файла Всего строк Количество описательных операторов Комментарии Пустых строк Количество процедур Количество описательных операторов процедур Код
    Всего Строк
    Tcalc 21 2 9 3 5 1 3 8
    Tcell 1962 490 206 20 153 46 152 1147
    Tcellsp 228 39 24 5 18 6 25 141
    Tchash 262 50 47 23 23 14 43 123
    Tcinput 334 63 39 15 22 9 32 202
    Tclstr 243 45 120 20 12 15 52 114
    Tcmenu 234 48 40 20 21 22 66 79
    Tcparser 677 73 29 5 17 9 64 518
    Tcrun 1367 146 128 59 57 47 163 942
    Tcscreen 523 215 92 37 16 8 96 159
    Tcsheet 1722 240 170 40 44 32 101 1297
    Tcutil 379 114 55 38 70 29 115 42
    Итого: 7952 1525 959 285 458 238 912 4772
    20,3% 3,8% 12,2% 63,7%

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

    Таблица 8.3

    Результаты сравнительного анализа технологий структурного и объектно-ориентированного программирования

    Имя программы Всего строк Количество описательных операторов Комментарии Пустых строк Количество процедур Количество операторов процедур Код
    Всего Строк
    MCalc 3232 460 274 111 253 102 365 2043
    15,4% 3,7% 12,3% 68,6%
    TCalc 7952 1525 959 285 458 238 912 4772
    20,3% 3,8% 12,2% 63,7%

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

    • процентного состава описательных операторов;

    • процентного состава количества комментариев;

    • процентного состава описательных операторов процедур;

    • процентного состава операторов кода программы.

    Рис. 8.3. Результаты сравнительного анализа технологий структурного и объектно-ориентированного программирования


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

    Собственно функционально полезный код программ Mcalc и Tcalc — одинаковый и составляет порядка 1500 строк.

    Почти 2,3–3,5 кратное увеличение трудоемкости разработки объясняется платой за организацию самостоятельности поведения объектов и их завершенную функциональность для повторного использования.

    8.4. ОСНОВНЫЕ ПОНЯТИЯ ОБЪЕКТНО-ОРИЕНТИРОВАННОЙ ТЕХНОЛОГИИ

    С чего же начинается создание объектно-ориентированной программы?

    Конечно, с объектно-ориентированного анализа (ООА — object-oriented analysis), который направлен на создание моделей реальной действительности на основе объектно-ориентированного мировоззрения. Объектно-ориентированный анализ (ООА) — это методология, при которой требования к системе воспринимаются с точки зрения классов и объектов, прагматически выявленных в предметной области.

    На результатах QOA формируются модели, на которых основывается объектно-ориентированное проектирование (object-oriented design, OOD).

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

    Что же такое объектно-ориентированное программирование (ООПр) (object-oriented programming)? Программирование прежде всего подразумевает правильное и эффективное использование механизмов конкретных языков программирования. Объектно-ориентированное программирование — это процесс реализации программ, основанный на представлении программы в виде совокупности объектов. ООПр предполагает, что любая функция (процедура) в программе представляет собой метод объекта некоторого класса, причем класс должен формироваться в программе естественным образом, как только в программе возникает необходимость описания новых физических предметов или их абстрактных понятий (объектов программирования). Каждый новый шаг в разработке алгоритма также должен представлять собой разработку нового класса на основе уже существующих классов, т. е. технология ООПр иначе может быть названа как программирование "от класса к классу".

    Можно ли реализовать объектно-ориентированную программу не на объектно-ориентированных языках? Ответ, скорее всего, положителен, хотя придется преодолеть ряд трудностей. Ведь главное, что требуется, — это реализовать объектную модель. Сокрытие информации при использовании обычных языков, в принципе, можно реализовать сокрытием доступности вызовов подпрограмм в файлах (Unit). Инкапсуляцию объектов можно достичь как и в объектно-ориентированных языках написанием отдельных подпрограмм. Далее можно считать, что каждый объект порождается от своего уникального класса. Конечно, иерархии классов в таком проекте не будет и для достижения параллелизма придется писать код для организации вызова к исполнению как бы сразу нескольких копий процедур, но программа при этом будет вполне объектно-ориентированной.

    8.5. ОСНОВНЫЕ ПОНЯТИЯ, ИСПОЛЬЗУЕМЫЕ В ОБЪЕКТНО-ОРИЕНТИРОВАННЫХ ЯЗЫКАХ

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

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

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

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

    Объект — это логическая единица, которая содержит данные и правила (методы с кодом алгоритма) (см. рис. 1.8). Другими словами, объект — это расположенные в отдельном участке памяти:

    — порция данных объекта или атрибуты исходных данных, называемые еще полями, членами данных (data members), значения которых определяют текущее состояние объекта;

    — методы объекта (methods, в разных языках программирования еще называют подпрограммами, действиями, member functions или функциями-членами), реализующие действия (выполнение алгоритмов) в ответ на их вызов в виде переданного сообщения;

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

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

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

    В соответствии с описанием класса внутри объекта данные и методы могут быть как открытыми по интерфейсу public, так и сокрытыми private.

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

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

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

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

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

    Преимущество 2. Целостность ссылок. Перед доступом к объекту, связанному с данным объектом, можно удостовериться, что косвенное поле содержит корректное значение (ссылку на экземпляр).

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

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

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

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

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

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

    Итак, наследование проявляется в том, что любой класс-потомок имеет доступ или, другими словами, наследует практически все ресурсы (методы, поля и свойства) родительского класса и всех предков до самого верхнего уровня иерархии.

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

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

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

    Таким образом, наследование выполняет в ООП несколько важных функций:

    • моделирует концептуальную структуру предметной области;

    • экономит описания, позволяя использовать их многократно для задания разных классов;

    • обеспечивает пошаговое программирование больших систем путем многократной конкретизации классов.

    Ряд языков, например Object Pascal, описание которого дается в приложении 4, поддерживает модель наследования, известную как простое наследование и которая ограничивает число родителей конкретного класса одним. Другими словами, определенный пользователем класс имеет только одного родителя. Схема иерархии классов в этом случае представляет собой ряд одиночно стоящих деревьев (hierarchical classification).

    Более мощная модель сложного наследования, называемая множественным наследованием, в которой каждый класс может, в принципе, порождаться сразу от нескольких родительских классов, наследуя поведение всех своих предков. Множественное наследование не поддерживается в Delphi, но поддерживается в Visual C++ и ряде других языков. При множественном наследовании составляется уже не схема иерархии, а сеть, которая может включать деревья со сросшимися кронами.

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

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

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

    На агрегировании основана работа таких систем визуального программирования, как Delphi, C++ Builder. В этих системах имеется порождающий объект пользователя класс-форма (пустое окно Windows). Системы обеспечивают подключение к форме через указатели нужных пользователю объектов, например кнопок, окон редакторов и т. д. При перерисовке формы на экране монитора как бы одновременно с ней перерисовываются изображения агрегированных объектов. Более того, при активизации формы агрегированные объекты также становятся активными: кнопки начинают нажиматься, а в окна редакторов можно начинать вводить информацию.

    Одним из базовых понятий технологии ООП является полиморфизм. Термин "полиморфизм" имеет греческое происхождение и означает приблизительно "много форм" (poly — много, morphos — форма).

    Полиморфизм — это средство для придания различных значений одному и тому же событию в зависимости от типа обрабатываемых данных, т. е. полиморфизм определяет различные формы реализации одноименного действия (см. рис. 8.2.).

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

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

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

    В более общей трактовке полиморфизм — это способность объектов, принадлежащих к разным типам, демонстрировать одинаковое поведение; способность объектов, принадлежащих к одному типу, демонстрировать разное поведение.

    Рассмотрим "вырожденный пример" полиморфизма. В MS DOS есть понятие "номер прерывания", за которым скрывается адрес в памяти. Поместите в ту же ячейку другой адрес — и программы начнут вызывать процедуру с другим "именем" и из другого модуля. Как видно из примера, принцип полиморфизма можно реализовать и не в объектно-ориентированных программах.

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

    Перегрузка функций. Одним из применений полиморфизма в C++ является перегрузка функций. Она дает одному и тому же имени функции различные значения. Например, выражение а + b имеет различные значения, в зависимости от типов переменных а и b (допустим, если это числа, то "+" означает сложение, а если строки, — то склейку этих строк или вообще сложение комплексных чисел, если а и b комплексного типа). Перегрузка оператора "+" для типов, определяемых пользователем, позволяет использовать их в большинстве случаев так же, как и встроенные типы. Двум или более функциям (операция — это тоже функция) может быть дано одно и то же имя. Но при этом функции должны отличаться сигнатурой (либо типами параметров, либо их числом).

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

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

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

    8.6. ЭТАПЫ И МОДЕЛИ ОБЪЕКТНО-ОРИЕНТИРОВАННОЙ ТЕХНОЛОГИИ

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

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

    Обычно проектируемая программная система первоначально представляется в виде трех взаимосвязанных моделей:

    1) объектной модели, которая представляет статические, структурные аспекты системы;

    2) динамической модели, которая описывает работу отдельных частей системы;

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

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

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

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

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

    1) определение объектов;

    2) подготовку словаря объектов с целью исключения схожих (синонимичных) понятий и уточнения имен, классификацию объектов, выделение классов;

    3) определение взаимосвязей между объектами;

    4) определение атрибутов объектов и методов (определение уровней доступа и проектирование интерфейсов классов);

    5) исследование качества модели.

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

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

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

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

    Объектную модель можно описать следующим образом:

    1) основные элементы модели — объекты и сообщения;

    2) объекты создаются, используются и уничтожаются подобно динамическим переменным в обычных языках программирования;

    3) выполнение программы заключается в создании объектов и передаче им последовательности сообщений.

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

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

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

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

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

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

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

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

    8.7. КАКИМИ БЫВАЮТ ОБЪЕКТЫ ПО УСТРОЙСТВУ

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

    В общем случае паттерн состоит из четырех основных элементов: имени, задачи, решения, результатов.

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

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

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

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

    — название и классификация паттерна (название паттерна должно четко отражать его назначение);

    — назначение (лаконичный ответ на следующие вопросы: каковы функции паттерна, его обоснование и назначение, какую конкретную задачу проектирования можно решить с его помощью);

    — известен также под именем (другие распространенные названия паттерна, если таковые имеются);

    — мотивация (сценарий, иллюстрирующий задачу проектирования и то, как она решается данной структурой класса или объекта. Благодаря мотивации, можно лучше понять последующее, более абстрактное описание паттерна);

    — применимость (описание ситуаций, в которых можно применять данный паттерн; примеры проектирования, которые можно улучшить с его помощью);

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

    — участники (классы или объекты, задействованные в данном паттерне проектирования, и их функции);

    — отношения (взаимодействие участников для выполнения своих функций);

    — результаты (насколько паттерн удовлетворяет поставленным требованиям? Результаты применения, компромиссы, на которые приходится идти. Какие аспекты поведения системы можно независимо изменять, используя данный паттерн?);

    — реализация (сложности и так называемые "подводные камни" при реализации паттерна; советы и рекомендуемые приемы; есть ли у данного паттерна зависимость от языка программирования?);

    — пример кода (фрагмент кода, иллюстрирующий вероятную реализацию на языках C++ или Smalltalk);

    — известные применения (возможности применения паттерна в реальных системах; даются, по меньшей мере, два примера из различных областей);

    — родственные паттерны (связь других паттернов проектирования с данными, важные различия, использование данного паттерна в сочетании с другими).

    Каталог содержит 23 паттерна. Ниже для удобства перечислены их имена и назначение.

    1. Abstract Factory (абстрактная фабрика). Предоставляет интерфейс для создания семейств, связанных между собой, или независимых объектов, конкретные классы которых неизвестны.

    2. Adapter (адаптер). Преобразует интерфейс класса в некоторый другой интерфейс, ожидаемый клиентами. Обеспечивает совместную работу классов, которая была бы невозможна без данного паттерна из-за несовместимости интерфейсов.

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

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

    5. Chain of Responsibility (цепочка обязанностей). Можно избежать жесткой зависимости отправителя запроса от его получателя, при этом запросом начинает обрабатываться один из нескольких объектов. Объекты-получатели связываются в цепочку, и запрос передается по цепочке, пока какой-то объект его не обработает.

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

    7. Composite (компоновщик). Группирует объекты в древовидные структуры для представления иерархий типа "часть-целое". Позволяет клиентам работать с единичными объектами так же, как с группами объектов.

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

    9. Facade (фасад). Предоставляет унифицированный интерфейс к множеству интерфейсов в некоторой подсистеме. Определяет интерфейс более высокого уровня, облегчающий работу с подсистемой.

    10. Factory Method (фабричный метод). Определяет интерфейс для создания объектов, при этом выбранный класс инстанцируется подклассами.

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

    12. Interpreter (интерпретатор). Для заданного языка определяет представление его грамматики, а также интерпретатор предложений языка, использующий это представление.

    13. Iterator (итератор). Дает возможность последовательно обойти все элементы составного объекта, не раскрывая его внутреннего представления.

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

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

    16. Observer (наблюдатель). Определяет между объектами зависимость типа "один ко многим", так что при изменении состояния одного объекта все зависящие от него получают извещение и автоматически обновляются.

    17. Prototype (прототип). Описывает виды создаваемых объектов с помощью прототипа и создает новые объекты путем его копирования.

    18. Proxy (заместитель). Подменяет другой объект для контроля доступа к нему.

    19. Singleton (одиночка). Гарантирует, что некоторый класс может иметь только один экземпляр, и предоставляет глобальную точку доступа к нему.

    20. State (состояние). Позволяет объекту варьировать свое поведение при изменении внутреннего состояния. При этом создается впечатление, что поменялся класс объекта.

    21. Strategy (стратегия). Определяет семейство алгоритмов, инкапсулируя их все и позволяя подставлять один вместо другого. Можно менять алгоритм независимо от клиента, который им пользуется.

    22. Template Method (шаблонный метод). Определяет скелет алгоритма, перекладывая ответственность за некоторые его шаги на подклассы. Позволяет подклассам переопределять шаги алгоритма, не меняя его общей структуры.

    23. Visitor (посетитель). Представляет операцию, которую надо выполнить над элементами объекта. Позволяет определить новую операцию, не меняя классы элементов, к которым он применяется.

    8.8. ПРОЕКТНАЯ ПРОЦЕДУРА ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОЕКТИРОВАНИЯ ПО Б. СТРАУСТРУПУ

    8.8.1. Укрупненное изложение проектной процедуры Б. Страуструпа

    Б. Страуструп — автор объектно-ориентированного языка программирования C++ с множественным наследованием. У Б. Страуструпа при описании методики проектирования вводится единица проектирования — "компонента". Под компонентой понимается множество классов, объединенных некоторым логическим условием, иногда это общий стиль программирования или описания, иногда — предоставляемый сервис. Ряд авторов вместо термина "компонента" используют термин "модуль".

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

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

    Шаг 1. Выделение понятий (классов, порождающих объекты) и установление основных связей между ними.

    Шаг 2. Уточнение классов с определением наборов операций (методов) для каждого.

    Шаг 3. Уточнение классов с точным определением их зависимостей от других классов. Выясняется наследование и использование зависимостей.

    Шаг 4. Задание интерфейсов классов. Более точно определяются отношения классов. Методы разделяются на общие и защищенные. Определяются типы операций над классами.

    8.8.2. Шаг 1. Выделение понятий и установление основных связей между ними

    Выделение объектов производится во время процесса мысленного представления системы. Часто это происходит как цикл вопросов "что/кто". Команда программистов определяет: что требуется делать? Это немедленно приводит к вопросу: кто будет выполнять действие? Теперь программная система в значительной мере становится похожей на некую организацию. Действия, которые должны быть выполнены, присваиваются некоторому программному объекту в качестве его обязанностей.

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

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

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

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

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

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

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

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

    Классы, являющиеся артефактами реализации или абстракциями периода реализации, — это те понятия, которые применяют программисты и проектировщики для описания методов реализации:

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

    • классы, представляющие системные ресурсы (процессы, потоки ввода-вывода);

    • классы, реализующие программные структуры (стеки, очереди, списки, деревья, словари и т. п.);

    • другие абстракции, например элементы управления программой (кнопки, меню и т. п.).

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

    Пример:

    1) классы, представляющие пользовательские понятия (например, легковые машины и грузовики);

    2) классы, представляющие обобщения пользовательских понятий (движущиеся средства);

    3) классы, представляющие аппаратные ресурсы (например, класс управления памятью);

    4) классы, представляющие системные ресурсы (например, выходные потоки);

    5) классы, используемые для реализации других классов (например, списки, очереди);

    6) встроенные типы данных и структуры управления.

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

    (1+2) — представляет пользовательское отражение системы;

    (3+4) — представляет машину, на которой будет работать система;

    (5+6) — представляет низкоуровневое (со стороны языка программирования) отражение реализации.

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

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

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

    8.8.3. Шаг 2. Уточнение классов с определением набора операций (методов) для каждого

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

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

    — рассмотрите, каким образом объект класса будет создаваться, копироваться (если нужно) и уничтожаться;

    — определите минимальный набор операций, необходимый для понятия, представленного классом;

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

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

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

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

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

    При определении набора операций (методов) больше внимания следует уделять тому, что надо сделать, а не тому, как это сделать.

    Иногда полезно классифицировать операции класса по тому, как они работают с внутренним состоянием объектов:

    1) базовые операции: конструкторы, деструкторы, операции копирования;

    2) селекторы: операции, не изменяющие состояния объекта;

    3) модификаторы: операции, изменяющие состояние объекта;

    4) операции преобразований, т. е. операции, порождающие объект другого типа, исходя из значения (состояния) объекта, к которому они применяются;

    5) повторители: операции, которые открывают доступ к объектам класса или используют последовательность объектов.

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

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

    8.8.4. Шаг 3. Уточнение классов с точным определением их зависимостей от других классов

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

    Еще одно взаимоотношение — отношение включения {агрегирования) — класс содержит в виде члена объект или указатель на объект другого класса. Позволяя объектам содержать указатели на другие объекты, можно создавать так называемые "иерархии объектов". Такие реализации альтернативно дополняют возможности использования иерархии классов.

    Очень важным при проектировании является вопрос: какое отношение выбрать — агрегации (включения) или наследования. В принципе эти методы взаимозаменяемы, кроме случая, когда используется позднее связывание. Наиболее предпочтителен тот вариант, в котором наиболее точно моделируется окружающая действительность, т. е. если понятие X является частью понятия Y, то используется включение. Если понятие X более общее, чем Y, — то наследование.

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

    — X использует Y;

    — X вызывает функцию-член (метод) Y;

    — X читает член Y;

    — X пишет в член Y;

    — X создает Y;

    — X размещает переменную из Y

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

    Запрограммированные отношения — те отношения проекта, которые не могут быть прямо представлены в виде конструкций языка. Допустим, в проекте оговорено, что каждая операция, не реализованная в классе А, должна обслуживаться объектом класса В. К запрограммированным отношениям относят также операции преобразования типов. Следует, по возможности, избегать применения этого вида отношений из-за усложнения реализации. Идеальный класс должен в минимальной степени зависеть от остального мира. Следовательно, следует стараться минимизировать зависимости.

    8.8.5. Шаг 4. Задание интерфейсов классов

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

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

    Правило 2. Разработчик программного обеспечения должен знать только требуемое поведение объекта и ничего кроме этого.

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

    На этом шаге дается четкое описание классов, их данных и методов (опуская реализацию и, возможно, скрытые методы). Всем методам задаются точные типы параметров.

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

    Интерфейсы классов предоставляют полную информацию для реализации классов на этапе кодирования.

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

    8.8.6. Перестройка иерархии классов

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

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

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

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

    Расщепление применяется в следующих случаях:

    1) если имеется сложный класс, иногда имеет смысл разделить его на несколько простых классов и тем самым обеспечить поэтапную разработку;

    2) класс содержит ряд несвязанных между собой функций или набор независимых друг от друга данных.

    Обобщение — выявление в группе классов общих свойств и вынесение их в общий базовый класс. Признаки необходимости обобщения таковы:

    1) общая схема использования;

    2) сходство между наборами операций;

    3) сходство реализаций;

    4) эти классы часто фигурируют вместе в дискуссиях по проекту.

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

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

    8.8.7. Свод правил

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

    Правило 1. Узнайте, что вам предстоит создать.

    Правило 2. Ставьте определенные и осязаемые цели.

    Правило 3. Не пытайтесь с помощью технических приемов решить социальные проблемы.

    Правило 4. Рассчитывайте на большой срок в проектировании и управлении людьми.

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

    Правило 6. Проектируйте в расчете на изменения: гибкость, расширяемость, переносимость, повторное использование.

    Правило 7. Документируйте, предлагайте и поддерживайте повторно используемые компоненты.

    Правило 8. Поощряйте и вознаграждайте повторное использование: проектов, библиотек, классов.

    Правило 9. Сосредоточьтесь на проектировании компоненты.

    Правило 10. Используйте классы для представления понятий.

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

    Правило 12. Проводите строгую типизацию интерфейсов всегда, когда это возможно.

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

    Правило 14. Многократно исследуйте и уточняйте как проект, так и реализацию.

    Правило 15. Используйте лучшие доступные средства для проверки и анализа проекта и реализации.

    Правило 16. Экспериментируйте, анализируйте и проводите тестирование на самом возможном раннем этапе.

    Правило 17. Стремитесь к простоте, максимальной простоте, но не сверх того.

    Правило 18. Не разрастайтесь, не добавляйте возможности "на всякий случай".

    Правило 19. Не забывайте об эффективности.

    Правило 20. Сохраняйте уровень формализации, соответствующий размеру проекта.

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

    8.8.8. Пример простейшего проекта

    Б. Страуструп придумал реализацию механизма множественного наследования и при этом отвергал агрегирование, хотя и реализовал это в своем языке C++.

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

    Первый способ решения задачи — использование множественного наследования.

    Постановка задачи примера. Вывести на экран фигуру, показанную на рис. 8.4.

    Рис. 8.4. Изображение выводимой фигуры


    Изображенная на рис. 8.4 фигура состоит из правильного пятиугольника и описанной вокруг него окружности, где хс, yc — координаты центра описанной вокруг пятиугольника окружности; R — радиус описанной вокруг пятиугольника окружности.

    Кроме того, фигура рисуется заданным цветом.

    Следует отметить, что задача может быть решена несколькими способами.

    Шаг 1а. Определение объектов и выявление их свойств.

    Объект — Рисунок. Свойства объекта:

    — радиус окружности (R);

    — координаты центра окружности (xc; yc);

    — цвет линий.

    Объект — Пятиугольник. Свойства объекта:

    — радиус описанной вокруг него окружности (R);

    — координаты центра описанной вокруг него окружности (хс; yc):

    — цвет линии.

    Объект — Окружность. Свойства объекта:

    — радиус (R);

    — координаты центра (хс; yc);

    — цвет линии.

    Решение задачи примера с использованием множественного наследования.

    Шаг 1б. Классификация объектов (составление словаря).

    Пятиугольник — центрально-симметричная фигура с пятью вершинами.

    Окружность — центрально-симметричная фигура, каждая точка которой отстоит от заданной точки — центра, на заданную величину — радиус окружности.

    Полученный граф наследования классов изображен на рис. 8.5.

    Шаг 2. Уточнение классов с точным определением их зависимостей от других классов. Выясняется наследование и использование зависимостей.

    Рис. 8.5. Граф наследования классов согласно первому способу


    Поскольку Пятиугольник и Окружность — это разновидности центрально-симметричных фигур, то им может соответствовать следующая иерархия классов. Базовый класс: Центрально-симметричная фигура с данными R, хс, yc. Классы Пятиугольник и Окружность являются наследниками этого класса, а класс Рисунок является наследником классов Окружность и Пятиугольник, поскольку в данной задаче рисунок является сочетанием пятиугольника и окружности.

    Шаг 3. Уточнение классов с определением наборов операций для каждого. Здесь анализируется потребность в конструкторах, деструкторах и операциях копирования. При этом принимается во внимание минимальность, полнота и удобство.

    Класс Рисунок. Экземпляр этого класса должен создаваться и рисоваться, а следовательно, в интерфейсе класса Рисунок должны присутствовать конструкторы и функция — член рисования рисунка. Тогда получаем:

    • конструктор без параметров;

    • конструктор с параметрами (Радиус, х-координата, y-координата, Цвет);

    • функцию-член вывода рисунка — "Начертить".

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

    • конструктор без параметров;

    • конструктор с параметрами (Радиус, х-координата, y-координата);

    • функцию-член вывода пятиугольника на экран — "Начертить".

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

    • конструктор без параметров;

    • конструктор с параметрами (Радиус, х-координата, у-координата);

    • функцию-член вывода окружности на экран — "Начертить".

    Класс Центрально-симметричная фигура. Экземпляр данного класса должен содержать информацию о центрально-симметричной фигуре в виде данных с защищенным доступом (не интерфейсная часть класса) и иметь чисто-виртуальную функцию перерисовки вместе с конструкторами. Тогда получаем:

    • конструктор без параметров;

    • конструктор с параметрами (Радиус, х-координата, y-координата);

    • чисто-виртуальную функцию-член вывода изображения на экран.

    Шаг 4. Задание интерфейсов классов. Более точно определяются отношения классов. Методы разделяются на общие и защищенные методы. Определяются типы операций над классами.

    Данные, расположенные в классе Центрально-симметричная фигура (R, хс, yc), должны быть доступны классам-наследникам Пятиугольник и Окружность, но недоступны "извне", значит, уровень доступа — "защищенный". В классе Центрально-симметричная фигура нужно расположить функцию "Нарисовать", которую предполагается сделать чисто-виртуальной. Классы, наследующие у класса Центрально-симметричная фигура, смогут переопределить функцию "Нарисовать" для рисования самих себя.

    Поскольку обоим объектам — экземплярам классов Пятиугольник и Окружность нужен только один центр на двоих, то, следовательно, экземпляр класса Центрально-симметричная фигура должен создаваться только один, а значит, при описании наследования в языке C++ нужно добавить зарезервированное слово virtual. Наследование классами Пятиугольник и Окружность признаков у класса Центрально-симметричная фигура должно происходить с открытым уровнем доступа, иначе при создании класса Рисунок мы не сможем запустить конструктор класса верхнего уровня. Наследование классом Рисунок признаков классов Пятиугольник и Окружность должно происходить закрыто, чтобы к методам этих классов нельзя было обратиться через объект класса Рисунок. К наследуемым признакам добавляется свойство "Цвет линии", значение которого будет храниться в классе Рисунок. В классе Рисунок, так же как и в классах Пятиугольник и Окружность, можно переопределить метод "Нарисовать". Этот метод выводит изображения на экран, в нем как раз и будет устанавливаться цвет линий, при котором будут рисоваться фигуры.

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

    Шаг 2. Уточнение классов с точным определением их зависимостей от других классов. Выясняется наследование и использование зависимостей.

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

    Шаг 3. Уточнение классов с определением наборов операций для каждого. Здесь анализируется потребность в конструкторах, деструкторах и операциях копирования. При этом принимается во внимание минимальность, полнота и удобство.

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

    • конструктор без параметров;

    • конструктор с параметрами (Радиус, x-координата, y-координата, Цвет);

    • метод вывода рисунка на экран;

    • деструктор для уничтожения создаваемых включенных объектов.

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

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

    • конструктор без параметров;

    • конструктор с параметрами (Радиус, x-координата, y-координата);

    • метод вывода пятиугольника на экран.

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

    • конструктор без параметров;

    • конструктор с параметрами (Радиус, x-координата, y-координата);

    • метод вывода окружности на экран.

    Шаг 4. Задание интерфейсов классов. Более точно определяются отношения классов. Методы разделяются на общие и защищенные. Определяются типы операций над классами.

    Классы Окружность и Пятиугольник должны содержать внутри себя переменные R, xc, yc, которые должны быть закрыты для доступа; функцию-член вывода фигуры на экран для доступа — открытую (как и конструкторы).

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

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

    8.9. ТЕХНОЛОГИЯ ПРОЕКТИРОВАНИЯ НА ОСНОВЕ ОБЯЗАННОСТЕЙ

    8.9.1. RDD-технология проектирования на основе обязанностей

    Далее будет изложена технология проектирования на основе обязанностей (или RDD-проектирование — Responsibility-Driven-Design), предложенная Т. Бадтом. Технология ориентирована на малые и средние проекты. Она основана на поведении систем. Данная технология по способу мышления аналогична разработке структуры служб какой-то организации: директора, заместителей директора, служб и подразделений.

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

    В качестве составной части этого процесса полезно изображать объекты с помощью CRC-карточек. Название CRC-карточки образовано от слов: Component, Responsibility, Collaborator — компонента (объект), обязанности, сотрудники. По мере того как для объектов выявляются обязанности, они записываются на лицевой стороне CRC-карточки (рис. 8.6).

    Рис. 8.6. Образец CRC-карточки


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

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

    8.9.2. Начинаем с анализа функционирования. Учебный пример объектно-ориентированного проекта средней сложности

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

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

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

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

    Анализ аналогов выявил, что уже известен ряд программных реализаций электронных поваренных книг с рецептами блюд. В данной области применения новой была бы программа, позволяющая планировать питание на заданный период. План питания на заданный период состоит из ежедневных планов питания с трех- или четырехразовым приемом пищи. Что надо учесть при разработке ежедневных планов питания? Число человек, калорийность питания каждого человека, любимые и нелюбимые блюда, затраты на питание. Ранние, описанные в литературе попытки оптимизации питания с учетом только продуктов, их калорийности и цен привели к решениям вида: оптимальный завтрак — 12 чашек уксуса. Генерация меню обеда с использованием датчика случайных чисел может привести к решениям с несовместимыми блюдами: молочный суп, сельдь с гороховым гарниром, квас. Решение проблемы — использование набора комплексных завтраков, обедов и ужинов. Есть ли в литературе достаточное описание возможных комплексов? Необходимо ли привлечь специалистов по питанию для разработки требуемого количества комплексов? Сколько будет стоить база данных комплексов? Следует ли реализовать функцию автоматической передачи заказа на продукты в магазин? На эти и другие вопросы необходимо дать ответ, чтобы уложиться в отпущенные средства и сроки.

    Как это обычно бывает при первоначальном описании многих программных систем, первичные спецификации весьма двусмысленны.

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

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

    2. Добавить новый рецепт в базу данных.

    3. Редактировать или добавить комментарий к существующему рецепту.

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

    5. Добавить новый комплекс в базу данных.

    6. Редактировать или добавить комментарий к существующему комплексу.

    7. Создать новый план питания.

    8. Пересмотреть существующий план в отношении некоторых дат, блюд и продуктов.

    Более детальное описание функций программы представлено на рис. 8.7.

    Рис. 8.7. Детальное описание функций программы


    Программа должна обеспечивать ведение базы данных (добавление, удаление и другие действия с отдельным рецептом или набором рецептов). Это, в общем-то, стандартные функции СУБД. Что касается функции планирования, то подразумевается, что программа по запросу пользователя будет составлять план питания на определенный период времени (неделю, месяц, год) для всей семьи или отдельных ее членов, исходя из заданных ограничений (например, ограничение на калорийность). После создания плана пользователю будет предоставлены следующие возможности:

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

    — получение списка продуктов, которые необходимо закупить на расчетный период;

    — осуществление распечатки данного плана питания или списка требуемых продуктов;

    — создание нового плана на данный период, но с другим ограничением (например, ограничение на продукты питания — церковный пост) или создание нового плана с тем же ограничением, но на другой период.

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

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

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

    • стандартные средства по ведению базы данных рецептов (просмотр, добавление, редактирование, удаление записей рецептов);

    • стандартные средства по ведению базы данных комплексов (просмотр, добавление, редактирование, удаление записей комплексов);

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

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

    • возможность вывода информации о составе продуктов (на экран, на принтер) как за весь период, так и по датам закупок, исходя из сроков хранения.

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

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

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

    Что такое план питания? План питания это список объектов DateList — дат. Дата это объект Date с включенными кулинарными рецептами, с соблюдением правил комплексов завтраков, обедов и ужинов.

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

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

    Вернемся к блоку Greeter (см. рис. 8.6). Планирование меню, как вы помните, было поручено объекту PlanManager. Пользователь должен иметь возможность сохранить существующий план. Следовательно, объект PlanManager может запускаться либо в результате открытия уже существующего плана питания, либо при создании нового. В последнем случае пользователя необходимо попросить ввести интервалы времени (список дат) для нового плана. Каждая дата ассоциируется с отдельным объектом типа Date. Пользователь может выбрать конкретную дату для детального исследования. В этом случае управление передается соответствующему объекту Date. Объект PlanManager должен уметь распечатывать меню питания на планируемый период. Наконец, пользователь может попросить объект PlanManager сгенерировать список продуктов на указанный период.

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

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

    Рис. 8.8. Схема статических связей между объектами программы РКП


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

    Изучив различные сценарии, команда разработчиков в конце решает, что все действия надлежащим образом могут быть распределены между семью объектами (рис. 8.8). Объект Greeter взаимодействует только с PlanManager и Recipe Database. Объект PlanManager "зацепляется" только с DateList, DateList с Date, a Date, в свою очередь, — с Meal. Объект Meal обращается к RecipeManager и через посредство этого объекта к конкретным рецептам (см. рис. 8.8).

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

    8.9.3. Динамическая модель системы

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

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

    Динамическая модель системы представляется диаграммой последовательности и диаграммой состояний объектов.

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

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

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

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

    Рис. 8.9. Пример диаграммы последовательности


    Рис. 8.10. Пример диаграммы состояний


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

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

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

    8.9.4. Уточнение классов с точным определением их зависимостей от других классов. Продолжение учебного примера

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

    Следует определить, как будет реализован каждый из объектов. Объект, характеризуемый только поведением (не имеющий внутреннего состояния — внутренних данных), может быть оформлен в виде функции. Например, объект, заменяющий в строке все прописные буквы на строчные, лучше представить в виде функции. Объекты со многими функциями лучше реализовать в виде классов. Каждой обязанности, перечисленной на CRC-карточке, присваивается имя. Эти имена станут затем названиями функций или процедур. Вместе с именами определяются типы аргументов, передаваемых функциям и процедурам. Затем описывается вся информация, содержащаяся внутри класса объекта. Если объекту требуются некоторые данные для выполнения конкретного задания, их источник (аргумент функции, глобальная или внутренняя переменная) должен быть описан явно.

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

    Идея классификации классов объектов программы через их поведение имеет чрезвычайное следствие. Программист знает, как использовать объект, разработанный другим программистом, и при этом ему нет необходимости знать, как он реализован. Пусть классы шести объектов РКП разрабатываются шестью программистами. Программист, разрабатывающий класс объекта Meal, должен обеспечить просмотр базы данных с рецептами и выбор отдельного рецепта при составлении блюда. Для этого объекта Meal просто вызывает функцию browse, привязанную к объекту RecipeDatebase. Функция browse возвращает отдельный рецепт Recipe из базы данных. Все это справедливо вне зависимости от того, как конкретно реализован внутри Recipe Database просмотр базы данных.

    Рис. 8.11. Уточненная CRC-карточка


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

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

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

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

    8.9.5. Совместное рассмотрение трех моделей

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

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

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

    8.10. ПРИМЕР РЕТРОСПЕКТИВНОЙ РАЗРАБОТКИ ИЕРАРХИИ КЛАССОВ БИБЛИОТЕКИ ВИЗУАЛЬНЫХ КОМПОНЕНТ DELPHI И C++ BUILDER

    Delphi и C++ Builder представляет собой визуальное средство разработки корпоративных информационных систем. В C++ Builder используется язык объектно-ориентированного программирования C++, а в Delphi — Object Pascal. Несмотря на это, обе среды используют одни и те же модули библиотеки визуальных компонент, написанных на Object Pascal.

    Каждый тип органов управления системы описывается классом, а помещаемые на формы конкретные органы управления являются объектами соответствующих классов. Так, например, Button1, Button2…, ButtonN являются объектами класса TButton; Edit1, Edit2…, EditM — объектами класса TEdit и т. п. Когда пользователь создает форму в визуальной интегрированной среде, он, по сути (в отличие от других органов управления), создает новый класс, объектом которого будет форма, появляющаяся при выполнении приложения (например, класс — TForm1, объект класса — Form1).

    С целью уяснения процессов разработки иерархии классов предпримем попытку ретроспективного анализа иерархии классов системы Delphi/C++ Builder.

    В процессе анализа была расписана иерархия классов, избранных для примера органов управления, выделены некоторые обязанности, которые мог бы наложить на них разработчик, а затем на основе сравнения списков выделенных обязанностей предпринята попытка обосновать иерархию классов, принятую в средах Delphi/C++ Builder.

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

    Рассмотрим органы управления, которые могут быть получены транспортировкой их при помощи мыши с палитры компонент Delphi/C++ Builder.

    — TButton — обыкновенная кнопка;

    — TRadioButton — радиокнопка (группа кнопок с зависимой фиксацией, обеспечивающей возможность выбора лишь одной кнопки из группы);

    — TListBox — обычный список;

    — TDBListBox — список для работы с таблицами данных;

    — TDataSource — источник данных (является посредником между элементами DataAccess: Table, Query, — и органами управления базами данных DataControls: DBGrid, DBEdit и т. п.).

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

    Обязанности объектов класса TDataSource:

    — контролировать доступ пользователя к элементам TDataSet;

    — обеспечивать возможность определения, подключен ли TDataSource к некоторому элементу TDataSet;

    — обеспечивать возможность работы с дочерними компонентами;

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

    — обеспечивать возможность уничтожения объекта с высвобождением памяти;

    — обеспечивать возможность посылать сообщения;

    — определять имя класса, объектом которого является данный элемент.

    Обязанности объектов класса TButton:

    — обрабатывать сообщения WMLBUTTONDOWN и WMLBUTTONDBLCLK (нажатие и двойное нажатие левой кнопки мыши);

    — программно эмулировать нажатие кнопки;

    — обрабатывать сообщения от клавиатуры;

    — получать фокус ввода;

    — определять, имеется ли фокус ввода;

    — определять, может ли объект иметь фокус ввода (например, если элемент невидим, ему нельзя передать фокус ввода);

    — обрабатывать сообщение BMCLICK (происходит после нажатия кнопки мыши);

    — становиться видимым и невидимым;

    — перерисовываться;

    — обеспечивать возможность перевода точки из системы координат окна в систему координат экрана;

    — хранить идентификатор родителя (с возможностью изменить родителя);

    — обеспечивать возможность работы с дочерними компонентами;

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

    — обеспечивать возможность уничтожения объекта с высвобождением памяти;

    — обеспечивать возможность посылать сообщения;

    — определять имя класса, объектом которого является данный элемент.

    Обязанности объектов класса TRadioButton:

    — обрабатывать сообщения WMLBUTTONDOWN и WMLBUTTONDBLCLK (нажатие и двойное нажатие левой кнопки мыши);

    — определять, какая выбрана кнопка из группы кнопок;

    — обрабатывать сообщения от клавиатуры;

    — получать фокус ввода;

    — определять, имеется ли фокус ввода;

    — определять, может ли объект иметь фокус ввода (например, если элемент невидим, ему нельзя передать фокус ввода);

    — обрабатывать сообщение BMCLICK. (происходит после нажатия кнопки мыши);

    — становиться видимым и невидимым;

    — перерисовываться;

    — обеспечивать возможность перевода точки из системы координат окна в систему координат экрана;

    — хранить идентификатор родителя (с возможностью изменить родителя);

    — обеспечивать возможность работы с дочерними компонентами;

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

    — обеспечивать возможность уничтожения объекта с высвобождением памяти;

    — обеспечивать возможность посылать сообщения;

    — определять имя класса, объектом которого является данный элемент.

    Обязанности объектов класса TListBox:

    — очищать список;

    — обеспечивать возможность нахождения нескольких элементов из списка;

    — определять номер элемента списка по координатам точки, принадлежащей объекту класса TListBox;

    — обрабатывать сообщения от клавиатуры;

    — получать фокус ввода;

    — определять, имеется ли фокус ввода;

    — определять, может ли объект иметь фокус ввода (например, если элемент невидим, ему нельзя передать фокус ввода);

    — обрабатывать сообщение BM_CLICK (происходит после нажатия кнопки мыши);

    — становиться видимым и невидимым;

    — перерисовываться;

    — обеспечивать возможность перевода точки из системы координат окна в систему координат экрана;

    — хранить идентификатор родителя (с возможностью изменить родителя);

    — обеспечивать возможность работы с дочерними компонентами;

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

    — обеспечивать возможность уничтожения объекта с высвобождением памяти;

    — обеспечивать возможность посылать сообщения;

    — определять имя класса, объектом которого является данный элемент.

    Обязанности объектов класса TDBListBox:

    — обеспечивать связь с источником данных (TDataSource);

    — очищать список;

    — обеспечивать возможность вывода нескольких элементов из списка;

    — определять номер элемента списка по координатам точки, принадлежащей объекту класса TDBListBox;

    — обрабатывать сообщения от клавиатуры;

    — получать фокус ввода;

    — определять, имеется ли фокус ввода;

    — определять, может ли объект иметь фокус ввода (например, если элемент невидим, ему нельзя передать фокус ввода);

    — обрабатывать сообщение BMCLICK (происходит после нажатия кнопки мыши);

    — становиться видимым и невидимым;

    — перерисовываться;

    — обеспечивать возможность перевода точки из системы координат окна в систему координат экрана;

    — хранить идентификатор родителя (с возможностью изменить родителя);

    — обеспечивать возможность работы с дочерними компонентами;

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

    — обеспечивать возможность уничтожения объекта с высвобождением памяти;

    — обеспечивать возможность посылать сообщения;

    — определять имя класса, объектом которого является данный элемент.

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

    Логично было бы ввести дополнительные классы, объекты которых имели бы общие для рассмотренных классов обязанности. Тогда рассмотренные классы (TDataSource, TButton, TRadioButton, TListBox, TDBListBox) могли бы унаследовать функции, обеспечивающие выполнение этих обязанностей у введенных дополнительных классов (в объектно-ориентированном программировании имеется механизм, который так и называется — механизм наследования, — который делает доступными из дочерних классов свойства и методы, с учетом прав доступа, разумеется, из родительских (или базовых) классов).

    Попытаемся выделить упомянутые классы и назначить им их обязанности:

    Обязанности объектов класса Класс_1:

    — обеспечивать возможность работы с дочерними компонентами;

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

    — обеспечивать возможность уничтожения объекта с высвобождением памяти;

    — обеспечивать возможность посылать сообщения;

    — определять имя класса, объектом которого является данный элемент.

    Обязанности объектов класса Класс_2:

    — обрабатывать сообщения от клавиатуры;

    — получать фокус ввода;

    — определять, имеется ли фокус ввода;

    — определять, может ли объект иметь фокус ввода (например, если элемент невидим, ему нельзя передать фокус ввода);

    — обрабатывать сообщение BM_CLICK (происходит после нажатия кнопки мыши);

    — становиться видимым и невидимым;

    — перерисовываться;

    — обеспечивать возможность перевода точки из системы координат окна в систему координат экрана;

    — хранить идентификатор родителя (с возможностью изменить родителя).

    Обязанность объектов класса Класс_3:

    — обрабатывать сообщения WM_LBUTTONDOWN и WM_LBUTTONDBLCLK (нажатие и двойное нажатие левой кнопки мыши).

    Обязанности объектов класса Класс_4:

    — очищать список;

    — обеспечивать возможность нахождения нескольких элементов из списка;

    — определять номер элемента списка по координатам точки, принадлежащей объекту класса TDBListBox.

    Теперь, исключив из множества обязанностей объектов рассматриваемых классов те, которые переданы дополнительным классам, перечислим оставшиеся обязанности:

    Обязанности класса TDataSource:

    — контролировать доступ пользователя к элементам TDataSet;

    — обеспечивать возможность определения, подключен ли TDataSource к некоторому элементу TDataSet.

    Итак, обязанности класса Tbutton — программно эмулировать нажатие кнопки; класса TradioButton — определять, какая из кнопок с зависимой фиксацией выбрана; классов TlistBox и TDBListВох — обеспечивать связь с источником данных (TDataSource).

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

    Теперь рассмотрим на рис. 8.13 фрагмент схемы иерархии классов для перечисленных элементов (до корневого суперкласса).

    Сравнивая рисунки 8.12 и 8.13, можно заметить:

    1. Класс_1 в нашей иерархии соответствует ветви TObject → TPersistent → TComponent;

    2. Класс_2 — TControl —> TWinControl;

    3. Класс_3 — TbuttonControl;

    4. Класс_4 — TCustomListBox.

    Если с двумя последними классами все понятно, то возникает вопрос: почему первые два класса нашей иерархии соответствуют не одному, а целым цепочкам классов C++ Builder? Дело в том, что в рассматриваемом примере описаны не все классы C++ Builder. Поэтому те из них, которые приводят к разветвлению двух первых цепочек, здесь просто не учтены. Например, элемент TImage, предназначенный для расположения на формах графических изображений, имеет следующую цепочку наследования классов: TObject → TPersistent → TComponent → TControl → TGraphicsControl, — т. е. цепочка TControl → TWinControl превращается в дерево, на котором классы TWinControl и TGraphicsControl оказываются на одном уровне. Данный фрагмент схемы иерархии классов изображен на рис. 8.14.

    Рис. 8.12. Предварительная иерархия классов


    Рис. 8.13. Иерархия некоторых классов C++ Builder


    Рис. 8.14. Фрагмент схемы иерархии


    8.11. АЛЬТЕРНАТИВНЫЙ ПРОЕКТ ГРАФИЧЕСКОГО ИНТЕРФЕЙСА

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

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

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

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

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

    Контейнеры бывают двух типов: однородные (динамические) и разнородные (статические).

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

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

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

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

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

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

    Рассмотрим пример проекта с использованием контейнеров. Предположим, что перед вами стоит задача разработки графического интерфейса, аналогичного GUI Microsoft Windows. Аналогичный интерфейс создавали разработчики Delphi, и ранее мы ретроспективно выполняли данный проект.

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

    Прежде всего вы определяете все многообразие элементов GUI: labels, shapes, edit fields, buttons, check radio buttons, list combo boxes, bitmap и т. д. Несложно заметить, что большинство элементов представляет собой простые комбинации из двух или более визуальных элементов: например строка и рамка. Интуитивно понятно, что визуальный элемент и элемент интерфейса — это не одно и то же. Главной функцией элемента интерфейса является получение информации от пользователя, в то время как визуальный элемент служит для ее (информации) отображения. Это важно.

    Теперь раздробим нашу команду на четыре подкоманды.

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

    Вторая команда должна специфицировать иерархию элементов интерфейса.

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

    И наконец, функцией четвертой команды будет создание иерархии объектов ввода-вывода (клавиатура, мышь, дисплей и т. д.).

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

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

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

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

    Что же теперь имеем? Предположим, что надоели прямоугольные кнопки и захотелось круглых, многоугольных или вообще произвольных кнопок. "Ну, уж нет", — сказал бы специалист по множественному наследованию. Но мы спросим: "Вам в runtime или специально настроить?" Действительно, любой наследник от плоской фигуры может быть подставлен в контейнер в любое время, включая время выполнения. И тут вы с удивлением замечаете, что можно считать проект готовым к употреблению, отладив его схемы взаимодействия всего на одном-двух реальных объектах и добавляя все остальное по мере необходимости.

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

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

    8.12. ПРОЕКТ АСУ ПРЕДПРИЯТИЯ

    Развивая идею использования контейнеров А. Усова, можно получить идею системы генерации все новых программ с используемыми "кубиками" — готовыми объектами, которые при формировании программы автоматически извлекаются объектно-ориентированной СУБД из базы данных объектов.

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

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

    Каждое предприятие имеет, как правило, иерархическую структуру подразделений. Структурное подразделение (СП) включает в себя три информационных класса: служащие, оборудование и материалы. Здесь под оборудованием будут пониматься основные фонды предприятия или данного СП. Термином "материалы" обозначаются те сущности, которые потребляются в процессе производства. Базовые информационные классы — служащие, оборудование и материалы — могут иметь общий суперкласс (НЕЧТО, УЧАСТВУЮЩЕЕ В ПРОИЗВОДСТВЕ) или нет (дело вкуса).

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

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

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

    Здесь же можно отметить, что современная теория управления предприятиями базируется на BPR (bussiness process re-engineering) и TQM (total quality managment). Одно из основных положений BPR говорит о необходимости переноса точки принятия тактических решений как можно ближе к исполнителям, т. е. СП должно быть в максимальной степени самостоятельным, самодостаточным и компетентным в принятии решений.

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

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

    Рис. 8.15. Производственный цикл промышленного предприятия


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

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

    Система поддерживает произвольное количество логических слоев (аналог — многоуровневые системы клиент — сервер). Слой хранения информации представлен средой хранения (СУБД), слой отображения — средой отображения, основанной на GUI (пользовательскими приложениями), слой бизнес правил — схемами и т. д.

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

    8.13. ОБЗОР ОСОБЕННОСТЕЙ ПРОЕКТОВ ПРИКЛАДНЫХ СИСТЕМ

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

    — системы пакетной обработки — обработка данных производится один раз для каждого набора входных данных;

    — системы непрерывной обработки — обработка данных производится непрерывно над сменяющимися входными данными;

    — системы с интерактивным интерфейсом — системы, управляемые внешними воздействиями;

    — системы динамического моделирования — системы, моделирующие поведение объектов внешнего мира;

    — системы реального времени — системы, в которых преобладают строгие временные ограничения;

    — системы управления транзакциями — системы, обеспечивающие сортировку и обновление данных; имеют коллективный доступ (типичной системой управления транзакциями является СУБД).

    При разработке системы пакетной обработки необходимо выполнить следующие шаги:

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

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

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

    При разработке системы непрерывной обработки необходимо выполнить следующие шаги:

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

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

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

    При разработке системы с интерактивным интерфейсом необходимо выполнить следующие шаги:

    — выделяем объекты, формирующие интерфейс;

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

    — структуру программы определяем по ее динамической модели, а для реализации интерактивного интерфейса используем параллельное управление (многозадачный режим) или механизм со-

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

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

    При разработке системы динамического моделирования необходимо выполнить следующие шаги:

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

    — определяем дискретные события; такие события соответствуют дискретным взаимодействиям объекта (например, включение питания) и реализуются как операции объекта;

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

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

    Разработка системы реального времени аналогична разработке системы с интерактивным интерфейсом.

    При разработке системы управления транзакциями необходимо выполнить следующие шаги:

    — отобразить объектную модель на базу данных;

    — определить асинхронно работающие устройства и ресурсы с асинхронным доступом; в случае необходимости определить новые классы;

    — определить набор ресурсов (в том числе структур данных), к которым необходим доступ во время транзакции (участники транзакции);

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

    8.14. ГИБРИДНЫЕ ТЕХНОЛОГИИ ПРОЕКТИРОВАНИЯ

    8.14.1. Игнорирование классов

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

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

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

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

    Рис. 8.16. Игнорирование классов и их наследования


    8.14.2. Игнорирование наследования

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

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

    8.14.3. Игнорирование статического контроля типов

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

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

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

    На раннем этапе проектирования вполне достаточно простого утверждения:

    Рис. 8.17. Игнорирование статического контроля типов


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

    8.14.4. Гибридный проект

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

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

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

    Рис. 8.18. Гибридный проект


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

    ВЫВОДЫ

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

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

    — уменьшение сложности программного обеспечения;

    — повышение надежности программного обеспечения;

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

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

    • Методы объектно-ориентированного проектирования используют в качестве строительных блоков объекты.

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

    • Наследование выполняет в ООП несколько важных функций:

    — моделирует концептуальную структуру предметной области;

    — экономит описания, позволяя использовать их многократно для задания разных классов;

    — обеспечивает пошаговое программирование больших систем путем многократной конкретизации классов.

    • Классы из предметной (прикладной) области непосредственно отражают понятия, которые использует конечный пользователь для описаний своих задач и методов их решения.

    • Идеальный класс должен в минимальной степени зависеть от остального мира. Каждый класс имеет набор поведений и характеристик, которые его определяют.

    • При перестройке иерархии классов применяются четыре процедуры:

    1) расщепление класса на два и более;

    2) абстрагирование (обобщение);

    3) слияние;

    4) анализ возможности использования существующих разработок.

    • Разработка проекта начинается с составления функциональной модели.

    • Объектная модель представляет статическую структуру проектируемой системы (подсистемы).

    • Динамическая модель системы представляется диаграммой последовательности и диаграммой состояний объектов.

    Контрольные вопросы

    1. При решении каких проблем лучше использовать объектно-ориентированный подход?

    2. Какие характеристики являются фундаментальными в объектно-ориентированном мышлении?

    3. На каких принципах базируется объектная модель?

    4. Что такое паттерн проектирования?

    5. Какому паттерну соответствует динамический и статический контейнер А. Усова?

    6. Какие преимущества дает объектная модель?

    7. В чем заключаются преимущества инкапсуляции?

    8. В чем заключается важность наследования?

    9. Для чего полезен полиморфизм?

    10. Что такое агрегирование объекта?

    11. Из каких этапов состоит процесс построения объектной модели?

    12. Каким образом взаимодействуют между собой объекты в программе?

    13. Какие процедуры применяются при перестройке схемы наследования классов?

    14. Почему так важен анализ функционирования системы?

    15. В чем заключается удобство использования CRC-карточек?

    16. Какие диаграммы используют в проектах средней сложности?









    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх