Этюды в тональности C#. TV паттерн программирования 2

Этюды в тональности C#. TV паттерн программирования

Продолжение следует. Начало в КГ 1 от 12.01.04.

В предыдущей статье мы с вами рассмотрели достоинства и недостатки MVC паттерна программирования и начали создавать приложение, базирующееся на паттерне, использовавшемся в библиотеке Turbo Vision. Эту библиотеку фирма Borland выпустила для своего компилятора Turbo Pascal 6.0 еще в добрые старые времена господства MSDOS. Сегодня мы продолжим рассмотрение паттерна, названного мной "Базовый TV паттерн программирования". От полного TV-паттерна этот вариант отличается более упрощенной функциональностью. Я намеренно ее упрощаю только для того, чтобы вам было легче ухватить используемые в нем идеи. Продолжим? Итак...

Первое, о чем нам стоит подумать, разрабатывая наше приложение, это то, каким именно образом мы с вами будем реализовывать в нем команды. Ведь команды — это тот клей, который связывает воедино разные блоки кода нашего приложения.
В библиотеке Turbo Vision команды были объявлены как именованные константы, которые в дальнейшем упаковывались в структуру (record), именуемую событием. Сама идеология паттерна TV подразумевает, что команда, возникшая в одном из блоков нашего кода, должна посетить в идеале все остальные блоки кода. Поэтому мы с вами изначально обречены на многочисленные вызовы разных методов, которым эта команда передается в качестве параметра. С помощью этих методов объекты будут спихивать возникающие события друг другу.
В языке C# структура (struct) является типом данных, передаваемым по значению, в противовес объектам, передаваемым как ссылка (указатель на объект). Поэтому, если мы с вами пойдем по пути, предложенному Borland, нам придется гонять взад-вперед по нашему приложению довольно-таки объемные блоки данных. В случае же использования объекта мы с вами передаем всего лишь указатель на него, что намного быстрее. Поэтому использование объектов в нашей ситуации более предпочтительно.
Вместе с тем, реализуя события в виде объекта, мы сильно захламляем "кучу". Для каждого вновь генерируемого события выделяется оперативная память, которая впоследствии после уничтожения события возвращается обратно сборщиком мусора (Garbage Collector). Сборка мусора — процесс неторопливый, поэтому наша программа теоретически может занимать под себя довольно много оперативной памяти. Это не есть хорошо. Впрочем, мы можем с этим бороться, принудительно вызывая Garbage Collector сразу после того, как мы обработали много команд подряд.
Обдумав все это, я решил, что применение объектов в качестве носителей событий все-таки более выгодно по сравнению с использованием в этой роли структур. Если вы считаете по-другому, ради бога, используйте структуры. На функционировании предлагаемого паттерна это никак не скажется.
Прежде чем создавать сам объект события, давайте решим, как будет выглядеть идентификатор команды. Надо же нам как-то выяснять, какая именно команда вызвала то или иное событие. На мой взгляд, эту задачу лучше всего выполняет перечисление C# (enum). Я буду использовать следующее перечисление, в котором указаны все задействованные мной в этом примере команды:
public enum MyCmd { None, FileOpen, FileSave,TextChange, LogAdd }

В случае необходимости вы без труда сможете расширить набор используемых команд, просто добавив новую константу в перечисление. Весь остальной код приложения при такой методике исправлять не придется.
Дальше давайте приступим к созданию объекта, инкапсулирующего в себе вновь возникшее событие. Он будет довольно несложен.
public class TMyEvent { public MyCmd cmd = MyCmd.None; public object sender=null; public objec
t Parameter=null; public TMyEvent(object sender,MyCmd cmd) { this.sender=sender; this.cmd = cmd; } p
ublic TMyEvent(object sender,MyCmd cmd,object Parameter):this(sender,cmd) { this.Parameter=Parameter
; } }

