Главная » C# » Создание оболочки в Visual C# (Sharp)

0

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

Сборка компонентов с помощью программы-отражателя

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

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

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

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

Рис.  10.2.  Архитектура  приложения  считывателя/записывателя

Считывание и запись в поток

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

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

Итак, мы хотим обработать следующую командную строку:

type lotto.txt | TextProcessor.exe

Неудачная  операция  чтения  из  канала  приложением  TextProcessor.exe  вызывает исключение, указывающие, что переданные по каналу данные не были прочитаны.

ПРИМЕЧАНИЕ

Необходимо, что файлы lotto.txt и TextProcessor.exe находились в одной папке. По умоанию фай л TextProcessor.exe находится в папке [Visual Studio project]\bin\debug. Скируйте фай л TextProcessor.exe в папку, содержащу ю  фай л  lotto.exe,  или  наоборот. Можно  также  поместить  эти  два  файла  в  какую-либо другую  папку.

В   архитектуре   приложения   TextProcessor код   начальной  загрузки   находится

В сборке Readerwriter. Консольное  Приложение TextProcessor ДОЛЖНО вызвать

код начальной загрузки и создать экземпляр локального типа, который реализует интерфейс iprocessor. Метод Main о приложения TextProcessor выглядит таким образом:

using ReaderWriter;

namespace TextProcessor { public static class Program {

static void Main(string[] args) {

Bootstrap.Process(args, new LottoTicketProcessor());

}

}

}

(Данный этап будет лучшим моментом для добавления ссылки на  проект ReaderWriter. Для этого выберите команды меню Reference s | Ad d Referenc e | Project s  |  ReaderWriter. )

Метод  Processor .Main ()   передает  все  имеющиеся  аргументы  (которые  хранятся в массиве args) процедуре Bootstrap. Process о, которая в действительности волняет обработку.  Класс  LottoTicketProcessor реализует интерфейс  IProcessor и имеет временное назначение отражения данных. Исходный код для определения интерфейса IProcessor таков:

namespace ReaderWriter {  public interface IProcessor {

string Process(string input);

}

}

Интерфейс  IProcessor содержит  один  метод  Process о,  который  принимает  стру,  подлежащую  обработке,  и  возвращает обработанную  строку.

Исходный  код для  реализации  LottoTicketProcessor выглядит таким  образом:

using ReaderWriter;

namespace TextProcessor {

// TODO:  Finish implementing the class class LottoTicketProcessor : IProcessor {

public string Process(string input) { return input;

}

}

}

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

Для метод а Bootstrap. Process () МОЖН О было бы определить КЛДСС EchoProcessor, после чего передать этот класс. Но помните, что на данном этапе мы просто волняем сборку компонентов, и класс EchoProcessor не является настоящим рабим классом, который будет использоваться в завершенной программе. Настоящим рабочим классом является класс LottoTicketProcessor, хотя временно он всего лишь отражает переданные ему данные.

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

using System.10;                            •

namespace ReaderWriter {

public static class Bootstrap {

public static void Process(string[] args, IProcessor processor) { TextReader reader = Console.In;

TextWriter writer = Console.Out;

writer.Write(processor.Process(reader.ReadToEnd()));

}

}

}

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

Присвоение значений консольным  потокам заключается в присвоении членов даых in и Out переменным TextReader и TextWriter соответственно. Код, вызающий метод processor. Process о, отправляет поток процессору и ожидает оет, который приходит в виде другого потока.

Зная, что TextReader и TextWriter являются общими интерфейсами или технички абстрагируют базовые классы, у нас может возникнуть соблазн переконструовать интерфейс IProcessor следующим образом:

namespace ReaderWriter {  public interface IProcessor {

void Process(TextReader input, TextWriter output);

}

}

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

ПРИМЕЧАНИЕ

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

Реализовав все каналы, можно скомпилировать исходный код и исполнить команду для чтения данных, поставляемых по каналу. Но для этого нужен файл lotto.txt, сержащий эти данные. Для примера создайте текстовый файл  lotto.exe и вставьте в него следующие данные:

