Framework design

Автор: Михаил Чащин
The RSDN Group

Источник: RSDN Magazine #5-2003
Опубликовано: 12.06.2004
Версия текста: 1.0
Введение
Что такое framework
С чего начинается framework
Что такое control
Монополия на создание объектов
Форма и логика представления
Форма как отдельный компонент
Framework как библиотека
Типизированный Combobox
Слои
Блокировка формы в Web-приложениях
О добродетели кодогенерации
Ложка дёгтя
Заключение

Введение

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

Поскольку я не смог найти аналога слова framework в русском языке, который абсолютно точно передавал бы смысловую нагрузку этого термина, я предпочёл использовать в статье непереведённый вариант. Это, на мой взгляд, более приемлемо, нежели использование слова «каркас», по причине того, что разработчики, свободно читающие техническую литературу на английском языке (а таких, я думаю, большинство), могут испытывать трудности с восприятием этого русского варианта в контексте данной статьи.

Для чтения статьи желательно знать шаблоны проектирования GoF или хотя бы иметь представление о том, что они собой представляют и как их использовать (GoF, или Gang of Four – это группа авторов, написавших книгу «Design Patterns: Elements of Reusable Object-Oriented Software»). Любой предыдущий опыт проектирования будет полезен, но его наличие не обязательно. Также стоит упомянуть, что примеры кода были написаны на языках C# и Java. Хотя я использовал такие специфические структуры этих языков как свойства и события (C#) и анонимные классы (Java), думаю, что разработчикам, пишущим на других языках, таких как C++, не составит труда понять идеи, представленных в коде. Таким образом, я полагаю, что знание принципов объектно-ориентированного программирования будет вполне достаточно. Вроде бы я ничего не забыл, но если вам что-то ещё понадобится по ходу чтения статьи, то я вам об этом сообщу. :)

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

Что такое framework

Что такое framework? Можно было бы дать точное научное или какое-либо пространное философское определение, но я попытаюсь объяснить, что это такое, а также, зачем кто-либо пытается создать framework, и кто те люди, которые framework создают и используют.

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

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

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

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

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

Приведём пример. Почему .NET Framework называется именно так? За .NET я не ручаюсь, но вот Framework говорит о том, что, во-первых, имеется библиотека готовых решений (включающая в себя функции работы с памятью, строками и событиями). А во-вторых, имеется набор ограничений (например, автоматическая проверка корректности кода и метаданных). И если разработчик попытается уклониться от этих ограничений, то framework напомнит ему об этом.

Теперь нам наверно стоит ответить на очень важный вопрос: чего стоит разработка framework, и какие способности надо иметь?

Начнём с первого правила архитектора программного обеспечения. Если вы хотите спроектировать framework, то не делайте этого. Второе правило - если вы хотите спроектировать framework, то никогда не делайте этого. Третье правило – если вы всё ещё хотите спроектировать framework, то так вам и надо, отвечать за последствия будете сами :)

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

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

С чего начинается framework

Итак, с чего начинался разработанный мной framework. Нет, не с картинки в букваре, а с картины на рабочем месте. День изо дня я решал одни и те же проблемы, и, казалось, не было им ни конца, ни края. В моём случае проблема – это формы. Тысячи форм.

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

Вы когда-нибудь пытались создать приложение, состоящее из хотя бы нескольких сотен форм на C++ с использованием MFC? Неблагодарное это занятие, скажу я вам, особенно когда каждый клиент хочет чего-то своего. Вы начинаете хранить отдельные файлы ресурсов для каждого клиента. Изменения в формах приходится объединять перед построением всего приложения. Сложные control-ы, вроде grid, требуют уйму однотипного кода. Логика доступа к базе данных смешана с логикой обработки данных. Бр-р-р... В общем, картина неприятная и что интересно, вполне устраивающая менеджеров. В конце концов, приложение будет работать, а как – это не важно, лишь бы поскорее сдать.

Именно с этого и начинался мой framework – слегка упорядоченный хаос.

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

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

А на горизонте маячил Web.

Портирование C++ Windows-приложения в Web – занятие не для слабонервных. Проще говоря, пришлось бы всё переписывать заново.

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

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

Кроме этого на проектирование сильно повлияла специфика выбранного языка программирования, конкретно – C#. В теории такого быть не должно, но мы в академиях не учились и потому я до сих пор иногда предпочитаю C# (или Java) тому же UML, когда дело касается описания структуры классов и их взаимодействия.

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

Что такое control

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

В данной книге также упоминается такое понятие как виджет. Я предпочту не пользоваться им, поскольку этого слова нет в моём англо-русском словаре, и возьму более подходящий, в моём представлении, англоязычный термин – control.

Далее для простоты изложения я дам следующее определение: control – это некая прямоугольная область на форме, которая может отображать какие-либо данные, либо принимать какие-либо данные, вводимые пользователем.

В связи с этим определением у меня однажды возникли неприятности. Оказалось что далеко не все control-ы прямоугольны (кто бы мог подумать :) ). Я потратил очень много времени, споря с одним из моих коллег. Он настаивал на том, что control-ы бывают очень разными, и потому определение не корректно. В итоге мне удалось с ним сойтись на том, что если за прямоугольную форму принять наименьший прямоугольник, в который control может поместиться, то определение таки будет корректным. В этом случае правда стоит добавить, что некоторые control-ы могут перекрывать друг друга, но на практике это не представляет особых проблем, а порой, например в случае слоёв (смотрите ниже), является просто необходимым.

Примером control-ов могут служить кнопки (aka buttons) и поля ввода (aka textboxes).

Из самого определения контрола можно сделать вывод, что все они имеют одни и те же свойства: координаты левого верхнего угла control-а (свойства X и Y), ширину (Width) и высоту (Height). Также на этом этапе можно принять следующее архитектурное решение: каждый control может быть видимым/невидимым (Visible) и доступным/недоступным (Enabled), и, кроме этого, каждый control имеет уникальный идентификатор (ID).

Каждый control кроме вышеперечисленных свойств обладает также некоторыми специфическими свойствами. Знатокам объектно-ориентированного программирования не составит труда догадаться, что это значит: у нас будет абстрактный базовый класс, который мы назовём Control, и все control-ы будут его наследниками:

C# code
      abstract
      public
      class Control
{
  public Control(int id, int x, int y, int width, int height) 
  {
    this.id = id;
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.visible = true;
    this.enabled = true;
  }
  virtualpublicbool Visible
  {
    get{returnthis.visible;}
    set{this.visible = value;}
  }
  virtualpublicbool Enabled
  {
    get{returnthis.enabled;}
    set{this.enabled = value;}
  }
  protectedint id, x, y, width, height;
  protectedbool visible, enabled;
}
Java code
      abstract
      public
      class Control
{
  public Control(int id, int x, int y, int width, int height) 
  {
    this.id = id;
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.visible = true;
    this.enabled = true;
  }
  publicboolean isVisible(){returnthis.visible;}
  publicvoid setVisible(boolean value){this.visible = value;}

  publicboolean isEnabled(){returnthis.enabled;}
  publicvoid setEnabled(boolean value){this.enabled = value;}

  protectedint id, x, y, width, height;
  protectedboolean visible, enabled;
}