В теле объекта события инкапсулировано несколько полей. Первое поле, называющееся cmd, — это команда, приведшая к возникновению данного события.
Второе поле, sender, хранит информацию о том объекте, который сгенерировал данное событие. В предлагаемой упрощенной модели эта информация поможет нам правильно маршрутизировать данное событие. Если вы надумаете развивать предложенную мной модель, вы можете воспользоваться полем sender для того, чтобы из объекта — получателя события вызывать методы объекта — инициатора этого события. Поле sender может вам пригодиться также для адресной маршрутизации событий вместо используемой мной тут широковещательной маршрутизации... Хм. Что-то я размечтался и забежал далеко вперед.
Следующее поле, названное мной Parameter, — это опциональный параметр, который, возможно, потребуется для выполнения этой конкретной команды. Я выбрал для него тип object потому, что, используя полиморфизм, мы сможем подставить на место этого параметра любой произвольный объект. Так, если для выполнения команды потребуется параметр-строка, именно его мы и передадим. Если команде потребуется более сложная структура параметра, мы всегда сможем создать вспомогательный объект, в который он и будет упакован.
И отправитель сообщения, и его получатель хорошо осведомлены о типе параметров, которыми они оперируют, поэтому легко приведут его к тому виду, который им требуется. Для всех остальных блоков кода параметр останется безликим объектом, до которого им, как и до самой команды, вовсе нет никакого дела. В том случае, если команде не требуется параметр, мы инициализируем эту переменную значением null.
Помимо вышеописанных свойств, наш объект события имеет два конструктора, введенных для удобства последующего программирования. Первый конструктор принимает в качестве параметров отправителя и команду, а второй — отправителя, команду плюс параметр. Думаю, достоинства использования этих двух конструкторов самоочевидны, поэтому я не стану на них подробно останавливаться. Вы все поймете позже из примеров.
Ну вот, мы с вами разобрались с командами и их упаковкой в событие. Теперь давайте подумаем над тем, как именно мы собираемся научить произвольные объекты NET Framework их обрабатывать.

Программисты, создававшие Turbo Vision, просто создали свою иерархию наследуемых объектов, в которые обработка событий была вложена изначально, на уровне базового предка. Для нас такой подход неприемлем, так как мы хотим научить работать с нашими событиями любые произвольные объекты Net Framework. Ну и что же нам делать?
В этой ситуации к нам на выручку придет механизм интерфейсов С# (interface). Мы просто создадим нужный интерфейс, а затем, при необходимости научить некий произвольный объект понимать наши события, реализуем его в коде его потомка. Вот и все!
Если вы ничего не поняли, не расстраивайтесь. Чуть позже вы разберетесь, в чем тут соль. Раз уж мы с вами решили использовать интерфейс, давайте для начала подумаем, как он будет выглядеть. В Turbo Vision у каждого объекта иерархии имелся единственный метод — обработчик событий. Назывался он HandleEvent. В предлагаемой мной упрощенной модели паттерна мы используем два метода. Один для обработки входящих событий (HandleEvent), а второй — для информирования вышестоящих элементов о том, что внутри него возникло новое событие (OnMyCommand).
Чем-то предлагаемый мной подход похож на шоссе с двусторонним движением. Оба метода с точки зрения числа и типа параметров выглядят одинаково. Так и должно быть, так как они выполняют одинаковую функцию, ну прямо как две полосы движения на шоссе. По одной полосе машины едут туда, а по другой обратно.

Вот только оформим мы их в интерфейсе по-разному: метод OnMyCommand мы оформим как событие, а метод HandleEvent — как обычный метод. Так будет проще для изучения, хотя, на мой взгляд, это и не совсем правильно — значительно лучше было бы оформить HandleEvent как делегат. Его использование позволит нам проводить хитрые финты на манер динамической смены обработчика событий или и вовсе делегирования обработки событий другим объектам. Но применение делегата запутает мне изложение самой идеи, а вам — ее понимание, поэтому давайте пока остановимся на обычном методе. Если вас заинтересовала идея с делегатом, поиграйтесь с ее реализацией самостоятельно.
С учетом всего вышесказанного описание интерфейса обработки событий будет выглядеть следующим образом:
public delegate void MyCommandHandler(TMyEvent MyEvent); public interface IHandleEvent { event
 MyCommandHandler OnMyCommand; void HandleEvent(TMyEvent MyEvent); }

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