1970.01.10 7 8 12 17 32 40 24 1970.01.17 7 12 22 24 29 40 36 1970.01.24 16 22 25 27 30 35 24 1970.01.31 3 11 21 22 24 39 8 1970.02.07 2 5 11 16 18 38 37

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

Теперь выполните следующую команду:

type lotto.txt | TextProcessor.exe

В результате на экран должно быть выведено содержимое файла lotto.exe. Если вы получили такие результаты, то ваши данные успешно обрабатываются приложенм, означая, что все его компоненты взаимодействуют должным образом.

НЕ      ЗАБУДЬТЕ      РЕАЛИЗОВАТЬ       ВСЕ      КОМПОНЕНТЫ

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

11  Зак  555

Одной из проблем  при  работе с С# является необходимость знать не только сам  язык, но также и интерфейс API .NET. В данной книге  мы  не  будем  рассматривать  интеейс API .NET, т. к. вы состаритесь раньше, чем  сможете  прочить  все,  что  можно знать о нем.

Но нам не требуется применять весь обширный интерфейс API .NET для каждого раабатываемого приложения. Будет достаточным лишь знать его общие классы. Нример, классы, применяемые для чтения и записи в потоки или классы для создания элементов интерфейса GUI. Это означает, что вы никогда не  будете  экспертом  по всему интерфейсу API .NET, хотя можете стать  опытным  программистом  на  С#, имеющим хорошие знания общих принципов.   .

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

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

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

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

// TODO: Finish implementing the class

Слово TODO указывает, что это  специальный  тип  комментария.  Он  называется  задей и отслеживается средой Visual С# Express в окне задач Task List. Чтобы открыть это окно,  выберите  команды  меню  View |  Task List,  после чего  выберите  Comments из  выпадающего списка  вверху окна.

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

Другими специальными комментариями  являются  комментарий  HACK,  обозначающий код  с ошибкой,  но  по-быстрому исправленный,  чтобы  работал,  и  комментарий  UNDONE. В версиях Visual Studio иных, чем Express, можно определять свои идентификаторы комментариев. Дополнительную информацию на эту тему см. в статье в  MSDN  "Visual Studio How To: Create Custom Comment Tokens" по адресу http://msdn2.microsoft.com/ en-US/libra  ry/ekwz6akh(VS.80).aspx.

‘  НЕОБХОДИМО  СДЕЛАТЬ:  закончить реализацию  класса.  — Пер.

Реализация считывания и записи в поток

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

tdefine DEBUG_OUTFUT using System;

using System.Text; using System.10;

namespace ReaderWriter. {

public static class Bootstrap { public static void DisplayHelp() {

Console.WriteLine("You need help? Right now?");

}

public static void Process(string[] args, IProcessor processor) { TextReader reader = null;

TextWriter writer = null; if (args.Length ==0 ) {

reader = Console.In; writer = Console.Out;

}

else if (args.Length ==1 ) { if (args[0] == "-help") {

DisplayHelp(); return;

}

else {

reader = File.OpenText(args[0]); writer = Console.Out;

}

}

else if (args.Length ==2 ) { if (args[0] == "-out") { reader = Console.In;

writer = File.CreateText(args[l]);

}

else {

DisplayHelp(); return;

}

}

else if (args.Length == 3) { if (args[0] == "-out") {

reader = File.OpenText(args[2]) ; writer = File.CreateText(args[1]) ;

}

else {

DisplayHelp(); return;

}

}

else {

DisplayHelp(); return;

}

writer.Write(processor.Process(reader.ReadToEndf)));

#if DEBUG_OUTPUT

Console.WriteLine("Argument count(" + args.Length + ")"); foreach (string argument in args) {

Console.WriteLinel"Argument (" + argument + ")");

}

#endif

}

}

}

В коде, перед первым блоком if, значения переменных reader и writer устанавлаются равными null, указывая, что имеются читатель и писатель, но мы не знаем, будут они обращаться к потокам или файлам. Потом в блоках if обрабатываются различные комбинации аргументов командной строки (см. табл. 10.1).