Далее идёт достаточно интересная часть. Поскольку планируется использовать один и тот же код как для Windows, так и для Web, то логика представления должна использовать только один класс. Возьмём для примера кнопку. Если мы унаследуем от control-а два класса под названиями WindowsButton и WebButton, то и код, их использующий, соответственно будет разным, а это, как мы знаем, не то, что нам нужно. Вместо этого мы заведём ещё один абстрактный класс, наследующий от класса Control, и обзовём его Button. От этого-то класса как раз и будут унаследованы WindowsButton и WebButton, а прикладные программисты будут в своём коде использовать Button, как своего рода абстракцию от реализации. Поэтому, если однажды вы увидите код наподобие

      // C# code
Button b = ...;
b.Enabled = false;

то вы будете знать, что есть некая кнопка, которая сделана недоступной. Как эта недоступность реализована, прикладного программиста не касается. Системный программист будет решать, что ему надо делать в ответ на установку свойства Enabled. И он может решить, что наилучшей реализацией будет являться вывод сообщения «Кнопка такая-то не доступна, пожалуйста, не тыкайте в неё мышкой». :)

В этот момент также стоит упомянуть ещё одно правило проектирования framework – делайте только то, что нужно. Если какая-то функциональность кажется вам полезной, но на данный момент её никто не требует, отложите реализацию этой функциональности до лучших времён. Что это значит? Спроектируем, например, класс Button. От Control он наследует координаты, доступность и видимость. Кроме этого совершенно очевидно, что нам также необходимо иметь свойство для представления текста на кнопке и события от щелчка по ней.

C# code
      abstract
      public
      class Button : Control
{
  public Button(int id, int x, int y, int width, int height, string text) 
    : base(id, x, y, width, height)
  {
    this.text = text;
  }
  virtualpublicstring Text
  {
    get{returnthis.text;}
    set{this.text = value;}
  }
  publicabstractevent Click Click;

  protectedstring text;
}
Java code
      // Интерфейс делегата.
      public
      interface ClickDelegate
{
  publicvoid handle();
}

abstractpublicclass Button extends Control
{
  public Button(int id, int x, int y, int width, int height, string text) 
  {
    super(id, x, y, width, height);
    this.text = text;
  }
  public String getText(){returnthis.text;}
  publicvoid setText(String value){this.text = value;}

  // Вы можете содержать делегаты в массиве, но я предполагаю, // что каждое событие будет обрабатываться только одним делегатомpublicfinalvoid setClickDelegate(ClickDelegate delegate)
  {
    this.clickDelegate = delegate;
  }

  protected String text;
  protected ClickDelegate clickDelegate = null;
}

Как это ни странно, в моём случае этого было достаточно. Никто никогда меня не просил добавить функциональность для динамического изменения цвета кнопки или шрифта текста. Если бы я предположил, что такие вещи мне когда-нибудь понадобятся, то, во-первых, я бы провёл куда больше времени, реализуя классы WindowsButton и WebButton. Во-вторых, этого бы никто не оценил и никогда бы не использовал. В-третьих, у прикладных программистов была бы возможность выбора, а это отвлекает от написания логики представления. Так что лучше делать всё самым простым образом, помня, что добавлять функциональность всегда проще, чем её изменять.

Чтобы упростить дальнейшее повествование я приведу код для классов WindowsButton (здесь и далее код для Windows-версии приводится только для C#) и WebButton.

C# code
      public
      class WindowsButton : Button
{
  public WindowsButton(int id, int x, int y, int width, int height, string text) 
    : base(id, x, y, width, height, text)
  {
    this.control = new System.Windows.Forms.Button();
    this.control.Name = "b" + id.ToString();
    this.control.Location = new System.Drawing.Point(x, y);
    this.control.Size = new System.Drawing.Size(width, height);
    this.control.Text = text;
    this.control.Click += new EventHandler(this.OnClick); 
  }
  overridepublicbool Enabled
  {
    set
    {
      // Да, да, именно здесь вместо того, чтобы делать control // недоступным, можно было бы вывести вышеупомянутое сообщение.this.control.Enabled = value;
    }
  }
  overridepublicstring Text
  {
    set
    {
      // Кто сказал, что я не могу контролировать то, что прикладные // программисты пишут на моих кнопках?// Мне надо просто сделать что-то вроде SpellChecker.Check(value)// и в случае чего выбросить исключение FrameworkInternalError :)base.Text = value;

      // Дублирования данных можно избежать, если объявить свойство Text// в базовом классе абстрактным.this.control.Text = value;
    }
  }
    
  publicevent Click Click;

  privatevoid OnClick(...)
  {
    if (Click != null)
    {
      // Мы сами ответственны за генерирование событий,// что даёт нам право делать это тем способом, // который мы сочтём наиболее подходящим.// В моём случае я перехватываю все исключения, // которые могут возникнуть в логике представления,// дабы грациозно вывести сообщение о баге, // а не просто вылететь с исключением.// Для особо пытливых – это также место, где вы можете начать// транзакцию, вызвать Click(), и в случае отсутствия исключений // завершить транизакцию (чем не EJB контейнер?).try
      {
        Click();
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.Message);
      }
    }
  }
  private System.Windows.Forms.Button control;
}

publicclass WebButton : Button
{
  // нам нужен PlaceHolder, чтобы добавить // созданный control на web-страницуpublic WebButton(PlaceHolder form, 
           int id, int x, int y, int width, int height, string text) 
    : base(id, x, y, width, height, text)
  {
    this.control = new System.Web.UI.WebControls.Button();
    this.control.ID = "b" + id.ToString();
    this.control.Height = height;
    this.control.Width = width;
 
    // Я использую абсолютное позиционирование, хотя при использовании// старых версий Web-браузеров можно создать невидимую HTML-таблицу,// поместив данный control в нужную ячейку. В этом случае расчёт и // создание таблицы будет происходить не здесь, // а на главной aspx-странице (см. ниже).this.control.Style.Add("POSITION", "absolute");
    this.control.Style.Add("LEFT", x.ToString() + "px");
    this.control.Style.Add("TOP", y.ToString() + "px");
    this.control.Click += new EventHandler(OnClick);
    this.control.Text = text;
    form.Controls.Add(this.control);             
  }
  publicevent Click Click;
  overridepublicbool Enabled
  {
    set{control.Enabled = value;}
  }
  privatevoid OnClick(...)
  {
    if (Click != null)
      Click();
  }

  private System.Web.UI.WebControls.Button control;
}
Java code
      // Каждый control в framework должен реализовывать этот интерфейс
      public
      interface VisualControl
{
  // Эта функция служит для генерации сообщений.// Она вызывается главным сервлетом в момент обработки// параметров http-запроса (см. HttpServletRequest.getParameterValues)publicvoid fireEvent(String value);

  // Эта функция служит для отрисовки control-а.// Она вызывается главным сервлетом непосредственно// перед отправлением страницы клиентуpublicvoid render(javax.servlet.ServletOutputStream out);
}