Ну вот мы с вами и описали все общие логические структуры, необходимые для реализации базового TV-паттерна. Оформите их в отдельный файл, назовите его, скажем, GLOBALS.CS и подключите его к своему проекту. Как, вы его еще не создали? Так создавайте побыстрей. Сделайте новый проект C# Windows Application и подключите к нему файл с этими глобальными структурами.
Как правило, интерфейс Windows-приложения состоит из нескольких стандартных "кирпичиков". К таким "кирпичикам" относятся строка статуса, главное меню, тулбар и тому подобные вещи. Вот давайте мы такое приложение и реализуем. Мы с вами сделаем, скажем, несложный текстовый редактор. Дабы проще отслеживать прохождение событий, мы вместо строки статуса добавим целый ListBox. В нем будут накапливаться те строчки, которые обычно быстро пробегают по статусной строке. Сверху редактора у нас будет находиться тулбар с кнопками. Именно с его помощью мы и будем посылать управляющие команды. Ни тулбар, ни редактор, ни ListBox ничего не знают о существовании друг друга. Они просто в момент своего создания подключаются к нашему единому каналу маршрутизации событий и в дальнейшем управляются только проходящими по нему командами.

Реализацию нашего приложения мы начнем с тулбара. Создаем новый класс и указываем его предком System. Windows. Panel. Также говорим, что этот класс реализует у нас интерфейс IHandleEvent. После того как вы сообщите Visual Studio о своем решении, она любезно оформит для вас заготовки для всех методов этого интерфейса. А именно событие OnMyCom-mand и обработчик Handle-Event.
В целях упрощения последующего программирования и уменьшения объема кода я предлагаю вам сразу создать небольшой вспомогательный класс, описывающий командную кнопку. Основное отличие этой кнопки от Button заключается в том, что экземпляр нашего объекта хранит в себе команду, на которую он настроен. Кроме того, мы добавим в свой класс конструктор, который упростит создание этого объекта в последующем коде. Сейчас я приведу вам полный листинг класса MyToolbar, и ниже мы рассмотрим подробно назначение входящих в него элементов. Этот листинг самый длинный в моей статье. Мне хотелось бы, чтобы вы в нем разобрались, пока у вас голова еще свежая. В нем используются практически все приемы, используемые в предлагаемом паттерне. Разобравшись с тулбаром, вы без труда поймете и все остальное. Поэтому я прошу вас внимательно изучить предлагаемый код.
using System.Windows.Forms; public class MyToolbar:Panel,IHandleEvent { // вспомогательный кла
сс кнопки public class MyButton:Button { public MyCmd cmd=MyCmd.None; public MyButton(string text,My
Cmd cmd) { this.Text = text; this.cmd = cmd; } } // реализация интерфейса IHandleEvent public event 
MyCommandHandler OnMyCommand; public void HandleEvent(TMyEvent MyEvent){ if(OnMyCommand!=null) { OnM
yCommand(new TMyEvent( this, MyCmd.LogAdd,"MyToolbar process "+MyEvent.cmd.ToString() )); 
} } // конструктор объекта public MyToolbar() { this.Controls.AddRange( new MyButton[]{ new MyButton
("Open",MyCmd.FileOpen), new MyButton("Save",MyCmd.FileSave) }); foreach(Control
 ctrl in this.Controls) { if (ctrl is MyButton) { (ctrl as MyButton).Click += new System.EventHandle
r(btn_Click); (ctrl as MyButton).Width = 60; (ctrl as MyButton).Dock = DockStyle.Left; } } this.Heig
ht=24; this.Dock = DockStyle.Top; } // обработчик нажатия кнопок на тулбаре private void btn_Click(o
bject sender,System.EventArgs e){ if(OnMyCommand!=null) { OnMyCommand(new TMyEvent( this, MyCmd.LogA
dd,"MyButton generate "+(sender as MyButton).cmd) ); OnMyCommand(new TMyEvent(this,(sender
 as MyButton).cmd,null)); } } }

