6.3. ОСНОВНЫЕ ПОНЯТИЯ ОБЪЕКТНО-ОРИЕНТИРОВАННОЙ ТЕХНОЛОГИИ
С чего же начинается создание объектно-ориентированной программы?
Конечно, с объектно-ориентированного анализа (ООА — objest-oriented analysis), который направлен на создание моделей реальной действительности на основе объектно-ориентированного мировоззрения. Объектно-ориентированный анализ (ООА) — это методология, при которой требования к системе воспринимаются с точки зрения классов и объектов, прагматически выявленных в предметной области.
На результатах ООА формируются модели, на которых основывается объектно-ориентированное проектирование (object-oriented design, OOD).
Объектно-ориентированное проектирование (ООП) — это методология проектирования, соединяющая в себе процесс объектной декомпозиции и приемы представления логической и физической, а также статической и динамической моделей проектируемой системы.
Что же такое объектно-ориентированное программирование (ООПр) (object-oriented programming)? Программирование прежде всего подразумевает правильное и эффективное использование механизмов конкретных языков программирования. Объектно-ориентированное программирование — это процесс реализации программ, основанный на представлении программы в виде совокупности объектов. ООПр предполагает, что любая функция (процедура) в программе представляет собой метод объекта некоторого класса, причем класс должен формироваться в программе естественным образом, как только в программе возникает необходимость описания новых физических предметов или их абстрактных понятий (объектов программирования). Каждый новый шаг в разработке алгоритма также должен представлять собой разработку нового класса на основе уже существующих классов, т. е. технология ООПр иначе может быть названа как программирование «от класса к классу».
Можно ли реализовать объектно-ориентированную программу не на объектно-ориентированных языках? Ответ, скорее всего, положителен, хотя придется преодолеть ряд трудностей. Ведь главное, что требуется, — это реализовать объектную модель. Сокрытие информации при использовании обычных языков, в принципе, можно реализовать сокрытием доступности вызовов подпрограмм в файлах (Unit). Инкапсуляцию объектов можно достичь как и в объектно-ориентированных языках написанием отдельных подпрограмм. Далее можно считать, что каждый объект порождается от своего уникального класса. Конечно, иерархии классов в таком проекте не будет и для достижения параллелизма придется писать код для организации вызова к исполнению как бы сразу нескольких копий процедур, но программа при этом будет вполне объектно-ориентированной.
6.4. ОСНОВНЫЕ ПОНЯТИЯ, ИСПОЛЬЗУЕМЫЕ В ОБЪЕКТНО-ОРИЕНТИРОВАННЫХ ЯЗЫКАХ
Класс в одном из значений этого термина обозначает тип структурированных данных.
Объект — это структурированная переменная типа класс. Каждый объект является представителем (экземпляром) определенного класса. В программе может быть несколько объектов, являющихся экземплярами одного и того же класса. Все объекты — экземпляры данного класса — аналогичны друг другу, поскольку имеют одинаковый интерфейс, один и тот же набор операций (методов) и полей, определяемых в их классе. Интерфейс класса иногда называют особенностями класса.
Класс является описанием того, как будет выглядеть и вести себя его представитель. Обычно проектируют класс как образование (матрицу), отвечающее за создание своих новых представителей (экземпляров или объектов). Экземпляр объекта создается при помощи особого метода класса, называемого конструктором, так как необходимо создать экземпляр, прежде чем он станет активным и начнет взаимодействовать с окружающим миром. Уничтожение экземпляров поддерживает сам активный экземпляр, имеющий соответствующий метод — деструктор.
Объект — это структурированная переменная типа класс, содержащая всю информацию о некотором физическом предмете или реализуемом в программе понятии.
Объект — это логическая единица, которая содержит данные и правила (методы с кодом алгоритма) (см. рис. 1.8). Другими словами, объект — это расположенные в отдельном участке памяти:
— порция данных объекта или атрибуты исходных данных, называемые еще полями, членами данных (data members), значения которых определяют текущее состояние объекта;
— методы объекта (methods, в разных языках программирования еще называют подпрограммами, действиями, member functions или функциями-членами), реализующие действия (выполнение алгоритмов) в ответ на их вызов в виде переданного сообщения;
— часть методов, называемых свойствами (property), которые, в свою очередь, определяют поведение объекта, т. е. его реакцию на внешние воздействия (в ряде языков программирования свойства оформляются особыми операторами).
Объекты в программах воспроизводят все оттенки явлений реального мира: «рождаются» и «умирают», меняют свое состояние; запускают и останавливают процессы; «убивают» и «возрождают» другие объекты.
Объявления классов определяют уже описанные три характеристики объектов: поля объекта, методы объекта, свойства объектов. Также в объявлениях может указываться предок данного класса.
В соответствии с описанием класса внутри объекта данные и методы могут быть как открытыми по интерфейсу public, так и сокрытыми private.
Во время выполнения программы объекты взаимодействуют друг с другом посредством вызова методов вызываемого объекта — в этом и заключается передача сообщений. Для того чтобы объект послал сообщение другому объекту, в большинстве языков программирования требуется после указания имени вызываемого объекта записать вызов подпрограммы (метода) с соответствующим именем и указанием необходимых фактических параметров (аргументов). Получив сообщение, объект-получатель начинает выполнять код вызванной подпрограммы (метода) с полученными значениями аргументов. Таким образом, функционирование программы (выполнение всего алгоритма программы) осуществляется последовательным вызовом методов от одного объекта к другому.
Хотя можно получить прямой доступ к полям объекта, использование такого подхода не поощряется. Одно из больших преимуществ ООПр — это инкапсуляция, предназначенная для разрешения работы с данными в полях объектов только через сообщения. Для реализации методов обработки таких сообщений используются свойства. Свойства — это особым образом оформленные методы, предназначенные как для чтения и контролируемого изменения внутренних данных объекта (полей), так и выполнения действий, связанных с поведением объекта.
Так, например, если в заданном месте экрана уже отображена какая-то строка и мы хотим изменить положение строки на экране, то мы посылаем объекту новое значение свойства в виде набора нужных координат. Далее свойство автоматически трансформируется в вызов метода, который изменит значение поля координат отображения строки и выполнит действия по уничтожению изображения строки на прежнем месте экрана, а также по отображению строки в новом месте экрана.
Можно выделить несколько преимуществ инкапсуляции.
Преимущество 1. Надежность данных. Можно предотвратить изменение элемента данных, выполнив в свойстве (методе) дополнительную проверку значения на допустимость. Тем самым можно гарантировать надежное состояние объекта.
Преимущество 2. Целостность ссылок. Перед доступом к объекту, связанному с данным объектом, можно удостовериться, что косвенное поле содержит корректное значение (ссылку на экземпляр).
Преимущество 3. Предусмотренные побочные эффекты. Можно гарантировать, что каждый раз, когда выполняется обращение к полю объекта, синхронно с ним выполняется какое-либо специальное действие.
Преимущество 4. Сокрытие информации. Когда доступ к данным осуществляется только через методы, можно скрыть детали реализации объекта. Позднее, если реализация изменится, придется изменить лишь реализацию методов доступ а к полям. Те же части программы, которые использовали этот класс, не будут затронуты.
Весьма удобно рассматривать объекты как попытку создания активных данных. Смысл, вкладываемый в слова «объект представляет собой активные данные», основан на объектно-ориентированной парадигме выполнения операций, состоящей в посылке сообщений. В посылаемых объекту сообщениях указывается, что мы хотим, что бы он выполнил. Так, например, если мы хотим вывести на экране строку, то мы посылаем объекту строки сообщение, чтобы он изобразил себя. В этом случае строка — это уже не пассивный кусок текста, а активная единица, знающая, как правильно производить над собой различные действия.
Классы могут быть связаны между собой разными способами. Одна из фундаментальных концепций ООП — это понятие наследования классов, устанавливающее между двумя классами отношения «родитель-потомок».
Наследование — отношение самого высокого уровня и играет важную роль на стадии проектирования. Наследование — это определение класса и затем использование его для построения иерархии производных классов, причем каждый класс-потомок наследует от класса-предка интерфейс всех классов-предков в виде доступа к коду их методов и данным. При этом, возможно, переопределение или добавление как новых данных, так и методов.
Класс-предок — это класс, предоставляющий свои возможности и характеристики другим классам через механизм наследования. Класс, который использует характеристики другого класса посредством наследования, называется его классом-потомком.
Итак, наследование проявляется в том, что любой класс-потомок имеет доступ или, другими словами, наследует практически все ресурсы (методы, поля и свойства) родительского класса и всех предков до самого верхнего уровня иерархии.
Рассмотрим, как информация, содержащаяся в классе-потомке, может переопределять информацию, наследуемую от предков. Очень часто при реализации такого подхода метод, соответствующий подклассу, имеет то же имя, что и соответствующий метод в родительском классе.
При этом для поиска метода, подходящего для обработки сообщения, используется следующее правило. Поиск метода, который вызывается в ответ на определенное сообщение, начинается с методов, принадлежащих классу получателя. Если подходящий метод не найден, то поиск продолжается до родительского класса. Поиск продвигается вверх по цепочке родительских классов до тех пор, пока не будет найден нужный метод или пока не будет исчерпана последовательность родительских классов. В первом случае выполняется найденный метод, во втором выдается сообщение об ошибке. Во многих языках программирования уже на этапе компилирования, а не при выполнении программы определяется, что подходящего метода нет вообще и выдается сообщение об ошибке.
Семантически наследование описывает отношение типа «is-а». Например, медведь есть млекопитающее, дом есть недвижимость и «быстрая сортировка» есть сортирующий алгоритм. Таким образом, наследование порождает иерархию «обобщение — специализация», в которой подкласс представляет собой специализированный частный случай своего суперкласса. «Лакмусовая бумажка» наследования — обратная проверка: так, если В не есть А, то В не стоит производить от А.
Повторное использование — это использование в программе класса для создания экземпляров или в качестве базового для создания нового класса, наследующего часть или все характеристики родителя. Порождая классы от базовых, вы эффективно повторно используете код базового класса для собственных нужд. Повторное использование сокращает объем кода, который необходимо написать и оттестировать при реализации программы, что сокращает объемы труда.
Таким образом, наследование выполняет в ООП несколько важных функций:
~ моделирует концептуальную структуру предметной области;
сэкономит описания, позволяя использовать их многократно для задания разных классов;
~ обеспечивает пошаговое программирование больших систем путем многократной конкретизации классов.
Ряд языков, например Object Pascal, описание которого дается в приложении 4, поддерживает модель наследования, известную как простое наследование и которая ограничивает число родителей конкретного класса одним. Другими словами, определенный пользователем класс имеет только одного родителя. Схема иерархии классов в этом случае представляет собой ряд одиночно стоящих деревьев (hierarchical classification).
Более мощная модель сложного наследования, называемая множественным наследованием, в которой каждый класс может, в принципе, порождаться от одного или сразу от нескольких родительских классов, наследуя поведение всех своих предков, в Object Pascal не поддерживается, но поддерживается в Visual С++ и ряде других языков. При множественном наследовании составляется уже не схема иерархии, а сеть, которая может включать деревья со сросшимися кронами.
Обычно если объекты соответствуют конкретным сущностям реального мира, то классы являются абстракциями, выступающими в роли понятий. Между классами, как между понятиями, существует иерархическое отношение конкретизации, связывающее класс с классом-потомком. Это отношение реализуется в системах ООП механизмом наследования. Наследование — это способность одного класса использовать характеристики другого.
Наследование позволяет практически без ограничений последовательно строить и расширять классы, созданные вами или кем-то еще. Начиная с самых простых классов можно создавать производные классы по возрастающей сложности, которые не только легки при отладке, но и просты по внутренней структуре.
Множественное наследование многие аналитики считают «вредным» механизмом, приводящим к сложно разрешимым проблемам проектирования и реализации. В языках с отсутствующим множественным наследованием целей множественного наследования достигают агрегированием объектов с дополнительным делегированием полномочий.
На агрегировании основана работа таких систем визуального программирования, как Delphi, С++ Builder. В этих системах имеется порождающий объект пользователя класс-форма (пустое окно Windows). Системы обеспечивают подключение к форме через указатели нужных пользователю объектов, например кнопок, окон редакторов и т. д. При перерисовке формы на экране монитора как бы одновременно с ней перерисовываются изображения агрегированных объектов. Более того, при активизации формы агрегированные объекты также становятся активными: кнопки начинают нажиматься, а в окна редакторов можно начинать вводить информацию.
Одним из базовых понятий технологии ООП является полиморфизм. Термин «полиморфизм» имеет греческое происхождение и означает приблизительно «много форм» (poly — много, morphos — форма).
Полиморфизм — это средство для придания различных значений одному и тому же событию в зависимости от типа обрабатываемых данных, т. е. полиморфизм определяет различные формы реализации одноименного действия (см. рис. 6.2.).
Целью полиморфизма применительно к объектно-ориентированному программированию является использование одного имени для задания общих для класса действий, причем каждый объект имеет возможность по-своему реализовать это действие своим собственным, подходящим для него кодом.
Полиморфизм является предпосылкой для расширяемости объектно-ориентированных программ, поскольку он предоставляет способ старым программам воспринимать новые типы данных, которые не были определены во время написания программы.
Противоположность полиморфизму называется мономорфизмом; он характерен для языков с сильной типизацией и статическим связыванием (Ada).
В более общей трактовке полиморфизм — это способность объектов, принадлежащих к разным типам, демонстрировать одинаковое поведение; способность объектов, принадлежащих к одному типу, демонстрировать разное поведение.
Рассмотрим «вырожденный пример» полиморфизма. В MS DOS есть понятие «номер прерывания», за которым скрывается адрес в памяти. Поместите в ту же ячейку другой адрес — и программы начнут вызывать процедуру с другим «именем» и из другого модуля. Как видно из примера, принцип полиморфизма можно реализовать и не в объектно-ориентированных программах.
Ряд авторов книг по теории объектно-ориентированного проектирования соотносят термин «полиморфизм» с разными понятиями, например понятием перегрузки; для обозначения одного двух или большего количества механизмов полиморфизма; чистого полиморфизма.
Перегрузка функций. Одним из применений полиморфизма в С++ является перегрузка функций. Она дает одному и тому же имени функции различные значения. Например, выражение а + b имеет различные значения, в зависимости от типов переменных а и b (допустим, если это числа, то «+» означает сложение, а если строки, — то склейку этих строк или вообще сложение комплексных чисел, если а и b комплексного типа). Перегрузка оператора «+» для типов, определяемых пользователем, позволяет использовать их в большинстве случаев так же, как и встроенные типы. Двум или более функциям (операция — это тоже функция) может быть дано одно и то же имя. Но при этом функции должны отличаться сигнатурой (либо типами параметров, либо их числом).
Полиморфный метод в С++ называется виртуальной функцией, позволяющей получать ответы на сообщения, адресованные объектам, точный вид которых неизвестен. Такая возможность является результатом позднего связывания. При позднем связывании адреса определяются динамически во время выполнения программы, а не статически во время компиляции как в традиционных компилируемых языках, в которых применяется раннее связывание. Сам процесс связывания заключается в замене виртуальных функций на адреса памяти.
Виртуальные методы определяются в родительском классе, а в производных классах происходит их до определение и для них создаются новые реализации. Основой виртуальных методов и динамического полиморфизма являются указатели на производные классы. При работе с виртуальными методами сообщения передаются как указатели, которые указывают на объект вместо прямой передачи объекту.
Практический смысл полиморфизма заключается в том, что он позволяет посылать общее сообщение о сборе данных любому классу, причем и родительский класс, и классы-потомки ответят на сообщение соответствующим образом, поскольку производные классы содержат дополнительную информацию. Программист может сделать регулярным процесс обработки несовместимых объектов различных типов при наличии у них такого полиморфного метода.
6.5. ЭТАПЫ И МОДЕЛИ ОБЪЕКТНО-ОРИЕНТИРОВАННОЙ ТЕХНОЛОГИИ
Почему в начале процесса проектирования работу начинают с анализа функционирования или поведения системы? Дело в том, что поведение системы обычно известно задолго до остальных ее свойств. Программа должна выполнять набор действий, согласно выявленным ее функциям. Процесс разработки модели в форме функциональной спецификации уже был изложен ранее в гл. 3.
Объектно-ориентированная технология создания программ основывается на так называемом объектном подходе. Одним из проявлений этого подхода является то, что сначала довольно долго создаются и оптимизируются объектная модель и иные модели и лишь затем осуществляется кодирование.
Обычно проектируемая программная система первоначально представляется в виде трех взаимосвязанных моделей:
1) объектной модели, которая представляет статические, структурные аспекты системы;
2) динамической модели, которая описывает работу отдельных частей системы;
3) функциональной модели, в которой рассматривается взаимодействие отдельных частей системы (как по данным, так и по управлению) в процессе ее работы.
Эти три вида моделей должны позволить рассматривать три взаимно-ортогональных представления системы в одной системе обозначений.
Объектная модель на более поздних этапах проектирования дополняется моделями, отражающими как логическую (классы и объекты), так и физическую структуру системы (процессы и деление на компоненты, файлы или модули).
Поскольку при разработке объектно-ориентированного проекта используется множество моделей, которые необходимо увязать в единое целое, далее в гл. 8 рассматриваются средства автоматизации составления, верификации (проверки) и графической визуализации этих моделей.
Процесс построения объектной модели включает в себя следующие, возможно, повторяющиеся до достижения приемлемого качества модели этапы:
1) определение объектов;
2) подготовку словаря объектов с целью исключения схожих (синонимичных) понятий и уточнения имен, классификацию объектов, выделение классов;
3) определение взаимосвязей между объектами;
4) определение атрибутов объектов и методов (определенна уровней доступа и проектирование интерфейсов классов);
5) исследование качества модели.
Теперь, используя функциональную модель, можно начинать: работу с динамической моделью, наделяя объекты необходимыми методами и данными.
Модели, разработанные на первой фазе жизненного цикла системы, продолжают использоваться на всех последующих его фазах, облегчая программирование системы, ее отладку и тестирование, сопровождение и дальнейшую модификацию.
Объектная модель описывает структуру объектов, составляющих систему, их атрибуты, операции, взаимосвязи с другими объектами. В объектной модели должны быть отражены те понятия и объекты реального мира, которые важны для разрабатываемой системы. В объектной модели отражается прежде всего прагматика разрабатываемой системы, что выражается в использовании терминологии прикладной области, связанной с использованием разрабатываемой системы.
Прагматика определяется целью разработки программной системы: для обслуживания покупателей железнодорожных билетов, управления работой аэропорта, обслуживания чемпионата мира по футболу и т. п. В формулировке цели участвуют предметы и понятия реального мира, имеющие отношение к разрабатываемой программной системе.
Объектную модель можно описать следующим образом:
1) основные элементы модели — объекты и сообщения;
2) объекты создаются, используются и уничтожаются подобно динамическим переменным в обычных языках программирования;
3) выполнение программы заключается в создании объектов и передаче им последовательности сообщений.
Объектная модель базируется на е четырех главных принципах: абстрагировании; инкапсуляции; модульности; иерархии.
Эти принципы являются главными в том смысле, что без любого из них модель не будет по-настоящему объектно-ориентированной.
Абстрагирование концентрирует внимание на внешних особенностях объекта и позволяет отделить самые существенные особенности поведения от несущественных. Выбор правильного набора абстракций для заданной предметной области представляет собой главную задачу объектно-ориентированного проектирования.
Все абстракции обладают как статическими, так и динамическими свойствами. Например, файл как объект требует определенного объема памяти на конкретном устройстве, имеет имя и содержание. Эти атрибуты являются статическими свойствами, Конкретные же значения каждого из перечисленных свойств динамичны и изменяются в процессе использования объекта: файл можно увеличить или уменьшить, изменить его имя и содержимое.
Абстракция и инкапсуляция дополняют друг друга: абстрагирование направлено на наблюдаемое поведение объекта, а инкапсуляция занимается внутренним устройством. Чаще всего инкапсуляция выполняется посредством скрытия информации, т. е. маскировкой всех внутренних деталей, не влияющих на внешнее поведение. Объектный подход предполагает, что собственные ресурсы, которыми могут манипулировать только методы самого объекта, скрыты от внешних компонент.
При объектно-ориентированном проектировании необходимо физически разделить классы и объекты, составляющие логическую структуру проекта. Такое разделение делает возможным повторно использовать во все новых проектах код модулей, написанных ранее. Модулю в данном контексте соответствует отдельный файл исходного текста. На выбор разбиения на модули могут влиять и некоторые внешние обстоятельства. При коллективной разработке программ распределение работы осуществляется, как правило, по модульному принципу, и правильное разделение проекта минимизирует связи между участниками.
Таким образом, принципы абстрагирования, инкапсуляции и модульности являются взаимодополняющими. Объект логически определяет границы определенной абстракции, а инкапсуляция и модульность делают их физически незыблемыми.
Имея выявленные объекты, можно приступить к выявлению классов. Классы чаще всего строятся постепенно, начиная от простых родительских классов и заканчивая более сложными. Непрерывность процесса основана на наследовании. Каждый раз, когда из предыдущего класса производится последующий, производный класс наследует какие-то или все родительские качества, добавляя к ним новые. Завершенный проект может включать десятки и сотни классов, но часто все они произведены от считанного количества родительских классов.
6.6. КАКИМИ БЫВАЮТ ОБЪЕКТЫ ПО УСТРОЙСТВУ
Под паттернами проектирования понимается описание взаимодействия объектов и классов, адаптированных для решения общей задачи проектирования в конкретном контексте. Паттерн проектирования — это образец, типовое решение какого-либо механизма объектно-ориентированной программы. Паттерны создавались несколько лет коллективом [26] с целью уравнивания шансов на хороший проект опытных и не очень опытных проектировщиков. По словам архитектора Кристофера Александра, «любой паттерн описывает задачу, которая снова и снова возникает в нашей работе, а также принцип ее решения, причем таким образом, что это решение можно потом использовать миллион раз, ничего не изобретая заново».
В общем случае паттерн состоит из четырех основных элементов: имени, задачи, решения, результатов.
Имя. Сославшись на него, можно сразу описать проблему проектирования, ее решения и их последствия. С помощью словаря паттернов можно вести обсуждение с коллегами, упоминать паттерны в документации.
Задача — описание того, когда следует применять паттерн. Здесь может описываться конкретная проблема проектирования, например способ представления алгоритмов в виде объектов. Иногда отмечается, какие структуры классов или объектов свидетельствуют о негибком дизайне. Также может включаться перечень условий, при выполнении которых имеет смысл применять данный паттерн.
Решение — описание элементов дизайна, отношений между ними, функций каждого элемента. Конкретный дизайн или реализация не имеются в виду, поскольку паттерн — это шаблон, применимый в самых разных ситуациях. Просто дается абстрактное описание задачи проектирования и того, как она может быть решена с помощью некоего весьма обобщенного сочетания элементов (в нашем случае классов и объектов).
Результаты — это следствия применения паттерна и разного рода компромиссы. Хотя при описании проектных решений о последствиях часто не упоминают, знать о них необходимо, чтобы можно было выбрать между различными вариантами и оценить преимущества и недостатки данного паттерна. Здесь речь идет о выборе языка и реализации. Поскольку в объектно-ориентированном проектировании повторное использование зачастую является важным фактором, то к результатам следует относить и влияние на степень гибкости, расширяемости и переносимости системы. Перечисление всех последствий поможет вам понять и оценить их роль. Ниже приведен полный список разделов описания паттерна:
— название и классификация паттерна (название паттерна должно четко отражать его назначение);
— назначение (лаконичный ответ на следующие вопросы: каковы функции паттерна, его обоснование и назначение, какую конкретную задачу проектирования можно решить с его помощью);
— известен также под именем (другие распространенные названия паттерна, если таковые имеются);
— мотивация (сценарий, иллюстрирующий задачу проектирования и то, как она решается данной структурой класса или объекта. Благодаря мотивации, можно лучше понять последующее, более абстрактное описание паттерна);
— применимость (описание ситуаций, в которых можно применять данный паттерн; примеры проектирования, которые можно улучшить с его помощью);
— структура (графическое представление классов в паттерне с использованием нотации, основанной на методике ОМТ, а также с использованием диаграмм взаимодействий для иллюстрации последовательностей запросов и отношений между объектами);
— участники (классы или объекты, задействованные в данном паттерне проектирования, и их функции);
— отношения (взаимодействие участников для выполнения своих функций);
— результаты (насколько паттерн удовлетворяет поставленным требованиям? Результаты применения, компромиссы, на которые приходится идти. Какие аспекты поведения системы можно независимо изменять, используя данный паттерн?);
— реализация (сложности и так называемые «подводные камни» при реализации паттерна; советы и рекомендуемые приемы есть ли у данного паттерна зависимость от языка программирования?);
— пример кода (фрагмент кода, иллюстрирующий вероятную реализацию на языках С++ или Smalltalk);
— известные применения (возможности применения паттерна в реальных системах; даются, по меньшей мере, два примера из различных областей);
— родственные паттерны (связь других паттернов проектирования с данными, важные различия, использование данного паттерна в сочетании с другими).
Каталог [26] содержит 23 паттерна. Ниже для удобства перечислены их имена и назначение.
1. Abstract Factory (абстрактная фабрика). Предоставляет интерфейс для создания семейств, связанных между собой, или независимых объектов, конкретные классы которых неизвестны.
2. Adapter (адаптер). Преобразует интерфейс класса в некоторый другой интерфейс, ожидаемый клиентами. Обеспечивает совместную работу классов, которая была бы невозможна без данного паттерна из-за несовместимости интерфейсов.
3. Bridge (мост). Отделяет абстракцию от реализации, благодаря чему появляется возможность независимо изменять то и другое.
4. Builder (строитель). Отделяет конструирование сложного объекта от его представления, позволяя использовать один и тот же процесс конструирования для создания различных представлений.
5. Chain оf Responsibility (цепочка обязанностей). Можно избежать жесткой зависимости отправителя запроса от его получателя, при этом запросом начинает обрабатываться один из нескольких объектов. Объекты-получатели связываются в цепочку, и запрос передается по цепочке, пока какой-то объект его не обработает.
6. Command (команда). Инкапсулирует запрос в виде объекта, позволяя тем самым параметризовывать клиентов типом запроса, устанавливать очередность запросов, протоколировать их и поддерживать отмену выполнения операций.
7. Composite (компоновщик). Группирует объекты в древовидные структуры для представления иерархий типа «часть-целое». Позволяет клиентам работать с единичными объектами так же, как с группами объектов.
8. Decorator (декоратор). Динамически возлагает на объект новые функции. Декораторы применяются для расширения имеющейся функциональности и являются гибкой альтернативой порождению подклассов.
9. Facade (фасад). Предоставляет унифицированный интерфейс к множеству интерфейсов в некоторой подсистеме. Определяет интерфейс более высокого уровня, облегчающий работу с подсистемой.
10. Factory Method (фабричный метод). Определяет интерфейс для создания объектов, при этом выбранный класс инстанцируется подклассами.
11. Flyneight (приспособленец). Использует разделение для эффективной поддержки большого числа мелких объектов.
12. Interpreter (интерпретатор). Для заданного языка определяет представление его грамматики, а также интерпретатор предложений языка, использующий это представление.
13. Iterator (итератор). Дает возможность последовательно обойти все элементы составного объекта, не раскрывая его внутреннего представления.
14. Mediator (посредник). Определяет объект, в котором инкапсулировано знание о том, как взаимодействуют объекты из некоторого множества. Способствует уменьшению числа связей между объектами, позволяя им работать без явных ссылок друг на друга. Это, в свою очередь, дает возможность независимо изменять схему взаимодействия.
15. Memento (хранитель). Позволяет, не нарушая инкапсуляции, получить и сохранить во внешней памяти внутреннее состояние объекта, чтобы позже объект можно было восстановить точно в таком же состоянии.
16. Observer (наблюдатель). Определяет между объектами зависимость типа «один ко многим», так что при изменении состояния одного объекта все зависящие от него получают извещение и автоматически обновляются.
17. Prototype (npomomun). Описывает виды создаваемых объектов с помощью прототипа и создает новые объекты путем его копирования.
18. Proxy (заместитель). Подменяет другой объект для контроля доступа к нему.
19. Singleton (одиночка). Гарантирует, что некоторый класс может иметь только один экземпляр, и предоставляет глобальную точку доступа к нему.
20. State (состояние). Позволяет объекту варьировать свое поведение при изменении внутреннего состояния. При этом создается впечатление, что поменялся класс объекта.
21. Strategy (стратегия). Определяет семейство алгоритмов, инкапсулируя их все и позволяя подставлять один вместо другого. Можно менять алгоритм независимо от клиента, который им пользуется.
22. Template Method (шаблонный метод). Определяет скелет алгоритма, перекладывая ответственность за некоторые его шаги на подклассы. Позволяет подклассам переопределять шаги алгоритма, не меняя его общей структуры.
23. Visitor (посетитель). Представляет операцию, которую надо выполнить над элементами объекта. Позволяет определить новую операцию, не меняя классы элементов, к которым он применяется.
6.7. ПРОЕКТНАЯ ПРОЦЕДУРА ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОЕКТИРОВАНИЯ ПО Б. СТРАУСТРУПУ
6.7.1. Укрупненное изложение проектной процедуры Б. Страуструпа
Б. Страуструп — автор объектно-ориентированного языка программирования С++ с множественным наследованием. У Б. Страуструпа при описании методики проектирования вводится единица проектирования — «компонента». Под компонентой понимается множество классов, объединенных некоторым логическим условием, иногда это общий стиль программирования или описания, иногда — предоставляемый сервис. Ряд авторов вместо термина «компонента» используют термин «модуль».
Структура компонент проектируется использованием итерационного нарастающего процесса. Обычно для получения проекта, который можно уверенно использовать для первичной реализации или повторной, нужно несколько раз проделать последовательность из следующих четырех шагов.
Шаг 1. Выделение понятий (классов, порождающих объекты) и установление основных связей между ними.
Шаг 2. Уточнение классов с определением наборов операций (методов) для каждого.
Шаг 3. Уточнение классов с точным определением их зависимостей от других классов. Выясняется наследование и использование зависимостей.
Шаг 4. Задание интерфейсов классов. Более точно определяются отношения классов. Методы разделяются на общие и защищенные. Определяются типы операций над классами.
6.7.2. Шаг 1. Выделение понятий и установление основных связей между ними
Выделение объектов производится во время процесса мысленного представления системы. Часто это происходит как цикл вопросов «что/кто». Команда программистов определяет: что требуется делать? Это немедленно приводит к вопросу: кто будет выполнять действие? Теперь программная система в значительной мере становится похожей на некую организацию. Действия, которые должны быть выполнены, присваиваются некоторому программному объекту в качестве его обязанностей.
Понятия (объекты) соответствуют порождающим классам и могут иметь форму в виде существительных и, как экзотика, глаголов и прилагательных.
Часто говорят, что понятия в форме существительных играют роль классов и объектов, используемых в программе. Например: трактор, редуктор, гайка, редактор, кнопка, файл, матрица. Это действительно так, но это только начало.
Глаголы могут представлять операции над объектами или обычные (глобальные) функции, вырабатывающие новые значения, исходя из своих параметров, или даже классы. В качестве примера можно рассматривать манипуляторы, предложенные А. Кенигом. Суть идеи манипулятора в том, что создается объект, который можно передавать куда угодно и который используется как функция. Такие глаголы, как «повторить» или «совершить» могут быть представлены итеративным объектом или объектом, представляющим операцию выполнения программы в базах данных.
Даже прилагательные можно успешно представлять с помощью классов. Например, такими классами могут быть: «хранимый», «параллельный», «регистровый», «ограниченный», — а также классы, которые помогут разработчику или программисту, задав виртуальные базовые классы, специфицировать и выбрать нужные свойства для классов, проектируемых позднее.
Все классы можно условно разделить на две группы: классы, из предметной (прикладной) области и классы, являющиеся артефактами реализации или абстракциями периода реализации. Классы из предметной (прикладной) области — непосредственно отражают понятия из прикладной области, т. е. понятия, которые использует конечный пользователь для описаний своих задач и методов их решения.
Лучшее средство для поиска этих понятий/классов — грифельная доска, а лучший метод первого уточнения — беседа со специалистами в области приложения или просто с друзьями. Обсуждение необходимо, чтобы создать начальный словарь терминов и понятийную структуру.
Главное в хорошем проекте — прямо отразить какое-либо понятие «реальности», т. е. уловить понятие из области приложения классов, представить взаимосвязь между классами строго определенным способом, например с помощью наследования, и повторить эти действия на разных уровнях абстракции.
Классу являющиеся артефактами реализации или абстракциями периода реализации, — это те понятия, которые применяют программисты и проектировщики для описания методов реализации:
• классы, отражающие ресурсы оборудования (оперативная память, механизмы управления ресурсами, дисковое пространство);
• классы, представляющие системные ресурсы (процессы, потоки ввода-вывода);
• классы, реализующие программные структуры (стеки, очереди, списки, деревья, словари и т. п.);
• другие абстракции, например элементы управления программой (кнопки, меню и т. п.).
Хорошо спроектированная система должна содержать классы, которые дают возможность рассматривать систему с логически разных точек зрения.
Пример:
1) классы, представляющие пользовательские понятия (например, легковые машины и грузовики);
2) классы, представляющие обобщения пользовательских понятий (движущиеся средства);
3) классы, представляющие аппаратные ресурсы (например, класс управления памятью);
4) классы, представляющие системные ресурсы (например, выходные потоки);
5) классы, используемые для реализации других классов (например, списки, очереди);
6) встроенные типы данных и структуры управления.
В больших системах очень трудно сохранять логическое разделение типов различных классов и поддерживать такое разделение между различными уровнями абстракции. В приведенном выше перечислении представлены три уровня абстракции:
(1+2) — представляет пользовательское отражение системы;
(3+4) — представляет машину, на которой будет работать система;
(5+6) — представляет низкоуровневое (со стороны языка программирования) отражение реализации.
Чем больше система, тем большее число уровней абстракции необходимо для ее описания и тем труднее определять и поддерживать эти уровни абстракции. Отметим, что таким уровням абстракции есть прямое соответствие в природе и в различных построениях человеческого интеллекта. Например, можно рассматривать дом как объект, состоящий из атомов; молекул; досок и кирпичей; стен, пола и потолков; комнат.
Пока удается хранить раздельно представления этих уровней абстракции, можно поддерживать целостное представление о доме. Однако если смешать их, возникнет бессмыслица.
Взаимоотношения, о которых мы говорим, естественно устанавливаются в области приложения или (в случае повторных проходов по шагам проектирования) возникают из последующей работы над структурой классов. Они отражают наше понимание основ области приложения и часто являются классификацией основных понятий. Пример такого отношения — машина с выдвижной лестницей есть грузовик, есть пожарная машина, есть движущееся средство.
6.7.3. Шаг 2. Уточнение классов с определением набора операций (методов) для каждого
В действительности нельзя разделить процессы определения классов и выяснения того, какие операции для них нужны. Однако на практике они различаются, поскольку при определении классов внимание концентрируется на основных понятиях, не останавливаясь на программистских вопросах их реализации, тогда как при определении операций прежде всего сосредоточиваются на том, чтобы задать полный и удобный набор операций. Часто бывает слишком трудно совместить оба подхода, в особенности, учитывая, что связанные классы надо проектировать одновременно.
Возможно несколько подходов к процессу определения набора операций. Предлагаем следующую стратегию:
— рассмотрите, каким образом объект класса будет создаваться, копироваться (если нужно) и уничтожаться;
— определите минимальный набор операций, необходимый для понятия, представленного классом;
— рассмотрите операции, которые могут быть добавлены для удобства записи, и включите только несколько действительно важных;
— рассмотрите, какие операции можно считать тривиальными, т. е. такими, для которых класс выступает в роли интерфейса для реализации производного класса;
— рассмотрите, какой общности именования и функциональности можно достигнуть для всех классов компонента.
Очевидно, что это стратегия минимализма. Гораздо проще добавить любую функцию, приносящую ощутимую пользу, и сделать все операции виртуальными. Но чем больше функций, тем больше вероятность, что они не будут использоваться, наложат определенные ограничения на реализацию и затруднят эволюцию системы. Гораздо легче включить в интерфейс еще одну функцию, как только установлена потребность в ней, чем удалить ее оттуда, когда уже она стала привычной.
Причина, по которой мы требуем явного принятия решения о виртуальности данной функции, не оставляя его на стадию реализации, в том, что, объявив функцию виртуальной, существенно повлияем на использование ее класса и на взаимоотношения этого класса с другими.
При определении набора операций (методов) больше внимания следует уделять тому, что надо сделать, а не тому, как это сделать.
Иногда полезно классифицировать операции класса по тому, как они работают с внутренним состоянием объектов:
1) базовые операции: конструкторы, деструкторы, операции копирования;
2) селекторы: операции, не изменяющие состояния объекта;
3) модификаторы: операции, изменяющие состояние объекта;
4) операции преобразований, т. е. операции, порождающие объект другого типа, исходя из значения (состояния) объекта, к которому они применяются;
5) повторители: операции, которые открывают доступ к объектам класса или используют последовательность объектов.
Кроме уже перечисленных групп методов, в классы могут: быть введены дополнительные методы самотестирования и проверки корректности данных. Это не есть разбиение на ортогональные группы операций. Например, повторитель может быть спроектирован как селектор или модификатор.
Выделение этих групп просто предназначено помочь в процессе проектирования интерфейса класса. Конечно, допустима и другая классификация.
6.7.4. Шаг 3. Уточнение классов с точным определением их зависимостей от других классов
Виды взаимоотношений между классами могут быть следующими: отношения наследования; отношения включения; отношения использования; запрограммированные отношения.
Еще одно взаимоотношение — отношение включения (агрегирования) — класс содержит в виде члена объект или указатель на объект другого класса. Позволяя объектам содержать указатели на другие объекты, можно создавать так называемые «иерархии объектов». Такие реализации альтернативно дополняют возможности использования иерархии классов.
Очень важным при проектировании является вопрос: какое отношение выбрать — агрегации (включения) или наследования. В принципе эти методы взаимозаменяемы, кроме случая, когда используется позднее связывание. Наиболее предпочтителен тот вариант, в котором наиболее точно моделируется окружающая действительность, т. е. если понятие Х является частью понятия У, то используется включение. Если понятие Х более общее, чем Y, — то наследование.
Для составления и понимания проекта часто необходимо знать, какие классы и каким способом они используются, другими словами, отношения использования. Возможно следующим образом классифицировать те способы, с помощью которых класс Х может использовать класс Y:
— Х использует Y;
— Х вызывает функцию-член (метод) Y;
— Х читает член Y;
— Х пишет в член Y;
— Х создает Y;
— Х размещает переменную из Y.
Анализ подобных взаимосвязей позволяет выявить потребности в определенных методах классов или, наоборот, выявить их ненужность.
Запрограммированные отношения — те отношения проекта, которые не могут быть прямо представлены в виде конструкций языка.
Допустим, в проекте оговорено, что каждая операция, не реализованная в классе А, должна обслуживаться объектом класса В. К запрограммированным отношениям относят также операции преобразования типов. Следует, по возможности, избегать применения этого вида отношений из-за усложнения реализации. Идеальный класс должен в минимальной степени зависеть от остального мира. Следовательно, следует стараться минимизировать зависимости.
6.7.5. Шаг 4. Задание интерфейсов классов
Спрячем подробности реализации за фасадом интерфейса. Объект инкапсулирует поведение, если он умеет выполнять некоторые действия, но подробности, как это делается, остаются скрытыми за фасадом интерфейса. Эта идея была сформулирована специалистом по информатике Дэвидом Парнасом в виде правил, которые часто называются принципами Парнаса.
Правило 1. Разработчик программы должен предоставлять пользователю всю информацию, которая нужна для эффективного использования приложения, и ничего кроме этого.
Правило 2. Разработчик программного обеспечения должен знать только требуемое поведение объекта и ничего кроме этого.
Следствие принципа отделения интерфейса от реализации состоит в том, что программист может экспериментировать с различными алгоритмами, не затрагивая остальные классы объектов программы.
На этом шаге дается четкое описание классов, их данных и методов (опуская реализацию и, возможно, скрытые методы). Всем методам задаются точные типы параметров.
Идеальный интерфейс представляет пользователю полный и последовательный набор понятий; согласован со всеми частями компоненты; не открывает подробности реализации и может быть реализован различными способами; ограниченно и четко определенным образом зависит от других интерфейсов.
Интерфейсы классов предоставляют полную информацию для реализации классов на этапе кодирования.
Существует золотое правило: если класс не допускает, по крайней мере, двух существенно отличающихся реализаций, то что-то явно не в порядке с этим классом, это просто замаскированная реализация, а не представление абстрактного понятия. Во многих случаях для ответа на вопрос: «Достаточно ли интерфейс класса независим от реализации?» — надо указать, возможна ли для класса схема обычных вычислений.
6.7.6. Перестройка иерархии классов
Пытаясь провести классификацию некоторых новых объектов, задаем следующие вопросы: В чем сходство этого объекта с другими объектами общего класса? В чем его различия? Каждый класс имеет набор поведений и характеристик, которые его определяют. Начнем с верхушки фамильного дерева образца и будем спускаться по ветвям, задавая эти вопросы на протяжении всего пути. Более высокие уровни являются более общими, а вопросы — более простыми. Каждый уровень является более специфическим, чем предыдущий уровень, и менее общим.
Без сомнения, это тривиальная задача, но установить идеальную иерархию классов для определенного применения очень трудно. Прежде чем написать строку кода программы, необходимо хорошо подумать о том, какие классы необходимы и на каком уровне. По мере того как увеличивается применение, может оказаться, что необходимы новые классы, которые фундаментально изменяют всю иерархию классов.
На втором и третьем шагах итеративной процедуры проектирования производится выявление того, насколько адекватно классы и их иерархия подходят по сути проекта. Проектировщики вынуждены реорганизовывать, улучшать проект и повторять все шаги сначала, и так до тех пор, пока качество проекта не будет удовлетворительным.
При перестройке иерархии классов применяются четыре процедуры: расщепление класса на два и более; абстрагирование (обобщение); слияние; анализ возможности использования существующих разработок.
Расщепление применяется в следующих случаях:
1) если имеется сложный класс, иногда имеет смысл разделить его на несколько простых классов и тем самым обеспечить поэтапную разработку;
2) класс содержит ряд несвязанных между собой функций или набор независимых друг от друга данных.
Обобщение — выявление в группе классов общих свойств и вынесение их в общий базовый класс. Признаки необходимости, обобщения таковы:
1) общая схема использования;
2) сходство между наборами операций;
3) сходство реализаций;
4) эти классы часто фигурируют вместе в дискуссиях по проекту.
Слияние — объединение нескольких небольших, но тесно взаимодействующих классов в один. Таким образом, взаимодействие будет скрыто в реализации нового класса.
Использование существующих разработок. Обособленный класс или группа классов из уже существующего проекта может быть легко интегрирована в новый класс. Однако подобная интеграция вносит определенные ограничения в структуру системы и может сказаться на эффективности разработки самой программы. Изготовители систем объектно-ориентированного программирования поставляют системы с совместимыми библиотеками классов. Очевидно, чем больше готовых библиотечных классов будет использовано в программе, тем меньше кода придется писать при реализации программы.
В рассмотренных ранее темах не было дано настоятельных и конкретных рекомендаций по проектированию. Это соответствует убеждению, что нет «единственно верного решения». Принципы и приемы следует применять такие, которые лучше подходят для решения конкретных задач. Для этого нужен вкус, опыт и разум. Тем не менее можно указать некоторый свод правил (эвристических приемов), который разработчик может использовать в качестве ориентиров, пока не будет достаточно опытен, чтобы выработать лучшие правила. Ниже приведен свод таких эвристических правил.
Правило 1. Узнайте, что вам предстоит создать.
Правило 2. Ставьте определенные и осязаемые цели.
Правило 3. Не пытайтесь с помощью технических приемов решить социальные проблемы.
Правило 4. Рассчитывайте на большой срок в проектировании 5 и управлении людьми.
Правило 5. Используйте существующие системы в качестве 5 моделей, источника вдохновения и отправной точки.
Правило 6. Проектируйте в расчете на изменения: гибкость, расширяемость, переносимость, повторное использование.
Правило 7. Документируйте, предлагайте и поддерживайте повторно используемые компоненты.
Правило 8. Поощряйте и вознаграждайте повторное использование: проектов, библиотек, классов.
Правило 9. Сосредоточьтесь на проектировании компоненты.
Правило 10. Используйте классы для представления понятий.
Правило 11. Определяйте интерфейсы так, чтобы сделать открытым минимальный объем информации, требуемой для интерфейса.
Правило 12. Проводите строгую типизацию интерфейсов всегда, когда это возможно.
Правило 13. Используйте в интерфейсах типы из области приложения всегда, когда это возможно.
Правило 14. Многократно исследуйте и уточняйте как проект так и реализацию.
Правило 15. Используйте лучшие доступные средства для проверки и анализа проекта и реализации.
Правило 16. Экспериментируйте, анализируйте и проводите тестирование на самом возможном раннем этапе.
Правило 17. Стремитесь к простоте, максимальной простоте, но не сверх того.
Правило 18. Не разрастайтесь, не добавляйте возможности «на всякий случай».
Правило 19. Не забывайте об эффективности.
Правило 20. Сохраняйте уровень формализации, соответствующий размеру проекта.
Правило 21. Не забывайте, что разработчики, программисты и даже менеджеры остаются людьми.
6.7.8. Пример простейшего проекта
Б. Страуструп придумал реализацию механизма множественного наследования и при этом отвергал агрегирование, хотя и реализовал это в своем языке С++.
Приведенный далее пример показывает невозможность осуществления решения следующей простой задачи двумя способами решения — с использованием множественного наследования и агрегирования. В процессе решения задач было выявлено, что в ряде задач без выполнения третьего шага невозможно корректное выполнение второго шага. Таким образом, при решении одного и того же примера двумя способами второй и третий шаги проекта были взаимно переставлены. Также добавлен шаг «классификация объектов» (составление словаря).
Первый способ решения задачи — использование множественного наследования.
Постановка задачи примера. Вывести на экран фигуру, показанную на рис. 6.4.
Изображенная на рис. 6.4 фигура состоит из правильного пятиугольника и описанной вокруг него окружности, где хс, ус — координаты центра описанной вокруг пятиугольника окружности; R — радиус описанной вокруг пятиугольника окружности.
Кроме того, фигура рисуется заданным цветом.
Следует отметить, что задача может быть решена несколькими способами.
Шаг 1a. Определение объектов и выявление их свойств.
Объект — Рисунок. Свойства объекта:
— радиус окружности (R);
— координаты центра окружности (кс; ус);
— цвет линий.
Объект — Пятиугольник. Свойства объекта:
— радиус описанной вокруг него окружности (R);
— координаты центра описанной вокруг него окружности (хс; ус):
— цвет линии.
Объект — Окружность. Свойства объекта:
— радиус (R);
— координаты центра (хс; ус);
— цвет линии.
Решение задачи примера с использованием множественного наследования.
Шаг 1б. Классификация объектов (составление словаря). Пятиугольник — центрально-симметричная фигура с пятью вершинами.
Окружность — центрально-симметричная фигура, каждая точка которой отстоит от заданной точки — центра, на заданную величину — радиус окружности.
Полученный граф наследования классов изображен на рис. 6.5.
Шаг 2. Уточнение классов с точным определением их зависимостей от других классов. Выясняется наследование и использование зависимостей.
Поскольку Пятиугольник и Окружность — это разновидности центрально-симметричных фигур, то им может соответствовать следующая иерархия классов. Базовый класс: Центрально-симметричная фигура с данными R, хс, ус. Классы Пятиугольник и Окружность являются наследниками этого класса, а класс Рисунок является наследником классов Окружность и Пятиугольник, поскольку в данной задаче рисунок является сочетанием пятиугольника и окружности.
Шаг 3. Уточнение классов с определением наборов операций для каждого. Здесь анализируется потребность в конструкторах, деструкторах и операциях копирования. При этом принимается 1 во внимание минимальность, полнота и удобство.
Класс Рисунок. Экземпляр этого класса должен создаваться и рисоваться, а следовательно, в интерфейсе класса Рисунок должны присутствовать конструкторы и функция — член рисования 1 рисунка. Тогда получаем:
• конструктор без параметров;
• конструктор с параметрами (Радиус, x-координата, y-координата, Цвет);
• функцию-член вывода рисунка — «Начертить».
Класс Пятиугольник. Экземпляр этого класса должен создаваться и рисоваться, а следовательно» в интерфейсе класса Пятиугольник должны присутствовать конструкторы и функция-член рисования пятиугольника. Тогда получаем:
• конструктор без параметров;
• конструктор с параметрами (Радиус, x-координата, y-координата);
• функцию-член вывода пятиугольника на экран — «Начертить».
Класс Окружность. Экземпляр этого класса должен создаваться и рисоваться, а следовательно, в интерфейсе класса Окружность должны присутствовать конструкторы и функция-член вывода окружности на экран. Тогда получаем:
• конструктор без параметров;
• конструктор с параметрами (Радиус,x-координата, y-координата);
• функцию-член вывода окружности на экран — «Начертить». Класс Центрально-симметричная фигура. Экземпляр данного класса должен содержать информацию о центрально-симметричной фигуре в виде данных с защищенным доступом (не интерфейсная часть класса) и иметь чисто-виртуальную функцию перерисовки вместе с конструкторами. Тогда получаем:
• конструктор без параметров;
• конструктор с параметрами (Радиус, x-координата, y-координата);
• чисто-виртуальную функцию-член вывода изображения на экран.
Шаг 4. Задание интерфейсов классов. Более точно определяются отношения классов. Методы разделяются на общие и защищенные методы. Определяются типы операций над классами.
Данные, расположенные в классе Центрально-симметричная фигура (R, xc, ус), должны быть доступны классам-наследникам Пятиугольник и Окружность, но недоступны «извне», значит, уровень доступа — «защищенный». В классе Центрально-симметричная фигура нужно расположить функцию «Нарисовать», которую предполагается сделать чисто-виртуальной. Классы, наследующие у класса Центрально-симметричная фигура, смогут переопределить функцию «Нарисовать» для рисования самих себя.
Поскольку обоим объектам — экземплярам классов Пятиугольник и Окружность нужен только один центр на двоих, то, следовательно, экземпляр класса Центрально-симметричная фигура должен создаваться только один, а значит, при описании наследования в языке С++ нужно добавить зарезервированное слово virtual. Наследование классами Пятиугольник и Окружность признаков у класса Центрально-симметричная фигура должно происходить с открытым уровнем доступа, иначе при создании класса Рисунок мы не сможем запустить конструктор класса верхнего уровня. Наследование классом Рисунок признаков классов. Пятиугольник и Окружность должно происходить закрыто, чтобы к методам этих классов нельзя было обратиться через объект класса Рисунок. К наследуемым признакам добавляется свойство «Цвет линии», значение которого будет храниться в классе Рисунок. В классе Рисунок, так же как и в классах Пятиугольник и Окружность, можно переопределить метод «Нарисовать». Этот метод выводит изображения на экран, в нем как раз и будет устанавливаться цвет линий, при котором будут рисоваться фигуры.
Второй способ решения задачи с использованием агрегирования. Поскольку шаги 1a и 1б выполняются полностью аналогично предшествующему способу решения, начинаем с шага 2.
Шаг 2. Уточнение классов с точным определением их зависимостей от других классов. Выясняется наследование и использование зависимостей.
Объект рисунок состоит из объектов пятиугольник и окружность, форма и размер которых определяются настройками, задаваемыми при создании объекта рисунок, т. е. можно создать два независимых класса Пятиугольник (правильный) и Окружность, а затем экземпляры этих классов агрегировать в объект рисунок — экземпляр класса Рисунок.
Шаг 3. Уточнение классов с определением наборов операций для каждого. Здесь анализируется потребность в конструкторах, деструкторах и операциях копирования. При этом принимается во внимание минимальность, полнота и удобство.
Класс Рисунок. Объект этого класса должен уметь создать, е уничтожить и нарисовать себя, поэтому интерфейсная часть класса будет следующей:
• конструктор без параметров;
• конструктор с параметрами (Радиус, x-координата, y-координата, Цвет);
• метод вывода рисунка на экран;
• деструктор для уничтожения создаваемых включенных объектов.
Примечание: Включение объектов типов Пятиугольник и Окружность происходит в закрытой, не интерфейсной части, класса.
Класс Пятиугольник. Объект класса Пятиугольник должен уметь создать и рисовать себя, поэтому интерфейсная часть класса будет выглядеть следующим образом:
• конструктор без параметров;
• конструктор с параметрами (Радиус, x-координата, y-координата);
• метод вывода пятиугольника на экран.
Класс Окружность. Объект класса Окружность должен создавать и рисовать сам себя, поэтому интерфейсная часть класса будет выглядеть следующим образом:
• конструктор без параметров;
• конструктор с параметрами (Радиус, x-координата, y-координата);
• метод вывода окружности на экран.
Шаг 4. Задание интерфейсов классов. Более точно определяются отношения классов. Методы разделяются на общие и защищенные. Определяются типы операций над классами.
Классы Окружность и Пятиугольник должны содержать внутри себя переменные А, хс, ус, которые должны быть закрыты для доступа; функцию-член вывода фигуры на экран для доступа — открытую (как и конструкторы).
Для класса Рисунок включаемые экземпляры классов Пятиугольник и Окружность являются полями, поэтому их нужно скрыть, чтобы командовать этими объектами мог только экземпляр класса Рисунок. Функцию вывода рисунка на экран, как и конструкторы, нужно сделать открытыми.
Анализ результатов шагов 2 и 3 показывает, что проектная процедура допускает предварительное выполнение определения набора операций до определения зависимостей класса от других классов с последующим уточнением наборов операций классов.
6.8. ТЕХНОЛОГИЯ ПРОЕКТИРОВАНИЯ НА ОСНОВЕ ОБЯЗАННОСТЕЙ
6.8.1. RDD-технология проектирования на основе обязанностей
Далее будет изложена технология проектирования на основе обязанностей (или RDD-проектирование — Responsibility-Driven-Design), предложенная Т. Бадтом. Технология ориентирована на малые и средние проекты. Она основана на поведении систем. Данная технология по способу мышления аналогична разработке структуры служб какой-то организации: директора, заместителей директора, служб и подразделений.
Чтобы выявить отдельные объекты и определить их обязанности, команда программистов прорабатывает сценарий системы, т.е. мысленно воспроизводится запуск приложения как если бы оно уже было готово. Любое действие, которое может произойти, приписывается некоторому объекту в качестве его обязанности.
В качестве составной части этого процесса полезно изображать объекты с помощью CRC-карточек. Название CRC-карточки образовано от слов: Component, Responsibility, Collaborator — компонента (объект), обязанности, сотрудники. По мере того как для объектов выявляются обязанности, они записываются на лицевой стороне CRC-карточки (рис. 6.6).
При проработке сценария полезно разделить CRC-карточки между различными членами проектной группы. Человек, имеющий карточку, которая представляет собой определенный объект, записывает его обязанности и исполняет функции заменителя программной системы, передавая «управление» следующему члену команды, когда программная система нуждается в услугах других объектов.
Преимущества CRC-карточек в том, что они недорогие и с них можно стирать информацию. Это стимулирует экспериментирование, поскольку альтернативные проекты могут быть испробованы, изучены и отброшены с минимальными затратами. Физическое разделение карточек стимулирует интуитивное понимание важности логического разделения классов объектов. Небольшой размер карточки служит хорошей оценкой примерной сложности отдельного класса объекта. Объект, которому приписывается больше за
дач, чем может поместиться на его карточке, вероятно, является излишне сложным. Может быть следует пересмотреть разделение обязанностей или разбить объект на два.
Из-за чего процесс проектирования начинают с анализа функционирования или поведения системы? Простой ответ состоит в том, что поведение системы обычно известно задолго до остальных ее свойств.
Поведение — это нечто, что может быть описано в момент возникновения идеи программы и (в отличие от формальной спецификации системы) выражено в терминах, понятных как для программиста, так и для клиента.
Представим себе, что вы являетесь главным архитектором программных систем в ведущей компьютерной фирме. Однажды появляется ваш начальник с идеей, которая, как он надеется, будет очередным успехом компании. Вам поручают разработать систему под названием «Интерактивный разумный кухонный помощник» (РКП).
Задача, поставленная перед вашей командой программистов, 1 сформулирована в нескольких скупых словах. Программа «разумный кухонный помощник» (РКП) предназначена для домашних персональных компьютеров. Ее цель — заменить собой набор карточек с рецептами, который можно встретить почти в каждой кухне.
Анализ аналогов выявил, что уже известен ряд программных реализаций электронных поваренных книг с рецептами блюд. В данной области применения новой была бы программа, позволяющая планировать питание на заданный период. План питания на заданный период состоит из ежедневных планов питания с трех или четырехразовым приемом пищи. Что надо учесть при разработке ежедневных планов питания? Число человек, калорийность питания каждого человека, любимые и нелюбимые блюда, затраты на питание. Ранние, описанные в литературе попытки оптимизации питания с учетом только продуктов, их калорийности и цен привели к решениям вида: оптимальный завтрак — 12 чашек уксуса. Генерация меню обеда с использованием датчика случайных чисел может привести к решениям с несовместимыми блюдами: молочный суп, сельдь с гороховым гарниром квас. Решение проблемы — использование набора комплексных завтраков, обедов и ужинов. Есть ли в литературе достаточное описание возможных комплексов? Необходимо ли привлечь специалистов по питанию для разработки требуемого количества комплексов? Сколько будет стоить база данных комплексов? Следует ли реализовать функцию автоматической передачи заказа на продукты в магазин? На эти и другие вопросы необходимо дать ответ, чтобы уложиться в отпущенные средства и сроки.
Как это обычно бывает при первоначальном описании многих программных систем, первичные спецификации весьма двусмысленны.
Команда разработчиков решает, что когда система начинает работу, пользователь видит привлекательное информационное окно. Ответственность за его отображение приписывается объекту Greeter. CRC-карточка Greeter представлена на рис. 6.6. Некоторым, пока еще неопределенным образом (с помощью кнопок, всплывающего меню и т. д.) пользователь выбирает одно из следующих восьми действий.
1. Просмотреть базу данных с рецептами, но без ссылок на какой-то план питания.
2. Добавить новый рецепт в базу данных.
3. Редактировать или добавить комментарий к существующему рецепту.
4. Просмотреть базу данных комплексов.
5. Добавить новый комплекс в базу данных.
6. Редактировать или добавить комментарий к существующему комплексу.
7. Создать новый план питания.
8. Пересмотреть существующий план в отношении некоторых дат, блюд и продуктов.
Более детальное описание функций программы представлено на рис. 6.7.
Программа должна обеспечивать ведение базы данных (добавление, удаление и другие действия с отдельным рецептом или набором рецептов). Это, в общем-то, стандартные функции СУБД. Что касается функции планирования, то подразумевается, что программа по запросу пользователя будет составлять план питания на определенный период времени (неделю, месяц, год) для всей семьи или отдельных ее членов, исходя из заданных ограничений (например, ограничение на калорийность). После создания плана пользователю будет предоставлены следующие возможности:
— просмотр плана питания на каждый день из заданного периода действия плана, причем пользователь сможет не только просматривать предлагаемые наборы блюд на обед, ужин и т. д.,
но и редактировать рецепты их приготовления, выбирать рецепт блюда из предложенного программой списка или добавлять свой из базы данных рецептов;
— получение списка продуктов, которые необходимо закупить на расчетный период;
— осуществление распечатки данного плана питания и списка требуемых продуктов;
— создание нового плана на данный период, но с другим ограничением (например, ограничение на продукты питания — церковный пост) или создание нового плана с тем же ограничением, но на другой период.
Важной задачей является уточнение спецификации. В исходных спецификациях наиболее понятны лишь общие положения. Вероятно то, что спецификации для конечного продукта будут изменяться во время разработки программной системы. Далее действия, осуществляемые программной системой, приписываются объектам.
Первые три действия связаны с базой данных рецептов; следующие три действия связаны с базой данных комплексов, последние два — с планированием питания. В результате команда принимает следующее решение: создать объекты, соответствующие этим двум обязанностям.
Таким образом, может быть сформулирована постановка задачи: разработать и реализовать систему по ведению базы данных рецептов с возможностью планирования питания членов семьи. Система должна содержать:
• стандартные средства по ведению базы данных рецептов (просмотр, добавление, редактирование, удаление записей рецептов);
• стандартные средства по ведению базы данных комплексов (просмотр, добавление, редактирование, удаление записей комплексов);
• средства разработки плана питания (создание, корректировка) на определенный период (неделя, месяц, год), исходя из заданных групповых и индивидуальных ограничений (на калорийность, содержание определенных компонентов) для каждого члена семьи;
• возможность вывода информации по приготовляемым блюдам в соответствии с планом питания (на экран, принтер) на весь расчетный период или на требуемый день;
• возможность вывода информации о составе продуктов (на экран, на принтер) как за весь период, так и по датам закупок, исходя из сроков хранений.
Создание сложной физической системы, подобной зданию или автомобилю, упрощается с помощью разбиения проекта на структурные единицы. Точно так же разработка программного обеспечения облегчается после выделения отдельных объектов программы. Объект — это просто абстрактная единица, которая может выполнять определенную работу (т. е. иметь определенные обязанности). На этом этапе нет необходимости знать в точности то, как задается объект или как он будет выполнять свою работу. Объекты может в конечном итоге быть преобразован в отдельную функцию, структуру или же совокупность других объектов. На этом уровне разработки имеются две важные особенности: объект должен иметь небольшой набор четко определенных обязанностей; объект должен взаимодействовать с другими объектами настолько слабо, насколько это возможно.
Отложенные действия. В конце концов придется решать, как пользователь станет просматривать базу данных. Например, должен ли он сначала входить в список таких категорий, как «супы», «салаты», «горячие блюда», «десерты»? С другой стороны, может ли пользователь задавать ключевые слова поиска ингредиентов, например «клубника», «сыр». Следует применять полосы прокрутки или закладки в виртуальной книжке?
Размышлять об этих предметах доставляет удовольствие, но важно то, что нет необходимости принимать конкретные решения на данном этапе проектирования. Поскольку они влияют только на отдельный объект и не затрагивают функционирования остальных частей системы, то все, что надо для продолжения работы над сценарием, — это информация о том, что пользователь должен выбрать комплекс с конкретными рецептами.
Что такое план питания? План питания это список объектов DateList — дат. Дата это объект Date с включенными кулинарными рецептами, с соблюдением правил комплексов завтраков, обедов и ужинов.
Каждый кулинарный рецепт будет идентифицироваться с конкретным объектом. Если рецепт выбран пользователем, управление передается объекту, ассоциированному с рецептом. Рецепт должен содержать определенную информацию, которая в основном состоит из списка ингредиентов и действий, необходимых для трансформирования составляющих в конечный продукт. Согласно нашему сценарию, объект-рецепт должен выполнять и другие действия. Например, он будет отображать рецепт на экране. Пользователь получит возможность снабжать рецепт аннотацией, менять список ингредиентов или набор инструкций, а также может потребовать распечатать рецепт на принтере. Все эти действия являются обязанностью объекта Recipe. На этапе проектирования мы можем рассматривать Recipe как прототип многочисленных объектов-рецептов.
Определив вчерне, как осуществить просмотр базы данных, вернемся к ее блоку управления и предположим, что пользователь хочет добавить новый рецепт. В блоке управления базой данных некоторым образом определяется, в какой раздел поместить новый рецепт (в настоящее время нас не интересуют детали), запрашивается имя рецепта и выводится окно для набора текста. Таким образом, эту задачу естественно отнести к такому объекту, который отвечает за редактирование рецептов. Вернемся к блоку Greeter (см. рис. 6.6). Планирование меню, как вы помните, было поручено объекту PlanManager. Пользователь должен иметь возможность сохранить существующий план. Следовательно, объект PlanManager может запускаться либо в результате открытия уже существующего плана питания, либо при создании нового. В последнем случае пользователя необходимо попросить ввести интервалы времени (список дат) для нового плана. Каждая дата ассоциируется с отдельным объектом типа Date. Пользователь может выбрать конкретную дату для детального исследования. В этом случае управление передается соответствующему объекту Date. Объект PlanManager должен уметь распечатывать меню питания на планируемый период. Наконец, пользователь может попросить объект PlanManager сгенерировать список продуктов на указанный период.
В объекте Date хранятся следующие данные: список блюд на соответствующий день и (необязательно) текстовые комментарии, добавленные пользователем (например, юбилейные даты). Объект должен выводить на экран вышеперечисленные данные. Кроме того, в нем должна быть предусмотрена функция печати. В случае желания пользователя более детально ознакомиться с тем или иным блюдом следует передать управление объекту Meal.
В объекте Meal хранится информация о блюде. Не исключено, что у пользователя окажется несколько рецептов одного блюда. Поэтому необходимо удалять и добавлять рецепты. Кроме того, желательно иметь возможность распечатать информацию о том или ином блюде. Разумеется, должен быть обеспечен вывод информации на экран. Пользователю, вероятнее всего, захочется обратиться еще к каким-нибудь рецептам. Следовательно, необходимо наладить контакт с базой данных рецептов, а значит, объекты Meal и база данных должны взаимодействовать между собой.
Далее команда разработчиков продолжает исследовать все возможные сценарии. Необходимо предусмотреть обработку исключительных ситуаций. Например, что происходит, если пользователь задает ключевое слово для поиска рецепта, а подходящий рецепт не найден? Как пользователь сможет прервать действие (например, ввод нового рецепта), если он не хочет продолжать дальше? Все это должно быть изучено. Ответственность за разработку подобных ситуаций следует распределить между объектами.
Изучив различные сценарии, команда разработчиков в конце решает, что все действия надлежащим образом могут быть распределены между семью объектами (рис. 6.8). Объект Greeter взаимодействует только с PlanManager и RecipeDatabase. Объект PlanManager «зацепляется» только с DateList, DateList с Date, а Date, в свою очередь, — с Meal. Объект Meal обращается к RecipeManager и через посредство этого объекта к конкретным рецептам (см. рис. 6.8).
Отдельные слова имеют слишком много интерпретаций. Поэтому необходимо в самом начале проектирования подготовить словарь, содержащий четкие и недвусмысленные определения всех объектов (классов), атрибутов, операций, ролей и других сущностей, рассматриваемых в проекте. Без такого словаря обсуждение проекта с коллегами по разработке и заказчиками системы не имеет смысла, так как каждый может по-своему интерпретировать обсуждаемые термины.
6.8.3. Динамическая модель системы
Объектная модель представляет статическую структуру проектируемой системы (подсистемы). Однако знания статической структуры недостаточно, чтобы понять и оценить работу подсистемы. Схема, изображенная на рис. 6.8, не годится для описания динамического взаимодействия во время выполнения программы.
Динамическая модель подсистемы строится после того, как объектная модель подсистемы построена и предварительно согласована и отлажена.
Динамическая модель системы представляется диаграммой последовательности и диаграммой состояний объектов.
На рис. 6.9 показана часть диаграммы последовательности для РКП. Время изменяется сверху вниз. Каждый объект представлен вертикальной линией. Сообщение от одного объекта к другому изображается горизонтальной стрелкой между вертикальными линиями. Возврат управления (и, возможно, результата) в объект представлен стрелкой в обратном направлении. Некоторые авторы используют для этих целей пунктирную стрелку. Комментарий справа от рисунка более подробно объясняет взаимодействие.
Благодаря наличию оси времени диаграмма последовательности лучше описывает последовательность событий в процессе работы программы. Поэтому диаграммы последовательности
являются полезным средством документирования для сложных программных систем.
Состояние определяется совокупностью текущих значений атрибутов. Например, банк может иметь состояния — платежеспособный и неплатежеспособный (когда большая часть банков одновременно оказывается во втором состоянии, наступает банковский кризис). Состояние определяет реакцию объекта на поступающее в него событие (в том, что реакция различна, нетрудно убедиться с помощью банковской карточки: в зависимости от состояния банка обслуживание или реакция банка на предъявление карточки будет разным). Реакция объекта на событие может включать некоторое действие и/или перевод объекта в новое состояние.
При определении состояний мы не рассматриваем те атрибуты, которые не влияют на поведение объекта, и объединяем в одно состояние все комбинации значений атрибутов и связей, которые дают одинаковые реакции на события.
Диаграмма состояний связывает события и состояния. При приеме события следующее состояние системы зависит как от ее текущего состояния, так и от события (рис. 6.10). Смена состояния называется переходом. Диаграмма состояний — это граф, узлы которого представляют состояния, а направленные дуги, помеченные именами соответствующих событий, — переходы. Диаграмма состояний позволяет получить последовательность состояний по заданной последовательности событий.
Являясь описанием поведения объекта, диаграмма состояний должна описывать, что делает объект в ответ на переход в
некоторое состояние или на возникновение некоторого события. Для этого в диаграмму состояний включаются описания активностей и действий.
Активностью называется операция, связанная с каким-либо состоянием объекта (она выполняется, когда объект попадает в указанное состояние); выполнение активности требует определенного времени. Примеры активностей: выдача картинки на экран телевизора, телефонный звонок, считывание порции файла в буфер и т. п.; иногда активностью бывает просто приостановка выполнения программы (пауза), чтобы обеспечить необходимое время пребывания в соответствующем состоянии (это бывает особенно важно для параллельной асинхронной программы).
Продолжим разработку программы РКП. На следующих этапах уточняется описание объектов. Сначала формализуются способы взаимодействия.
Следует определить, как будет реализован каждый из объектов. Объект, характеризуемый только поведением (не имеющий внутреннего состояния — внутренних данных), может быть оформлен в виде функции. Например, объект, заменяющий в строке все прописные буквы на строчные, лучше представить в виде функции. Объекты со многими функциями лучше реализовать в виде классов. Каждой обязанности, перечисленной на CRC-карточке, присваивается имя. Эти имена станут затем названиями функций или процедур. Вместе с именами определяются типы аргументов, передаваемых функциям и процедурам. Затем описывается вся информация, содержащаяся внутри класса объекта. Если объекту требуются некоторые данные для выполнения конкретного задания, их источник (аргумент функции, глобальная или внутренняя переменная) должен быть описан явно.
Как только для всех действий выбраны имена, CRC-карточка для каждого объекта переписывается заново с указанием имен функций и списка формальных параметров (рис. 6.11). Теперь CRC-карточка в себе отражает всю информацию для записи описания класса, порождающего объект, отображенный на этой карточке.
Идея классификации классов объектов программы через их поведение имеет чрезвычайное следствие. Программист знает, как использовать объект, разработанный другим программистом,
и при этом ему нет необходимости знать, как он реализован. Пусть классы шести объектов РКП разрабатываются шестью программистами. Программист, разрабатывающий класс объекта Meal, должен обеспечить просмотр базы данных с рецептами и выбор отдельного рецепта при составлении блюда. Для этого объекта Meal просто вызывает функцию browse, привязанную к объекту RecipeDatebase. Функция browse возвращает отдельный рецепт Recipe из базы данных. Все это справедливо вне зависимости от того, как конкретно реализован внутри RecipeDatabase просмотр базы данных.
Вероятно, в реальном приложении будет много рецептов. Однако все они будут вести себя одинаково. Отличается лишь состояние: список ингредиентов и инструкций по приготовлению. На ранних стадиях разработки нас должно интересовать поведение, общее для всех рецептов. Детали, специфические для отдельного рецепта, не важны. Заметим, что поведение ассоциировано с классом, а не с индивидуальным представителем, т. е. все экземпляры класса воспринимают одни и те же команды и выполняют их сходным образом. С другой стороны, состояние является индивидуальным, и это видно на примере различных экземпляров класса Recipe. Все они могут выполнять одни и те же действия (редактирование, вывод на экран, печать), но используют различные данные.
Двумя важными понятиями при разработке программ является зацепление (cohesion) и связанность (coupling). Связанность — это мера того, насколько отдельный объект образует логически законченную, осмысленную единицу. Высокая связанность достигается объединением в одном объекте соотносящихся (в том или ином смысле) друг с другом функций. Наиболее часто функции оказываются связанными друг с другом пути необходимости иметь доступ к общим данным. Именно это объединяет различные части объекта Recipe.
В частности, зацепление возникает, если один объект должен иметь доступ к данным (состоянию) другого объекта. Следует избегать подобных ситуаций. Возложите обязанность осуществлять доступ к данным на объект, который ими владеет. Например, за редактирование рецептов ответственность должна лежать на объекте RecipeDatabase, поскольку именно в нем впервые в этом возникает необходимость. Но тогда объект RecipeDatabase должен напрямую манипулировать состоянием отдельных рецептов (их внутренними данными: списком ингредиентов и инструкциями по приготовлению). Лучше избежать столь тесного сцепления, передав обязанность редактирования непосредственно рецепту.
С другой стороны, зацепление характеризует взаимосвязь между объектами программы. В общем случае желательно, как только можно, уменьшить степень зацепления, поскольку связи между объектами программы препятствуют их модификации и мешают дальнейшей разработке или повторному использованию в других программах.
6.8.5. Совместное рассмотрение трех моделей
В результате анализа получаем три модели: объектную, динамическую и функциональную. При этом объектная модель составляет базу, вокруг которой осуществляется дальнейшая разработка. При построении объектной модели в ней не всегда указываются операции над объектами, так как с точки зрения объектной модели объекты — это прежде всего структуры данных. Поэтому разработка системы начинается с сопоставления действиям и активностям динамической модели и процессам функциональной модели операций и внесения этих операций в объектную модель. С этого начинается процесс разработки программы, реализующей поведение, которое описывается моделями, построенными в результате анализа требований к системе.
Поведение объекта задается его диаграммой состояния; каждому переходу на этой диаграмме соответствует применение к объекту одной из его операций; можно каждому событию, полученному объектом, сопоставить операцию над этим объектом, а каждому событию, посланному объектом, сопоставить операцию над объектом, которому событие было послано. Активности, запускаемой переходом на диаграмме состояний, может соответствовать еще одна (вложенная) диаграмма состояний.
Результатом этого этапа проектирования является уточненная объектная модель, содержащая все классы проектируемой программной системы, в которых специфицированы все операции над их объектами.
Delphi и С++ Builder представляет собой визуальное средство разработки корпоративных информационных систем. В С++ Builder используется язык объектно-ориентированного программирования С++, а в Delphi — Object Pascal. Несмотря на это, обе среды используют одни и те же модули библиотеки визуальных компонент, написанных на Object Pascal.
Каждый тип органов управления системы описывается классом, а помещаемые на формы конкретные органы управления являются объектами соответствующих классов. Так, например, Buttonl, Button2, ..., ButtonN являются объектами класса TButton; Editl, Edit2, ..., EditM — объектами класса ТЕdit и т. п. Когда пользователь создает форму в визуальной интегрированной среде, он, по сути (в отличие от других органов управления), создает новый класс, объектом которого будет форма, появляющаяся при выполнении приложения (например, класс — TForm 1, объект класса — Forml).
С целью уяснения процессов разработки иерархии классов, предпримем попытку ретроспективного анализа иерархии классов системы Delphi/С++ Builder.
В процессе анализа была расписана иерархия классов, избранных для примера органов управления, выделены некоторые обязанности, которые мог бы наложить на них разработчик, а затем на основе сравнения списков выделенных обязанностей предпринята попытка обосновать иерархию классов, принятую в средах Delphi/С++ Builder.
Следует отметить, что данный анализ проводится с чисто учебными целями, поэтому здесь не рассмотрены все органы управления, а также все обязанности, которые может наложить разработчик на тот или иной орган управления.
Рассмотрим органы управления, которые могут быть получены транспортировкой их при помощи мыши с палитры компонент Delhi/С++ Builder.
— TButton — обыкновенная кнопка;
— TRadioButton — радио кнопка (группа кнопок с зависимой фиксацией, обеспечивающей возможность выбора лишь одной кнопки из группы);
— TListBox — обычный список;
— TDBListBox — список для работы с таблицами данных;
— TDataSource — источник данных (является посредником между элементами DataAccess: Table, Query, — и органами управления базами данных DataControls: DBGrid, DBEdit и т. п.).
Попытаемся в общих чертах прокомментировать данную иерархию и обосновать ее на основе метода распределения обязанностей. Ниже приведен набор обязанностей для перечисленных компонентов.
Обязанности объектов класса TDataSourse:
— контролировать доступ пользователя к элементам TDataSet; — обеспечивать возможность определения, подключен ли TDataSource к некоторому элементу TDataSet;
— обеспечивать возможность работы с дочерними компонентами;
— обеспечивать возможность копирования данных в другой объект того же класса;
— обеспечивать возможность уничтожения объекта с высвобождением памяти;
— обеспечивать возможность посылать сообщения;
— определять имя класса, объектом которого является данный элемент.
Обязанности объектов класса TButton:
— обрабатывать сообщения WM_ LBUTTONDOWN и WM_ LBUTTONDBLCLK (нажатие и двойное нажатие левой кнопки мыши);
— программно эмулировать нажатие кнопки;
— обрабатывать сообщения от клавиатуры;
— получать фокус ввода;
— определять, имеет ли фокус ввода;
— определять, может ли иметь фокус ввода (например, если элемент невидим, ему нельзя передать фокус ввода);
— обрабатывать сообщение ВМ_ CLICK (происходит после нажатия кнопки мыши);
— становиться видимым и невидимым;
— перерисовываться;
— обеспечивать возможность перевода точки из системы координат окна в систему координат экрана;
— хранить идентификатор родителя (с возможностью изменить родителя);
— обеспечивать возможность работы с дочерними компонентами;
— обеспечивать возможность копирования данных в другой
объект того же класса;
— обеспечивать возможность уничтожения объекта с высвобождением памяти;
— обеспечивать возможность посылать сообщения;
— определять имя класса, объектом которого является данный элемент.
Обязанности объектов класса TRadioButton:
— обрабатывать сообщения WM_ LBUTTONDOWN и WM_ LBUTTONDBLCLK (нажатие и двойное нажатие левой кнопки мыши);
— определять, какая выбрана кнопка из группы кнопок;
— обрабатывать сообщения от клавиатуры;
— получать фокус ввода;
— определять, имеет ли фокус ввода;
— определять, может ли иметь фокус ввода (например, если элемент невидим, ему нельзя передать фокус ввода);
— обрабатывать сообщение BM_ CLICK (происходит после нажатия кнопки мыши);
— становиться видимым и невидимым;
— перерисовываться;
— обеспечивать возможность перевода точки из системы координат окна в систему координат экрана;
— хранить идентификатор родителя (с возможностью изменить родителя);
— обеспечивать возможность работы с дочерними компонентами;
— обеспечивать возможность копирования данных в другой
объект того же класса;
— обеспечивать возможность уничтожения объекта с высвобождением памяти;
— обеспечивать возможность посылать сообщения;
— определять имя класса, объектом которого является данный элемент.
Обязанности объектов класса TListBox:
— очищать список;
— обеспечивать возможность нахождения нескольких элементов из списка;
— определять номер элемента списка по координатам точки, принадлежащей объекту класса TListBox;
— обрабатывать сообщения от клавиатуры;
— получать фокус ввода;
— определять, имеет ли фокус ввода;
— определять, может ли иметь фокус ввода (например, если элемент невидим, ему нельзя передать фокус ввода);
— обрабатывать сообщение ВМ_ CLICK (происходит после нажатия кнопки мыши);
— становиться видимым и невидимым; — перерисовываться;
— обеспечивать возможность перевода точки из системы координат окна в систему координат экрана;
— хранить идентификатор родителя (с возможностью изменить родителя);
— обеспечивать возможность работы с дочерними компонентами;
— обеспечивать возможность копирования данных в другой объект того же класса;
— обеспечивать возможность уничтожения объекта с высвобождением памяти;
— обеспечивать возможность посылать сообщения;
— определять имя класса, объектом которого является данный элемент.
Обязанности объектов класса TDBListBox:
— обеспечивать связь с источником данных (TDataSource);
— очищать список;
— обеспечивать возможность нескольких элементов из списка;
— определять номер элемента списка по координатам точки, принадлежащей объекту класса TDBListBox;
— обрабатывать сообщения от клавиатуры;
— получать фокус ввода;
— определять, имеет ли фокус ввода;
— определять, может ли иметь фокус ввода (например, если элемент невидим, ему нельзя передать фокус ввода);
— обрабатывать сообщение ВМ CLICK (происходит после нажатия кнопки мыши);
— становиться видимым и невидимым;
— перерисовываться;
— обеспечивать возможность перевода точки из системы координат окна в систему координат экрана;
— хранить идентификатор родителя (с возможностью изменить родителя);
— обеспечивать возможность работы с дочерними компонентами;
— обеспечивать возможность копирования данных в другой объект того же класса;
— обеспечивать возможность уничтожения объекта с высвобождением памяти;
— обеспечивать возможность посылать сообщения;
— определять имя класса, объектом которого является данный элемент.
При внимательном рассмотрении обязанностей, вменяемых объектам перечисленных классов, можно заметить, что некоторые из них совпадают, т. е. оказываются общими для объектов разных классов. Следует также отметить, что некоторые из обязанностей являются общими для объектов всех классов, а некоторые — лишь для ограниченного набора.
Логично было бы ввести дополнительные классы, объекты которых имели бы общие для рассмотренных классов обязанности. Тогда рассмотренные классы (TDataSource, TButton, TRadio Button, TListBox, TDBListBox) могли бы унаследовать функции, обеспечивающие выполнение этих обязанностей у введенных дополнительных классов (в объектно-ориентированном программировании имеется механизм, который так и называется — механизм наследования, — который делает доступными из дочерних классов свойства и методы, с учетом прав доступа, разумеется из родительских (или базовых) классов).
Попытаемся выделить упомянутые классы и назначить им их обязанности:
Обязанности объектов класса Класс_1:
— обеспечивать возможность работы с дочерними компонентами;
— обеспечивать возможность копирования данных в другой объект того же класса;
— обеспечивать возможность уничтожения объекта с высвобождением памяти;
— обеспечивать возможность посылать сообщения;
— определять имя класса, объектом которого является данный элемент.
Обязанности объектов класса Класс_2:
— обрабатывать сообщения от клавиатуры;
— получать фокус ввода;
— определять, имеет ли фокус ввода;
— определять, может ли иметь фокус ввода (например, если элемент невидим, ему нельзя передать фокус ввода);
— обрабатывать сообщение ВМ_ CLICK (происходит после нажатия кнопки мыши);
— становиться видимым и невидимым;
— перерисовываться;
— обеспечивать возможность перевода точки из системы координат окна в систему координат экрана;
— хранить идентификатор родителя (с возможностью изменить родителя).
Обязанность объектов класса Класс_ 3:
обрабатывать сообщения WM_LUBTTONDOWN и WM_ LBUTTONDBLCLK (нажатие и двойное нажатие левой кнопки мыши).
Обязанности объектов класса Касс_4:
— очищать список;
— обеспечивать возможность нахождения нескольких элементов из списка; — определять номер элемента списка по координатам точки, принадлежащей объекту класса TDBListBox.
Теперь, исключив из множества обязанностей объектов рассматриваемых классов те, которые переданы дополнительным классам, перечислим оставшиеся обязанности:
Обязанности класса TDataSource:
— контролировать доступ пользователя к элементам TDataSet;
— обеспечивать возможность определения, подключен ли TDataSource к некоторому элементу TdataSet.
Итак, обязанности класса Tbutton — программно эмулировать нажатие кнопки; класса TradioButton — определять, какая из кнопок с зависимой фиксацией выбрана; классов TlistBox и TDBListBox — обеспечивать связь с источником данных (TDataSource).
В результате получена иерархия классов (рис. 6.12), которая обеспечивает исключение избыточности кода (функции, осуществляющие выполнение объектами разных классов одинаковых обязанностей, кодируются приблизительно, а иногда совершенно
одинаково), повышает обозримость кода программы, а следовательно, потенциально сокращает время на ее отладку.
Теперь рассмотрим на рис. 6.13 фрагмент схемы иерархии классов для перечисленных элементов (до корневого суперкласса).
Сравнивая рисунки 6.12 и 6.13, можно заметить:
1. Класс_ 1 в нашей иерархии соответствует ветви TObject → TPersistent → TComponent;
2. Класс_ 2 — TControl →TWinControl;
3. Класс_ 3 — TbuttonControl;
4. Класс_ 4 — TCustomListBox.
Если с двумя последними классами все понятно, то возникает вопрос, почему первые два класса нашей иерархии соответствуют не одному, а целым цепочкам классов С++ Builder? Дело в том, что в рассматриваемом примере описаны не все классы С++ Builder. Поэтому те из них, которые приводят к разветвлению двух первых цепочек, здесь просто не учтены. Например, элемент TImage, предназначенный для расположения на формах графических изображений, имеет следующую цепочку наследования
классов: TObject →TPersistent → TComponent →TControl → TGraphicsControl, — т. е. цепочка TControl → TWinControl превращается в дерево, на котором классы TWinControl и TGraphics Control оказываются на одном уровне. Данный фрагмент схемы иерархии классов изображен на рис 6.14.