publicclass WebButton extends Button implements VisualControl
{
  public WebButton(int id, int x, int y, int width, int height, 
           String text) 
  {
    super(id, x, y, width, height, text);
  }
  // Согласно стандарту HTTP, если кнопка была нажата, то на сервер будет // передано то, что на кнопке написано. В нашем случае это означает,// что нам просто необходимо сгенерировать событие, игнорируя value.publicvoid fireEvent(String value)
  {
    if (super.clickDelegate != null)
      super.clickDelegete.handle();
  }
  // Каждый control знает, как представить себя в виде HTML-кодаpublicvoid render(javax.servlet.ServletOutputStream out)
  {
    if (super.visible)
    {
      out.print("<input type=\"submit\" value=\"");
      out.print(super.text);
      out.print("\" name=\"");
      out.print('b');
      out.print(super.id);
      if (!super.enabled)
        out.print("disabled=\"disabled\" ");
      out.print("style=\"POSITION:absolute;LEFT:");
      out.print(super.x);
      out.print("px;TOP:");
      out.print(super.y);
      out.print("px;WIDTH:");
      out.print(super.width);
      out.print("px;HEIGHT:");
      out.print(super.height);
      out.print("px;\"/>");
    }
  }
}

Как видно из приведённого примера, системный программист может делать практически все, что хочет, контролировать, что хочет, и на основании только ему ведомых принципов и требований выполнять или не выполнять то, что хочет прикладной программист. Например, если в свойстве Enabled (set) прописать, что данная кнопка на данной форме не доступна для пользователей определённой роли, то прикладной программист никогда не сможет сделать её доступной, поскольку button.Enabled = true – это единственная команда, которая ему доступна, а её реализация имеет дополнительную блокирующую логику. Это только один из примеров ограничений, которые вы, как архитектор, можете наложить на реализацию framework.

Монополия на создание объектов

Мы только что пополнили нашу библиотеку framework несколькими полезными артефактами – абстрактными классами Control и Button, которые доступны пользователям framework, а также классами WindowsButton и WebButton, недоступными пользователям. Отсюда следует и ограничение – программист не создаёт control-ы сам, а только пользуется ссылками на них, через абстрактные классы (в нашем случае через класс Button).

Вы можете заметить, что программисту ничего не стоит написать следующее:

C# code
Button b = new WindowsButton(...);
b.Enabled = false;

Но такой код не будет работать под Web, и потому одно из наших требований будет нарушено. Если не программист создаёт control, то кто? За ответом на этот вопрос мы отправимся в книгу GoF. Открываем страницу с описанием первого шаблона и видим следующее – «Абстрактная фабрика». Это примерно то, что нам надо, поскольку, передав создание control-а фабрике, мы тем самым абстргируемся от платформы.

Согласно описанию, если мы хотим использовать шаблон «Абстрактная фабрика» нам необходимо иметь:

  1. абстрактный класс фабрики (1 шт.)
  2. конкретные классы фабрики (2 шт. Одна для Windows, другая для Web)
  3. абстрактные классы control-ов (n шт.)
  4. конкретные классы control-ов (2n шт.)


Рисунок 1

C# code
      abstract
      public
      class AbstractFactory
{
  abstractpublic Button CreateButton(int id, 
                    int x, int y, int width, int height, string text);
  
  // функции для создания других видов control-ов
  .............
}
publicclass WindowsFactory
{
  public Button CreateButton(int id, int x, int y, int width, int height, 
                 string text)
  {
    returnnew WindowsButton(id, x, y, width, height, text);
  }
  .............
}
publicclass WebFactory
{
  public WebFactory(PlaceHolder form)
  {
    this.form = form;
  }
  public Button CreateButton(int id, int x, int y, int width, int height, 
                 string text)
  {
    returnnew WebButton(form, id, x, y, width, height, text);
  }
  .............

  private PlaceHolder form;
}

В таком случае, код логики представления может принять такой вид:

C# code
AbstractFactory factory = new WindowsFactory();
Button b = factory.CreateButton();
b.Enabled = false;

Знаю, знаю. Отобрали у программистов создание control-ов напрямую и всучили им обязанность создавать фабрику. Конечно, это уже не так плохо, как раньше, поскольку фабрика должна быть создана только один раз, но, следуя законам Мэрфи, если кто-то может создать WindowsFactory, ожидая, что она будет производить ему контролы для Web, то так и случится.

Отобрать! Не давать! Пусть уж лучше сам framework решает, что ему нужно. К тому же уж он-то точно знает, какая фабрика должна быть создана. Откуда появляется это знание? Вы, как системный программист в отличие от прикладных программистов точно знаете, какие платформы поддерживаются и как. Зная, например, что необходимо создать Windows-приложение, вы также знаете, что у вас есть функция Main. Зная, что это Web, вы предполагаете наличие aspx-страницы в случае использования .NET, или сервлета, если используется Java. И именно там лучше всего расположить код для создания фабрики. Например, написав код наподобие следующего

C# code for Windows
      void Main()
{
  AbstractFactory factory = new WindowsFactory();
  Logic.Execute(factory);
}
C# code for ASP.NET
      public
      class Engine : System.Web.UI.Page
{
  overrideprotectedvoid OnInit(EventArgs e)
  {
    this.PreRender += new EventHandler(this.EngineInit);
    base.OnInit(e);
  }
  privatevoid EngineInit(object sender, System.EventArgs e) 
  {
    PlaceHolder holder = new PlaceHolder();

    // Нужно добавить holder внутрь тега <form id=”MainForm”>
    HtmlForm htmlForm = (HtmlForm)Page.FindControl("MainForm");
    htmlForm.Controls.Add(holder);

    AbstractFactory factory = new WebFactory(holder);
    Logic.Execute(factory);
  }
}
Java code
      public
      class HostServlet extends HttpServlet
{
  publicvoid doGet(HttpServletRequest request, HttpServletResponse response)
           throws ServletException, IOException
  {
    AbstractFactory factory = new WebFactory();
    Logic.execute(factory);
  }
  publicvoid doPost(HttpServletRequest request, HttpServletResponse response)
           throws ServletException, IOException
  {
    doGet(request, response);
  }
}

мы добьемся того, что код логики представления будет действительно независим от платформы. Правда, код framework (а это именно он приведён выше), будет зависеть от платформы и очень сильно.

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

Форма и логика представления

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

То есть framework всегда строится на основе метода «обратных вызовов», и это является одной из самых характерных черт framework как системы.

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

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

Начнём со строки Logic.Execute(factory). Это, конечно же, псевдокод. На самом деле всё должно быть чуточку сложнее.

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

Форма – это прямоугольный контейнер для control-ов, который имеет название. Строго говоря, control-ы не могут существовать вне формы (примем это как ещё одно ограничение), поэтому можно также сделать вывод, что они должны создаваться непосредственно через экземпляр формы. Можно также предположить, что в любой момент работы приложения только одна форма является активной, это поможет избежать многих трудностей при реализации framework.

Как должно быть уже понятно, форма также должна быть представлена через абстрактный класс, назовём его Form.

C# code
      abstract
      public
      class Form
{
  abstractpublicstring Caption{get;set;}
  abstractpublicint Width{get;set;}
  abstractpublicint Height{get;set;}

  publicvoid AddControl(Control control)
  {
    this.сontrols.Add(control);
  }

  protected ArrayList сontrols = new ArrayList();
}
Java code
      abstract
      public
      class Form
{
  abstractpublic String getCaption();
  abstractpublicvoid setCaption(String value);

