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

We use cookies. Read the Privacy and Cookie Policy

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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