В коде применяется подход таблиц истинности, состоящий в проверке различных состояний и реагирующий на эти состояния. Например, состояние может быть оеделено как: "Если А=Х и BI=Y, тогда выполняем с". Для обработки аргументов командной строки мы определяем все возможные состояния и соответствующие действия.

ПРИМЕЧАНИЕ

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

В первом блоке выполняется проверка на нулевое число аргументов, во втором — на один аргумент и т. д. Рассмотрим первый тест:

if (args.Length ==0 ) { reader = Console.In; writer = Console.Out;

}

В данном случае исходный и конечный потоки данных номеров лотерейных билов являются входящими и выходящими консольными потоками. Код присваивает значения переменным reader и writer.

Если командная строка не содержит аргументов  или в случае положительных рультатов одного из тестов, вызывается реализация iProcessor: writer.Write(processor.Process(reader.ReadToEnd()));

Код  сразу  же  исполняет  методы  writer.Write о, processor.Process()  и reader. ReadToEnd (), не проверяя, указывают ли писатель, обработчик или читель на действительные объекты. Это может показаться основанием для добавлия кода для выполнения такой проверки, но в этом нет абсолютно никакой неоодимости. Такая проверка подразумевала бы, что наш блок тестирования истинностных значений является незавершенным, и мы не продумали все комбации, присваивающие значения переменным reader и writer.

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

TestProcessor.exe -help

или:

TestProcessor.exe lotto.txt

В первом  случае  мы  имеем  дело  с  явным  параметром  командной  строки  -help. А во втором случае аргументом является идентификатор файла, содержащего входные данные. Поэтому второй блок if проверки количества аргументов содеит вложенный блок if для проверки типа аргумента:

else if (args.Length == 1) { if (args[0] == "-help") {

DisplayHelp();

return;

}

else {

reader = File.OpenText (args [0] ) writer = Console.Out;

} ‘

}

Выполняя проверку на аргумент -help, сразу же после  вызова  метода DisplayHelp () необходимо прервать обработку с помощью  ключевого  слова return. Это исключительно важно, т. к. когда консольное приложение вызывает метод DisplayHelp (), оно, по существу говорит: "Мне безразлично, откуда идут входные данные и куда направляются выходные. Я сейчас занимаюсь совершенно другим, чем обработка данных, и поэтому должно прекратить выполнять их обротку". Если бы мы продолжали обработку данных, то читатель и писатель могли бы обратиться к недействительным состояниям, вызвав, таким образом, исключение.

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

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

#if DEBUG_OUTPUT

Console.WriteLine("Argument count(" + args.Length + ")"); foreach (string argument in args) {

Console.WriteLine("Argument (" + argument + ")");

}

#endif

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

Теперь наша оболочка готова, и все, что остается сделать, — это реализовать обротчик текста.

ИСПОЛЬЗОВАНИЕ       ТАБЛИЦ       ИСТИННОСТИ

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

А

В

Результат

А

=

X (Т)

В

=

Y(Т)

F

А

=

Х(Т)

С

=

Z(Т)

G

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

А

В

Результат

А

=

Х(Т )

В

=

Y(T )

F

А

=

Х(Т )

С

=

Z (Т)

G

А

=

X (F)

В

=

Y (Т)

•р

А

=

X (F)

С

=

Z (Т)

А

=

X(F )

В

=

Y (F)

А

=

X (F)

С

=

Z (F)

•р

А

=

X(F )

В

=

Y (F)

А

=

X (F)

С

=

Z (F)

•р

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

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

При подходе с применением таблиц истинности мы несем некоторую потерю эффеивности, по причине того, что части одних условий такие же, как и части других услий. Например, если одно условие определено как: "Если А=х и B=Y, тогда выполняем F", а второе как: "Если А=х и c=z, тогда выполняем G", то их можно было бы оптимировать, разделив проверку на А=х между двумя состояниями. Но я бы не советовал делать это, т. к. это нарушило бы индивидуальность каждого теста. Я обычно  не удяю  повторений.

Источник: Гросс  К. С# 2008:  Пер. с англ. — СПб.:  БХВ-Петербург, 2009. — 576 е.:  ил. — (Самоучитель)

По теме:

  • Комментарии