  abstractpublicint getWidth();
  abstractpublicvoid setWidth(int value);

  abstractpublicint getHeight();
  abstractpublicvoid setHeight(int value);

  publicvoid addControl(Control control)
  {
    this.сontrols.add(control);
  }

  protected ArrayList сontrols = new ArrayList();
}

Этот класс содержит коллекцию control-ов, а также такие свойства, как размеры и название. Классы-наследники будут реализовывать формы по-разному. В случае Windows, форма – это окно, в Web – это HTML-тег <form>. Поэтому имеет смысл для создания формы использовать вышеупомянутую абстрактную фабрику:

C# code
      abstract
      public
      class AbstractFactory
{
  abstractpublic Form CreateForm();
  ...............
}

Введя понятие формы, мы можем предположить, как будет выглядеть код внутри Logic.Execute(factory).

C# code
      // создаём форму
Form form1 = factory.CreateForm(); 

// создаём кнопку// ID кнопки – 0, координаты (10, 100), размер 70х20, текст – “Ok”
Button b1 = factory.CreateButton(0, 10, 100, 70, 20, "Ok");

// добавляем кнопку на форму
form1.AddControl(b1);

...............

// далее где-то в коде делаем эту кнопку доступной.
b1.Enabled = true;

Не знаю как у вас, но у меня такой код вызывает отвращение. Посмотрим на недостатки:

  1. Программист должен создавать форму сам.
  2. Созданный control надо не забыть добавить на форму.

Мы уже знаем, что делать с первой проблемой. Надо просто воспользоваться правилом монополии на создание объектов. То есть framework, а не программист, должен создавать форму, а потом передавать ссылку на неё в логику представления. Например, функция Main будет выглядеть примерно так:

C# code
      void Main()
{
  AbstractFactory factory = new WindowsFactory();
  Form form = factory.CreateForm();

  Logic.Execute(factory, form);
}

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

C# code
      void Main()
{
  AbstractFactory factory = new WindowsFactory();
  while (returnCode != Exit)
  {
    Form form = factory.CreateForm();
    returnCode = Logic.Execute(factory, form);
  }
}

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

C# code
      // создаём кнопку 
      // ID кнопки – 0, координаты (10, 100), размер 70х20, текст – “Ok”
Button b1 = factory.CreateButton(0, 10, 100, 70, 20, "Ok");

// добавляем control на созданную framework форму// легко заметить, что прикладной прошраммист не знает как форма была создана 
form.AddControl(b1);

.............

// далее где-то в коде делаем эту кнопку доступной.
b1.Enabled = true;

Легко отметить естественное разделение между инициализацией формы – созданием control-а и его добавлением на форму, и её использованием – вызовами функций контролов. Для начала поместим эту функциональность в две функции – FormInit и Execute. Тогда функция Main может быть записана так:

C# code
      void Main()
{
  AbstractFactory factory = new WindowsFactory ();
  while (returnCode != Exit)
  {
    Form form = factory.CreateForm();
    Logic.FormInit(factory, form);
    returnCode = Logic.Execute();
  }
}

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

C# code
      void Main()
{
  AbstractFactory factory = new WindowsFactory ();
  while (returnCode != Exit)
  {
    Form form = factory.CreateForm();

    // Предположим, что у нас есть интерфейс Logic
    Logic logic = new Form1Logic();
    logic.FormInit(factory, form);

    returnCode = logic.Execute();
  }
}

Надо отметить, что по определению код логики не зависит от платформы, потому создание объекта логики не стоит осуществлять через класс AbstractFactory. Статически указывать имя класса можно (именно это и сделано в вышеприведённом коде), но не стоит, если количество форм велико или может изменяться динамически.

Я предпочитаю следующую стратегию создания объектов логики: в глобальной переменной (в переменной HTTP-сессии в случае Web) хранится имя текущей формы или её уникальный идентификатор. Для создания объекта логики можно использовать Reflection (Activator.CreateInstance в .NET или Class.newInstance в Java), или, в случае с C++, некий класс-фабрику, который, основываясь на имени формы или её ID, создаёт нужный класс. Значение глобальной переменной изначально устанавливается в значение формы, которую пользователь будет видеть, когда запустит приложение, и изменится в тот момент, когда пользователь воспользуется навигацией (выберет пункт меню, например), либо когда логика представления потребует открыть новую форму (например, по щелчку на кнопке должно открыться окно диалога).

Итак, чего нам удалось достичь:

  1. Логика представления не зависима от платформы.
  2. Программист не создаёт объекты (вообще!), что является определённым достижением, поскольку в этом случае мы на уровне framework можем контролировать процесс создания и инициализации всех важных компонентов системы. Тем, кто хотел бы это использовать могу предложить поэкспериментировать со сценарием кэширования (только для Web). То есть вместо того, чтобы создавать объекты классов логики (а они будут создаваться для каждого запроса к серверу), мы можем создать некоторое их количество для часто используемых форм, поместить их в пул и затем использовать по мере необходимости.

Уже не плохо, но мы не можем останавливаться на достигнутом, поскольку пока что остаётся не освещённым один очень важный вопрос: как прикладной программист работает с событиями, возникающими в системе? Существует по крайне мере два типа событий: события от control-ов и внутренние события framework. К числу последних можно отнести, например, момент открытия и закрытия формы.

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

C# code
      class Form1Logic : Logic
{
  publicvoid FormInit(AbstractFactory factory, Form form)
  {
    Ok = factory.CreateButton(0, 10, 100, 70, 20, "Ok");
    Ok.Click += new Click(OnOk);
    form.AddControl(Ok);
  }
  privatevoid OnOk()
  {
    Ok.Enabled = false;
  }
  private Button Ok;
}
Java code
      class Form1Logic implements Logic
{
  publicvoid formInit(AbstractFactory factory, Form form)
  {
    Ok = factory.createButton(0, 10, 100, 70, 20, "Ok");

    // Используем анонимный класс для управления событиями
    Ok.setClickDelegate(new ClickDelegate()
    {
      publicvoid handle()
      {
        // Здесь можно было бы написать код обработки сообщения,// но я предпочту единообразие с C#-версией и просто вызову// подходящую функцию-обработчик
        OnOk();
      }
    });

    form.addControl(Ok);
  }
  privatevoid OnOk()
  {
    Ok.setEnabled(false);
  }
  private Button Ok;
}

Также легко можно заметить, что, если мы добавим в абстрактный класс Form события с именами FormOpen, FormClosed, то функция FormInit в Form1Logic может принять следующий вид:

C# code
      public
      void FormInit(AbstractFactory factory, Form form)
{
  form.FormOpen += new FormOpen(OnFormOpen);
  form.FormClosed += new FormOpen(OnFormClosed);
}
Java code
      public
      void formInit(AbstractFactory factory, Form form)
{
  form.setFormOpenDelegate(new FormOpenDelegate()
  {
    publicvoid handle(){OnFormOpen();}
  }
  form.setFormClosed(new FormClosedDelegate()
  {
    publicvoid handle(){OnFormClosed();}
  }
}

Немного подумав, можно прийти к выводу, что функция Execute (см. функцию Main), в общем-то, и не нужна, поскольку вся функциональность логики представления основана на обработке событий. Добавив к этому ещё и то, что инициализацию формы можно перенести в конструктор логики представления, мы получим следующий результат:

C# code
      void Main()
{
  AbstractFactory factory = new WindowsFactory ();
  // на самом деле в случае с Windows вы создаёте главное окно программы// и если пользователь его закрывает, то вы отлавливаете это сообщение,// генерируете событие FormClosed для текущей формы, // и выходите из приложения,// то есть вместо цикла, будет просто основной поток приложенияwhile(...)
  {
    Form form = factory.CreateForm();
    new Form1Logic(factory, form);
  }
}
class Form1Logic
{
  public Form1Logic(AbstractFactory factory, Form form)
  {
    form.FormOpen += new FormOpen(OnFormOpen);
    Ok = factory.CreateButton(0, 10, 100, 70, 20, "Ok");
    Ok.Click += new Click(OnOk);
    form.AddControl(Ok);
  }
  privatevoid OnFormOpen()
  {
    Ok.Enabled = true;
  }
  privatevoid OnOk()
  {
    Ok.Enabled = false;
  }
  private Button Ok;
}

Как можно заметить, класс логики представления не сильно отличается от аналогичных классов, которые пишутся для Windows- и Web-форм в .NET. Различий несколько, но они крайне важны. Во-первых, наш код не зависит от платформы, то есть, если поместить его в отдельную сборку, то при переносе приложения из Windows в Web, не понадобится его перекомпилировать. Во-вторых, мы контролируем, что происходит в системе, когда это происходит и как. Например, прикладной программист может делать только то, что системный программист, урезающий доступную ему функциональность, позволяет делать. В-третьих, у нас есть один стандартный набор контролов, контролируемый централизованно, что обеспечивает стандартный look & feel для разрабатываемой системы.

Форма как отдельный компонент

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

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

Предположим на данный момент, что у нас есть форма Form1, на которой есть кнопка Ok. Было бы неплохо написать что-то вроде:

C# code
      class Form1 : Form
{
  public Form1(AbstractFactory factory)
  {
    Ok = factory.CreateButton(0, 10, 100, 70, 20, "Ok");
    this.AddControl(Ok);
  }
  publicreadonly Button Ok;
}

Напомню, что Form – это абстрактный класс, код которого приведён выше.

В этом случае функция Main и класс логики представления выглядели бы так:

C# code
      void Main()
{
  AbstractFactory factory = new WindowsFactory ();
  Form1 form = new Form1(factory);

  // Ссылку на фабрику нам передавать не нужно, что соответственно// вводит ограничение на динамическое создание control-ов // прикладным программистом.// Я обнаружил, что это ограничение крайне полезно.new Form1Logic(form); 
}

class Form1Logic
{
  public Form1Logic(Form1 form)
  {
    this.form = form;
    form.FormOpen += new FormOpen(OnFormOpen);
    form.Ok.Click += new Click(OnOk);
  }
  privatevoid OnFormOpen()
  {
    this.form.Ok.Enabled = true;
  }
  privatevoid OnOk()
  {
    this.form.Ok.Enabled = false;
  }
  private Form1 form;
}

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

Обратим теперь взор на класс Form1. Самая большая проблема, которую вы могли уже заметить, заключается в том, что на самом деле недопустимо прямое наследование от Form. Вместо этого нужно использовать наследование либо от WindowsForm, либо от WebForm в зависимости от платформы. То есть от тех классов, которые кроме всего прочего ещё и содержат реальный код, отображающий форму на экране. И если нам нужны формы Form1, Form2, Form3 и т.д. то нам либо придётся заводить классы WindowsForm1, WebForm1, WindowsForm2, WebForm2, WindowsForm3, WebForm3 и т.д., либо каждый раз переписывать код для форм. Первый способ нам не подходит, второй тоже, если только вы не являетесь любителем такого кода:

C# code
      class Form1 :
#if Windows
  WindowsForm
#else
  WebForm
#endif
{...}

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

Проблема в том, что нам хотелось бы унаследовать класс от Form, а не от WindowsForm и WebForm, но напрямую сделать это мы не можем. Отправимся за поиском решения в поваренную книгу GoF. Пролистайте её пару раз. Хорошая книга, правда?

Но не будем отвлекаться. То, что нам нужно, в переводе на язык шаблонов проектирования звучит так: нам необходимо поддерживать независимость абстракции (нашей формы Form1) от конкретной реализации (WindowsForm и WebForm), что обеспечивается при использовании шаблона «Мост» (aka “Bridge”).


Рисунок 2

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

В результате:

C# code
      abstract
      public
      class FormBridge
{
  public FormBridge(Form form)
  {
    this.form = form;
  }
  publicstring Caption
  {
    get{returnthis.form.Caption;}
    set{this.form.Caption = value;}
  }
  publicint Width
  {
    get{returnthis.form.Width;}
    set{this.form.Width = value;}
  }
  publicint Height
  {
    get{returnthis.form.Height;}
    set{this.form.Height = value;}
  }

  private Form form;
}

class Form1 : FormBridge
{
  public Form1(AbstractFactory factory, Form form) : base(form)
  {
    Ok = factory.CreateButton(0, 10, 100, 70, 20, "Ok");
    form.AddControl(Ok);
  }
  publicreadonly Button Ok;
}

В этом случае функция Main будет иметь вид:

C# code
      void Main()
{
  AbstractFactory factory = new WindowsFactory();
  Form form = factory.CreateForm();
  Form1 form = new Form1(factory, form);
  new Form1Logic(form); 
}

Таким образом, мы получили систему, состоящую из множества абстрактных классов, их реализации, а также набора пар форма-логика. В случае с .NET, если мы поместим каждую форму и логику представления в системе в отдельную сборку, мы добьёмся компонентно-ориентированного подхода в разработке программного обеспечения. То есть формы будут кирпичиками, которые можно легко менять без перекомпиляции системы и, в случае Web-приложения, без остановки сервера. Замечу, что это также верно и для Java.

Framework как библиотека

Я упоминал в начале статьи, что framework – это также библиотека решений для часто встречающихся проблем. Продемонстрируем это на реальных примерах.

Типизированный Combobox

Возьмём combobox. Как известно, он содержит пары {значение, текст}. Причём значение имеет вполне определённый тип, который в случае с Web (DropDownList) изменить нельзя. Поэтому если вам вдруг понадобилось сохранять там объекты других типов, то есть, например, у вас есть список пациентов и вы бы хотели записывать в combobox пары {Patient, Patient.Name}, то вам придётся писать дополнительный код. И его придётся писать всякий раз, когда такая функциональность потребуется, причём в коде логики представления, с которой у неё мало общего.

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

C# code
        public
        class ComboBox : Control
{
  abstractpublicvoid Clear();
  abstractpublicvoid NewRow(object value, string text);
  abstractpublicint Count{get;}
  abstractpublicobject Value{get;set;}

  abstractpublicevent ValueChanged ValueChanged;
}
Java code
        public
        interface ValueChangedDelegate
{
  publicvoid handle();
}
publicclass ComboBox extends Control
{
  abstractpublicvoid clear();
  abstractpublicvoid newRow(Object value, String text);
  abstractpublicint size();
  abstractpublic Object getValue();
  abstractpublicvoid setValue(Object value);

  publicfinalvoid setValueChangedDelegate(ValueChangedDelegate delegate)
  {
    this.valueChangedDelegate = delegate;
  }

  protected ValueChangedDelegate valueChangedDelegate;
}

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

C# code
        private
        void OnFormOpen()
{
  // очищаем combobox
  form.ComboBox1.Clear();

  // добавляем два элемента
  form.ComboBox1.NewRow("First", "One");
  form.ComboBox1.NewRow("Second", "Two");

  // выбираем элемент “Second”
  form.ComboBox1.Value = "Second";
}

Посмотрим, во что выльется реализация такой функциональности:

C# code
        class WindowsComboBox : ComboBox
{
  public WindowsComboBox(int id, int x, int y, int width, int height) 
    : base(id, x, y, width, height, text)
  {
    this.control = new System.Windows.Forms.ComboBox();
    this.control.Name = "b" + id.ToString();
    this.control.Location = new System.Drawing.Point(x, y);
    this.control.Size = new System.Drawing.Size(width, height);
    this.control.SelectedIndexChanged += 
             new EventHandler(this.OnSelectionChanged); 
  }
  publicoverridebool Enabled
  {
    set{this.control.Enabled = value;}
  }
  publicvoid Clear()
  {
    this.control.Items.Clear();
    this.values.Clear();
  }
  publicvoid NewRow(object value, string text)
  {
    // здесь я предполагаю, что позиции text и value в коллекциях совпадаютthis.control.Items.Add(text);
    this.values.Add(value);
  }
  publicint Count
  {
    get{returnthis.values.Count;}
  }
  publicobject Value
  {
    get
    {
      if (this.values.Count == 0)
        returnnull;
      returnthis.values[this.control.SelectedIndex];
    }
    set
    {
      this.control.SelectedIndex = this.values.IndexOf(value);
    }
  }
  publicevent ValueChanged ValueChanged;
  privatevoid OnSelectionChanged(...)
  {
    if (ValueChanged != null)
      ValueChanged();
  }
  private System.Windows.Forms.ComboBox control;
  private ArrayList values = new ArrayList();
}
Java code
        class WebComboBox extends ComboBox implements VisualControl
{
  public WebComboBox(int id, int x, int y, int width, int height) 
  {
    super(id, x, y, width, height, text);
  }
  publicvoid clear()
  {
    this.currentSelection = -1;
    this.values.clear();
    this.texts.clear();
  }
  publicvoid newRow(Object value, String text)
  {
    this.values.add(value);
    this.texts.add(value);

    // В случае Web, если ComboBox не пуст, // то автоматически выбирается первое значениеif (this.currentSelection == -1)
      this.currentSelection = 0;
  }
  publicint size()
  {
    returnthis.values.size()
  }
  public Object getValue()
  {
    if (this.currentSelection == -1)
      returnnull;
    returnthis.values.get(this.currentSelection);  
  }
  publicvoid setValue(Object value)
  {
    if (value == null)
      this.currentSelection = -1;
    elsethis.currentSelection = this.values.indexOf(value);
  }
  // для ComboBox value – это значение, выбранное пользователемpublicvoid fireEvent(String value)
  {
    int newValue = Integer.parseInt(value);
    if (newValue != this.currentSelection)
    {
      this.currentSelection = newValue;
      if (super.valueChangedDelegate != null)
        super.valueChangedDelegate.handle();
    }
  }
  // отрисовываем ComboBoxpublicvoid render(javax.servlet.ServletOutputStream out)
  {
    if (super.visible)
    {
      out.print("<select name=\"");
      out.print('b');
      out.print(super.id);
      if (!super.enabled)
        out.print("disabled=\"disabled\" ");
      out.print("style=\"POSITION:absolute;LEFT:");
      out.print(super.x);
      out.print("px;TOP:");
      out.print(super.y);
      out.print("px;WIDTH:");
      out.print(super.width);
      out.print("px;HEIGHT:");
      out.print(super.height);
      out.print("px;\">");
      for (int i = 0; i < this.values.size(); ++i)
      {
        out.print("<option value=\"");
        out.print(i);
        out.print("/"")
        if (i == this.currentSelection)
          out.print(" SELECTED");
        out.print(">");
        out.print((String)texts.get(i));
      }
      out.print("</select>");
    }
  }

  // На самом деле, для корректной работы значения этих трёх // переменных должны храниться в HTTP-сессии. Это связано с тем, // что объекты этого класса создаются каждый раз, когда пользователь // отправляет страницу на сервер. privateint currentSelection = -1;
  private ArrayList values = new ArrayList();
  private ArrayList texts = new ArrayList();
}

Не так уж и плохо. Но не стоит останавливаться на достигнутом. Подумаем о том, как ещё можно облегчить жизнь прикладным программистам. «Забота о ближнем» - помните такое?

Что произойдёт, если написать следующее в логике представления:

C# code
        private
        void OnFormOpen()
{
  // очищаем combobox
  form.ComboBox1.Clear();

  // добавляем два элемента
  form.ComboBox1.NewRow("First", "One");
  form.ComboBox1.NewRow("Second", "Two");

  // выбираем элемент “Second”
  form.ComboBox1.Value = "Second";
  ............
  int value = (int)form.ComboBox1.Value;
}

Правильно, ошибка времени исполнения (InvalidCastException). А практика учит, что нужно делать всё возможное, чтобы избежать ошибок времени исполнения, и если возможно, перевести их в разряд ошибок компиляции. Как мы это будем делать? Начнём размышлять. Если бы у меня был класс вроде такого:

C# code
        class ComboBox : Control
{
  publicabstractvoid NewRow(string value, String text);
  publicabstractstring Value{get;set;}
}

а также класс вроде такого

C# code
        class ComboBox : Control
{
  publicabstractvoid NewRow(int value, String text);
  publicabstractint Value{get;set;}
}

и ещё вот такой класс:

C# code
        class ComboBox : Control
{
  // Patient – это класс, описывающий пациента в медицинской системе.publicabstractvoid NewRow(Patient value, String text); 
  publicabstract Patient Value{get;set;}
}

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

Решение такой проблемы в рамках C++ не составляет особого труда – используем шаблоны – и всё будет именно так, как и хотелось бы. Но как бороться с этой проблемой в C# или Java (на момент написания статьи шаблоны в этих языках реализованы не были)?

Возможно, существует несколько решений данной проблемы, но я выбрал использование шаблона проектирования “Мост”. То есть:


Рисунок 3

Классы ComboBox и ComboBoxBridge – абстрактные. Они принадлежат framework. Классы WindowsComboBox и WebComboBox принадлежат реализации framework. Где же расположены классы StringComboBox, и т.п.? Самым подходящим местом для них может оказаться класс Form1 и т.п. То есть:

C# code
        public
        class Form1 : FormBridge
{
  publicclass PatientComboBox : ComboBoxBridge
  {
    private PatientComboBox(ComboBox control) : base(control){}
    publicvoid NewRow(Patient value, string text)
    {
      this.control.NewRow(value, text);
    }
    public Patient Value
    {
      get{return (Patient)this.control.Value;}
      set{this.control.Value = value;}
    }
  }
  public Form1(AbstractFactory factory, Form form) : base(form)
  {
    Ok = factory.CreateButton(0, 10, 100, 70, 20, "Ok");
    form.AddControl(Ok);
    ComboBox c = factory.CreateComboBox(1, 10, 130, 70, 20);
    Patient = new PatientComboBox(c);
    form.AddControl(c);
  }
  publicreadonly Button Ok;
  publicreadonly PatientComboBox Patient;
}
Java code
        public
        class Form1 extends FormBridge
{
  publicclass PatientComboBox extends ComboBoxBridge
  {
    private PatientComboBox(ComboBox control){super(control);}
    publicvoid newRow(Patient value, String text)
    {
      this.control.newRow(value, text);
    }
    public Patient getValue()
    {
      return (Patient)this.control.getValue();
    }
    publicvoid setValue(Patient value)
    {
      this.control.setValue(value);
    }
  }
  public Form1(AbstractFactory factory, Form form)
  {
    super(form);
    Ok = factory.createButton(0, 10, 100, 70, 20, "Ok");
    form.addControl(Ok);
    ComboBox c = factory.createComboBox(1, 10, 130, 70, 20);
    Patient = new PatientComboBox(c);
    form.addControl(c);
  }
  publicfinal Button Ok;
  publicfinal PatientComboBox Patient;
}

Это демонстрирует ещё одну особенность framework – в большинстве случаев мы можем превратить ошибки времени выполнения в ошибки времени компиляции. Это приведёт к разбуханию кода, но упростит разработку системы.

Слои

Что есть слой? Это группа control-ов, которая может быть отображена на экране как единое целое. Предположим на форме слева находится control TreeView. Если выбрать узел дерева, в правой части формы появятся или исчезнут control-ы, предназначенные для ввода информации конкретно для этого узла. Обычно для реализации подобной функциональности как раз и применяются слои. То есть имеется набор слоёв, из которых в каждый момент времени может быть виден только один. Когда пользователь выбирает узел – логика представления, основываясь на информации об узле, показывает нужный слой и прячет все остальные.

Как легко заметить, существует некая общая функциональность для слоёв. Давайте подумаем, как это поместить в framework.

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

Java code
        class Form1 extends FormBridge
{
  public Form1(AbstractFactory factory, Form form)
  {
    super(form);
    Ok = factory.createButton(0, 10, 100, 70, 20, "Ok");
    form.addControl(Ok);
    ComboBox c = factory.createComboBox(1, 10, 130, 70, 20);
    Patient = new PatientComboBox(c);
    form.addControl(c);

    this.hideLayers();
  }
  publicvoid hideLayers()
  {
    this.Ok.setVisible(false);
    this.Patient.setVisible(false);
  }
  publicvoid showLayer1()
  {
    this.Ok.setVisible(true);
    this.Patient.setVisible(false);
  }
  publicvoid showLayer2()
  {
    this.Ok.setVisible(false);
    this.Patient.setVisible(true);
  }
  publicfinal Button Ok;
  publicfinal PatientComboBox Patient;
}

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

Конечно, никто не ограничивал прикладного программиста в способности установить visible в true для обоих control-ов, но как с этим бороться, я вам не скажу. Что-то ведь должно и вам достаться. :)

Блокировка формы в Web-приложениях

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

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

Для блокировки формы придётся написать кода на JavaScript, причём если control-ы на страницы сложны по своей структуре или представляют собой DHTML-контролы, то скрипт общего назначение написать, может быть и не удастся. То есть может понадобиться писать скрипты в зависимости от содержания формы. Под ASP.NET, то подобный код должен был бы располагаться на каждой странице. В случае же framework можно написать следующее:

C# code for ASP.NET
        public
        class Engine : System.Web.UI.Page
{
  protected System.Web.UI.HtmlControls.HtmlInputHidden Controls;

  overrideprotectedvoid OnInit(EventArgs e)
  {
    this.PreRender += new EventHandler(this.EngineInit);
    base.OnInit(e);
  }
  privatevoid EngineInit(object sender, System.EventArgs e) 
  {
    PlaceHolder holder = new PlaceHolder();

    // Нужно добавить holder внутрь тага <form id=”MainForm”>
    HtmlForm htmlForm = Page.FindControl("MainForm") as HtmlForm;
    htmlForm.Controls.Add(holder);

    AbstractFactory factory = new WebFactory(holder);
    Form form = factory.CreateForm();
    Form1 form = new Form1(factory, form);
    new Form1Logic(form); 
    
    // Составляем список control-ов, которые надо блокировать.// Если бы control-ы блокировались по-разному, то их идентификаторы// должны были бы располагаться в разных «скрытых» полях.
    StringBuilder s = new StringBuilder(100);
    foreach (Control control in form.Controls)
    {
      s.Append("b"); // префикс имени control-а
      s.Append(control.ID)
      s.Append(",");
    }
    if (s.Length > 0)
      s.Remove(s.Length – 1, 1); // удаляем последнюю запятуюthis.Controls.Value = s.ToString();

    // помещаем на страницу скрипт для блокировки
    s = new StringBuilder(100);
    s.Append("<script>");
    s.Append("function disableAll(){");
    s.Append("if(Controls.value!=\"\"){");
    s.Append("var c=Controls.value.split(\",\");");
    s.Append("for (i=0;i<c.length;i++) MainForm.all(c[i]).disabled=true;}}");
    s.Append("</script>");
    Page.RegisterClientScriptBlock("DisableAll", s.ToString());
  }
}

Если теперь на aspx-странице изменить тег <body> на

<body onbeforeunload="disableAll()">

то мы добьёмся того, что нам нужно.

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

Любопытным могу предложить ещё одну проблему, которая часто встречается при программировании под ASP.NET. Если страница загружается в Web-браузер, причём имеются валидаторы, то пользователь может успеть щёлкнуть по какой-либо кнопке раньше, чем загрузится скрипт валидации. Это приводит к ошибке скрипта. Чтобы этого избежать, я создаю все control-ы на web-форме изначально недоступными и добавляю скрипт в самый конец страницы, чтобы сделать их доступными. Как легко заметить, реализация подобной схемы с использованием framework имеет огромные преимущества.

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

О добродетели кодогенерации

Как-то, примерно полтора года назад, я посетил конференцию, посвящённую программным продуктам тогда ещё вполне независимой фирмы Rational. Одна из идей, высказанных оратором, заставила меня изменить мнение о том, как должно создаваться программное обеспечение. Он сказал примерно следующее: «Чтобы избежать ошибок при написании программного кода, надо прекратить писать этот код». То есть в контексте продуктов Rational, это значило, что лучше рисовать диаграммы UML, а потом положиться на сгенерированный Rational Rose код, который de facto не содержит ошибок.

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

  1. Необходимо создать язык или набор образов.
  2. Необходимо написать кодогенератор для перевода образов в код на C#, Java или C++.

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

      class Form
{
  string name;
  string caption;
  int width;
  int height;
  ArrayList controls;
}

Тогда функция генерации кода может быть записана так:

      string GenerateCSharpCode(Form form)
{
  StringBuilder sb = new StringBuilder(2000);
  sb.Append("class ");
  sb.Append(form.name);
  sb.Append(" : FormBridge{ public ");
  sb.Append(form.name);
  sb.Append("(AbstractFactory factory, Form form) : base(form){");

  // здесь мы вставляем код для создания control-ов
  ...........

  sb.Append("}");

  // здесь мы вставляем код для readonly-свойств,// предоставляющих доступ к control-ам
  ...........

  sb.Append("}");
  return sb.ToString();
}

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

Создание редактора представляет собой достаточно серьёзную, но вполне решаемую, проблему. Моя первая (и единственная) версия редактора, написанная на C#, не поддерживала многих операций, например, копирования и вставки контролов, а также отмены предыдущих операций. Но я мог визуально редактировать форму, сохранять её в XML, а также генерировать код. Этого было вполне достаточно. Потом редактор был переписан (не мной) при помощи дизайнеров .NET, и теперь он также поддерживает уйму другой функциональности, которая в конечном итоге сводится к кодогенерации. Например, этот редактор поддерживает генерацию кода для объектов предметной области (домена).

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

Конечно, на создание подобной среды разработки придётся потратить уйму времени, и это не считая времени, потраченного на проектирование и разработку framework. Но если вы создаёте действительно большую систему или набор однотипных систем для разных клиентов (например, программы бухгалтерского учёта), то со временем ваши разработки окупятся, поскольку простота и скорость разработки будет намного выше, чем если бы вы пользовались другими RAD-средствами.

Кроме этого, описанные таким образом формы независимы от языка программирования. Поэтому перенос приложения с .NET на Java, например, будет менее болезненным, поскольку придётся просто написать ещё одну функцию кодогенерации для артефактов системы. И, как вы уже должно быть заметили, код логики представления на C# и Java практически не отличим, за исключением различий в управлении событиями и использовании свойств вместо функций в C# версии. Но и эти различия могут быть сведены практически к нулю, например путём использования J# вместо C#.

Ложка дёгтя

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

Для начала – как бы мы не пытались мы никогда не сможем добиться абсолютно идентичной функциональности приложения в Windows и Web. Наипростейшим примером может служить вывод сообщения пользователю. В Windows вы будете писать что-то вроде:

C# code
MessageBox.Show("Hello");

В Web:

C# code for ASP.NET
Page.RegisterStartupScript("Message1", "<script>alert(\"Hello\")</script>");

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

То есть, как ни крутись, придётся сойтись на наименьшем общем наборе функциональности, которая может быть реализована в Windows и Web. И надо сказать, что практически всегда дизайн Web-составляющей будет иметь большее значение, поскольку если это работает в Web – вы можете это сделать и в Windows. Для борьбы с этим препятствием желательно потребовать, чтобы пользователи были оснащены web-браузером последней версии, например Internet Explorer 6.0. И если вас грызут сомнения по поводу этого ограничения, то смею вас заверить, что у меня ещё ни разу не возникло проблем из-за подобного выбора. Если же сомнения по-прежнему остались, то хочу заметить, что вы всегда можете написать ещё одну версию framework, которая будет поддерживать другие версии web-браузеров, не трогая ни абстрактных классов, ни формы, ни логику представления. Но это уже чисто техническая проблема.

Другая проблема, которая заслуживает внимания, и которая возникла из-за особенностей Web – это так называемый auto postback. То есть, например, когда пользователь выбирает что-то в combobox, необходимо обновить страницу. В этом случае говорят, что combobox должен делать auto postback, или, по-русски, идти на сервер. Данное свойство имеет значение для Web, но для Windows оно абсолютно бессмысленно. Вот как будет выглядеть код для combobox:

C# code
      abstract
      public
      class AbstractFactory
{
  abstractpublic ComboBox CreateComboBox(
        int id, int x, int y, int width, int height, bool autoPostBack);
}

publicclass WindowsFactory
{
  public ComboBox CreateComboBox(
        int id, int x, int y, int width, int height, bool autoPostBack)
  {
    returnnew ComboBox(id, x, y, width, height); // autoPostBack игнорируем
  }
}

publicclass WebFactory
{
  public ComboBox CreateComboBox(
        int id, int x, int y, int width, int height, bool autoPostBack)
  {
    returnnew ComboBox(form, id, x, y, width, height, autoPostBack); 
  }
}

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

C# code
      abstract
      public
      class AbstractFactory
{
  abstractpublic Control CreateControl(Type type, object[] parameters);
}
publicclass WindowsFactory
{
  Control CreateControl(Type type, object[] parameters)
  {
    if (type.Equals(typeof(Button)))
    {
      // разбираем параметры и создаём объект класса Button
    }
    if (type.Equals(typeof(TextBox)))
    {
      // разбираем параметры и создаём объект класса TextBox
    }
  }
}
Java code
      abstract
      public
      class AbstractFactory
{
  abstractpublic Control createControl(Class cl, Object[] parameters);
}
publicclass WebFactory
{
  Control createControl(Class cl, Object[] parameters)
  {
    if (cl.Equals(Button.class))
    {
      // разбираем параметры и создаём объект класса Button
    }
    if (cl.Equals(TextBox.class))
    {
      // разбираем параметры и создаём объект класса TextBox
    }
  }
}

В этом случае формы будут выглядеть так:

# code
      class Form1 : FormBridge
{
  public Form1(AbstractFactory factory, Form form) : base(form)
  {
    Ok = (Button)factory.CreateControl(
           typeof(Button), 
           newobject[]{0, 10, 100, 70, 20, "Ok"});
    form.AddControl(Ok);
  }
  publicreadonly Button Ok;
}
Java code
      class Form1 extends FormBridge
{
  public Form1(AbstractFactory factory, Form form)
  {
    super(form);
    Ok = (Button)factory.createControl(Button.class, 
           new Object[]{new Integer(0), 
                  new Integer(10),
                  new Integer(100),
                  new Integer(70),
                  new Integer(20),
                  "Ok"});
    form.addControl(Ok);
  }
  publicfinal Button Ok;
}

Некрасиво, но практично. Особенно если вы будете генерировать не просто C# или Java код, а непосредственно сборку (в случае использования .NET) или байт-код Java. В этом случае прикладные программисты не будут знать, что вы делаете что-то некрасивое внутри, ведь с их точки зрения интерфейс для взаимодействия с формой и контролами остаётся неизменным.

Заключение

Конечно, многие из вас могут поспорить со мной о необходимости создания подобного framework, особенно учитывая то, что требование одновременной работы приложения под Windows и в Web является крайне редким. Также стоит упомянуть, что для Windows-систем создание framework будет, скорее всего, не оправдано. Но то, что описанный в этой статье framework является очень даже полезным при разработке Web-приложений, в этом я могу вас заверить, исходя из собственного опыта. Просто взгляните на код логики представления и сравните его с аналогичным кодом для ASP.NET или JSP (Java Server Pages). Чистота, как известно, залог здоровья, и создание framework позволяет ни только навести эту чистоту, но и поддерживать её.

Существуют множество областей, где применимы принципы, изложенные в этой статье. Помните главное: проектирование framework – процесс творческий, и потому доставляющий безмерное чувство удовлетворения. А что может быть важнее занятия любимым делом? :)


Эта статья опубликована в журнале RSDN Magazine #5-2003. Информацию о журнале можно найти здесь