Итак, что же мы тут имеем. Про вспомогательный класс MyButton я вам уже рассказывал. Следом за ним в листинге идет реализация интерфейса IHandleEvent, а именно обработчик события OnMyCommand и метод HandleEvent. Событие OnMyCommand — это наша дверь в канал исходящих событий. Если нашему тулбару потребуется проинформировать внешний мир о каком-либо происшествии внутри себя, например, нажатии кнопки, он будет запускать информацию о нем во внешний мир с помощью этого объявленного публичного события.
Метод HandleEvent, напротив, принимает события из внешнего мира. Так как я в нашем приложении не придумал какой-либо функциональности, требующей обработки нашим тулбаром, в этом методе у нас стоит заглушка. Сначала она проверяет, подключен ли канал исходящих событий (OnMyCommand не равен null), и, если канал доступен, отправляет в него свою собственную вновь созданную команду. В этой команде (MyCmd. LogAdd) он просит добавить в лог строчку текста с описанием той команды, которую его попросили выполнить. В реальном приложении вы можете разместить тут обработку команд, отвечающих за разрешение-запрет кнопок тулбара в зависимости от происходящих в мире событий.
Следующим в нашем объекте тулбара идет его конструктор. В нем мы создаем две кнопки и добавляем их на поверхность тулбара. Затем в цикле foreach мы перебираем все элементы, находящиеся на поверхности нашего тулбара, и таким образом находим только что созданные нами кнопки. Каждой из них мы добавляем обработчик ее нажатия (один на всех), указываем ширину и говорим, что они должны прижиматься к левому краю тулбара. Зачем я делаю это так извращенно? А затем, чтобы избежать занудного создания, по крупному счету, не нужных мне временных переменных. Посмотрите код, который генерирует мастер Visual Studio при создании элементов, и обратите внимание, насколько предлагаемое мной решение короче и изящнее.

Разобравшись с кнопками, настраиваем высоту нашего тулбара и указываем, что он должен прижиматься к верхней части окна. Последний метод нашего объекта тулбара — это обработчик нажатия сразу всех кнопок. Если вы раньше обращали внимание на код нажатия кнопок, генерируемый мастером Visual Studio, то знаете, что по принятым в нем правилам каждой кнопке назначается свой собственный метод-обработчик. На мой взгляд, такой подход, во-первых, неудобен с точки зрения кодирования ответной реакции, а во-вторых, захламляет нашу программу большим количеством "лишних" функций. Обилие функций затрудняет восприятие программы как самому программисту, так и тем, кто будет читать код вслед за ним.
Итак, вернемся к нашему обработчику нажатия кнопки. Так как мы точно знаем, что метод вызван кнопкой типа MyButton, я не стал проверять "национальность" объекта sender, а просто привел его к MyButton. Считав из поля cmd кнопки команду, с которой она ассоциирована, я по описанному выше механизму отправляю во внешний мир событие, информирующее заинтересованные стороны о факте нажатия на кнопку. Перед отправкой этого события я отправляю еще одну, вновь сгенерированную, команду с просьбой добавить в лог строчку о том, что событие сгенерировано именно этой кнопкой.

Давайте теперь остановимся и посмотрим, что именно мы сделали. А сделали мы очень интересную штуку. Наш тулбар является полнофункциональным объектом, который хоть сейчас можно кидать на форму. Например, вот таким образом:
class Form1:Form { ... public Form1() { ... this.Controls.Add(new MyToolbar()); ... } ... }

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

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

Продолжение следует.


Компьютерная газета. Статья была опубликована в номере 02 за 2004 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета