LXF94:Java EE

Материал из Linuxformat.

Перейти к: навигация, поиск
Java EE

Содержание

Команды и фабрики

ЧАСТЬ 6 Антон Черноусов готов познакомить вас с очередной партией паттернов, которые помогут сделать ваши приложения еще более гибкими и расширяемыми.

Вместо предисловия

В предыдущей статье мой коллега Александр Бабаев рассмотрел вопросы организации и использования БД в Java-приложениях, и в том числе вопросы подключения к БД посредством ConnectionPool.

Сегодня мы рассмотрим применение двух паттернов, безусловно, оказавших огромное воздействие на проектирование систем — Command и Factory Method. Их применение позволит сделать ваше приложение расширяемым.

Команды

Скачать исходный код примера

В LXF92 мы кратко описали стратегии, предназначенные для реализации «Контроллера», и обещали более подробно рассмотреть стратегию Command and Controller. Чтобы выполнить это обещание, нам придется сначала познакомиться с паттерном Command.

Задача, которая стоит перед контроллером (сервлетом) при получении управляющего сигнала, как правило, заключается в выполнении последовательности действий, часто атомарной (то есть обрабатываемой как единое целое). Например, в сервлете AddressBookServlet, реализованном в предыдущей статье, метод handleEdit вызывается, когда адрес на который обращается пользователь — это «/edit».

К сожалению, на примере AddressBookServlet мы видим, что при увеличении функциональности web-приложения растёт и количество методов, реализованных в сервлете; класс «засоряется», код становится менее структурированным и читабельным. Решить проблемы с кодом можно, «обернув» методы в специальные классы, которые будут выполнять атомарные операции и предоставлять сервлету стандартный интерфейс, предназначенный для этих целей. Для выполнения поставленной задачи воспользуемся паттерном Command.

Команды удобны прежде всего тем, что они маскируют конкретную реализацию, находящуюся за интерфейсной прослойкой. Интерфейс остается одним и тем же, независимо от того, с чем работает команда [1].

public interface Command {
    public void execute() throws Exception;
 }

Выше представлен простой интерфейс Command всего с одним методом execute(), который и олицетворяет идею одноименного паттерна. Он будет нашим стандартым интерфейсом. Более сложная реализация интерфейса включает метод unexecute(). Класс, реализующий интерфейс Command, инкапсулирует в методе execute() обозначенные выше атомарные операции, а в методе unexecute() реализуется механизм отмены. Часто методы execute() и unexecute() называют do() и undo(), соответственно.

Рис. 1. Диаграмма классов.
Рис. 1. Диаграмма классов.

Выделим команды, которые нам необходимо реализовать (отметим удачное разделение на методы): Add, Auth, Edit, View, Remove. Учитывая то, что у наших команд будут некоторые идентичные методы и атрибуты, предлагаю создать абстрактный класс AbstractHTTPCommand, в котором они будут собраны. Класс будет реализовывать интерфейс Command, и его наследование автоматически позволит обеспечить необходимый уровень интеграции. Итак, в основе каждой команды будет лежать абстрактный класс AbstractHTTPCommand, в котором реализован интерфейс для выполнения операций (на Рис. 1 вы можете видеть диаграмму классов команд нашего приложения).

Определим общие методы для абстрактного класса: initCommand() — предназначен для инициализации команды, makeDataToView() — для подготовки данных для отображения в случае их изменения, outputPage() — метод для переадресации пользователя (он будет перенесен из AddressBookServlet без изменений) и другие. Ниже представлена реализация методов initCommand() и makeDataToView():

protected void initCommand(ServletContext sc, HttpServletRequest
 aRequest,
    HttpServletResponse aResponse, String viewPath,
    String resultPath, String errorPath) {
  this.setSc(sc);
  this.setARequest(aRequest);
  this.setAResponse(aResponse);
  this.setResultPath(resultPath);
  this.setErrorPath(errorPath);
  this.setViewPath(viewPath);
 }
 public void makeDataToView() {
  Map<String, String> numbers = new HashMap<String, String>();
  Map<String, String> comments = new HashMap<String, String>();
  for (Map.Entry<String, Contact> entry :
     _addressBook.getContacts().entrySet()) {
    numbers.put(entry.getKey(), entry.getValue().getNumber());
    comments.put(entry.getKey(), entry.getValue().getComment());
  }
  aRequest.setAttribute("numbers", numbers);
  aRequest.setAttribute("comments", comments);
  if (aRequest.getAttribute("message") == null) {
    aRequest.setAttribute("message", "");
  }
 }

Метод makeDataToView() — существенная часть метода handleView() класса AddressBookServlet. Вы можете удивиться, для чего метод initCommand() содержит так много параметров; это необходимо для того, чтобы в момент создания команды полностью передать ей всю необходимую для ее выполнения информацию. Параметры viewPath, resultPath и errorPath появились не случайно — они предназначены для адресов (видов, если использовать термины MVC), используемых в случае простого отображения данных, удачного и, соответственно, неудачного выполнения команды.

Перейдем к реализации самих команд. Рассмотрим, например, метод execute() класса EditHTTPCommand. Он практически полностью соответствует первоначальному методу handleEdit класса AddressBookServlet, исключая переадресацию пользователя на конкретный вид.

public void execute() throws Exception {
    if (aRequest.getParameter("number") == null) {
     _addressBook.removeContactByNumber(aRequest.getParameter("number"));
     aRequest.setAttribute("message","Не определено, что редактировать");
     outputPage(this.getErrorPath(), aRequest, aResponse);
    } else if (aRequest.getParameter("edited") != null) {
     _addressBook.editContactByNumber(aRequest.getParameter("edited"),
        aRequest.getParameter("name"),
        aRequest.getParameter("number"),
        aRequest.getParameter("comment"));
     aRequest.setAttribute("message", "Контакт \"" +
        aRequest.getParameter("name") + "\" отредактирован");
     makeDataToView();
     outputPage(this.getResultPath(), aRequest, aResponse);
  } else {
     Contact contact = _addressBook.getContactByNumber(aRequest.getParameter("number"));
     aRequest.setAttribute("action", "edit");
     aRequest.setAttribute("edit.name", contact.getName());
     aRequest.setAttribute("edit.number", contact.getNumber());
     aRequest.setAttribute("edit.comment", contact.getComment());
     outputPage(this.getViewPath(), aRequest, aResponse);
   }
 }

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

public void execute() throws Exception {
   makeDataToView();
   outputPage(this.getViewPath(), aRequest, aResponse);}

Выполнение любой команды унифицировано и будет выглядеть следующим образом:

Command cmd;
   cmd = new EditHTTPCommand(this.getServletContext(),
         aRequest, aResponse, "edit.jsp", "view.jsp", "view.jsp");
   try {
       cmd.execute();
   } catch (Exception e) {
       e.printStackTrace();
 }

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

private void handle(HttpServletRequest aRequest,
    HttpServletResponse aResponse) throws ServletException, IOException {
    aRequest.setCharacterEncoding("utf-8");
    String target = aRequest.getRequestURI().substring(aRequest.getContextPath().length());
    Command cmd;
    if (target.equals("/")) {
       cmd = new ViewHTTPCommand(this.getServletContext(),aRequest, aResponse, "index.jsp", null, null);
    } else if ("/add".equals(target)) {
       cmd = new AddHTTPCommand(this.getServletContext(), Request, aResponse, "edit.jsp", "view.jsp", null);
    } else if ("/view".equals(target)) {
       cmd = new ViewHTTPCommand(this.getServletContext(), aRequest, aResponse, "view.jsp", null, null);
    } else if ("/edit".equals(target)) {
       cmd = new EditHTTPCommand(this.getServletContext(), aRequest, aResponse, "edit.jsp", "view.jsp", "view.jsp");
    } else if ("/remove".equals(target)) {
       cmd = new RemoveHTTPCommand(this.getServletContext(), aRequest, aResponse, null, "view.jsp", null);
    } else if ("/auth".equals(target)) {
       cmd = new AuthHTTPCommand(this.getServletContext(), aRequest, aResponse, "auth.jsp", "index.jsp", "auth.jsp");
    } else {
       cmd = new ViewHTTPCommand(this.getServletContext(), aRequest, aResponse, "view.jsp", null, null);
    }
    try {
       cmd.execute();
    } catch (Exception e) {
        // oopst...
        e.printStackTrace();
    }
 }

С одной стороны, от внедрения паттерна Command в наше web-приложение мы получили следующие преимущества: управление сосредоточилось в одном месте (в методе handle(), представленном выше), код стал более структурированным. С другой стороны, мы пока не добились возможности полного отторжения продукта от разработчика (при котором исходный код не передается заказчику), так как для изменения функциональности или доработки приложения (как минимум, при добавлении новой команды) необходимо производить перекомпиляцию.

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

  • Вынести связи между адресами, командами и видами за пределы приложения.
  • Заложить возможность создания экземпляра команды при наличии ее названия.
  • Изменить процесс вызова конкретной команды, чтобы при расширении приложения (дополнении новых команд) не требовалось вносить изменения в уже существующий код.

Хранение настроек команд

Для решения первой из поставленных задач мы не будем «изобретать велосипед» и воспользуемся средствами конфигурирования web-приложения. В файл web.xml введем параметры для нашего контроллера:

<servlet>
  <display-name>AddressBook</display-name>
  <servlet-name>ABServlet</servlet-name>
  <servlet-class>AddressBookServlet</servlet-class>
  <init-param>
       <param-name>/</param-name>
       <param-value>root</param-value>
  </init-param>
  <init-param>
       <param-name>rootCommand</param-name>
       <param-value>ViewHTTPCommand</param-value>
  </init-param>
  <init-param>
       <param-name>rootView</param-name>
       <param-value>index.jsp</param-value>
  </init-param>
  <init-param>
       <param-name>rootResult</param-name>
       <param-value>null</param-value>
  </init-param>
  <init-param>
       <param-name>rootError</param-name>
       <param-value>null</param-value>
       </init-param>
  <load-on-startup>0</load-on-startup>
 </servlet>

Для каждого адреса вводится параметр, который мы будем называть ключом (для «/» это «root»). Ключ с добавлением суффикса (Command, View, Result, Error) обозначает конкретный параметр, значение которого мы будем извлекать прямо в приложении. Для получения необходимых данных можно воспользоваться следующим кодом (хотя мы в дальнейшем будем действовать по-другому):

ServletContext sc = this.getServletContext();
   String key = sc.getInitParameter("/");
   String commandName = sc.getInitParameter(key + "Command");
   String commandView = sc.getInitParameter(key + "View");
   String commandResult = sc.getInitParameter(key + "Result");
   String commandError = sc.getInitParameter(key + "Error");

Аналогичные настройки необходимо сделать для всех адресов, с которыми работает наше приложение («/», «/add», «/auth», «/edit», «/remove», «/view»). Включение в файл конфигурации этих значений позволит настраивать web-приложение без исправлений в коде.

Создание экземпляра класса по имени

Экземпляр класса можно создавать при наличии полного имени, записанного в строке, с помощью методов forName() и newInstance() класса Class (например, для класса Date: Object o = Class.forName(«java.util.Date»).newInstance()). Если при создании объекта на основе имени класса конструктору необходимо передать ряд параметров, то воспользоваться данным методом нельзя, но создать объект возможно с помощью класса java.lang.reflect.Constructor, например:

Class c = Class.forName(key);
 Class[] parameterTypes = new Class[3];
 parameterTypes[0] = String.class;
 parameterTypes[1] = String.class;
 parameterTypes[2] = String.class;
 Constructor constructor = c.getConstructor(parameterTypes);
 Object[] args = new Object[3];
 args[0] = "par1";
 args[1] = "par2";
 args[2] = "par3";
 Object o = constructor.newInstance(args);

Давайте разберем приведенный пример. Сначала мы создаем экземпляр класса Class по имени key, далее выделяем массив из типов параметров в том количестве и последовательности, как они идут в нужном нам конструкторе. Используя метод getConstructor(), в качестве параметра которому мы передаем наш массив, получаем экземпляр класса Conctructor. Если у Conctructor вызвать метод newInstance(args), то мы получим требуемый класс.

Осталось разобраться с последней проблемой, в решении которой нам поможет паттерн Factory Method.

Фабрика команд

Суть паттерна Factory Method заключается в том, что его реализация позволяет создавать экземпляры конкретных объектов, причем в этом случае сохраняется зависимость от абстрактных интерфейсов. Следовательно, данный шаблон в значительной мере может пригодиться в процессе активной разработки приложений, при которой конкретные классы обладают высоким уровнем изменчивости, что и требуется для решения последней задачи [2].

Если мы применим идею паттерна Factory Method для создания фабрики команд к нашему web-приложению, оно приобретет свойство расширяемости за счет слабосвязанности классов (см. Рис. 2), то есть класс AddressBookServlet напрямую не будет связан с конкретными командами, а будет взаимодействовать с ними посредством Command и CommandFactory.

Рис. 2. Диаграмма классов, применение фабрики команд.
Рис. 2. Диаграмма классов, применение фабрики команд.

Ниже представлен ключевой метод getCommand() класса CommanFactory, в котором согласно параметру name извлекаются настройки из файла конфигурации web-приложения и создается экземпляр необходимой команды (метод реализован не оптимально):

public Command getCommand(String name, HttpServletRequest
 aRequest,
 HttpServletResponse aResponse) {
 if (name != null) {
   if (config.getInitParameter(name) != null) {
     try {
         String key = config.getInitParameter(name);
         Class cmd1 = Class.forName(config.getInitParameter(key + "Command"));
         try {
             Class[] parameterTypes = new Class[6];
             parameterTypes[0] = ServletContext.class;
             parameterTypes[1] = HttpServletRequest.class;
             parameterTypes[2] = HttpServletResponse.class;
             parameterTypes[3] = String.class;
             parameterTypes[4] = String.class;
             parameterTypes[5] = String.class;
             Constructor constructor = cmd1.getConstructor(parameterTypes);
             Object[] args = new Object[6];
             args[0] = sc;
             args[1] = aRequest;
             args[2] = aResponse;
             args[3] = config.getInitParameter(key + "View");
             args[4] = config.getInitParameter(key + "Result");
             args[5] = config.getInitParameter(key + "Error");
             Command cmd = (Command) constructor.newInstance(args);
             return cmd;
          } catch (Exception e) {
             e.printStackTrace();
             return null;
          }
        } catch (ClassNotFoundException e) {
          e.printStackTrace();
          return null;
        }}}
 return null;}

Добавим последний штрих в web-приложение — изменим метод handle класса AddressBookServlet, чтобы он работал с созданной фабрикой:

Command cmd;
 CommandFactory cf = new CommandFactory(this.getServletConfig(),this.getServletContext());
 cmd = cf.getCommand(
 aRequest.getRequestURI().substring(aRequest.getContextPath().length()),
 aRequest, aResponse);
 if (cmd == null) {
     cmd = cf.getCommand("/", aRequest, aResponse);
 }
 try {
     cmd.execute();
 } catch (Exception e) {
     // oopst...
     e.printStackTrace();
 }

Предложенная реализация закрывает для пользователя вызов не описанных в конфигурации адресов.

Что дальше?

Основной целью этой статьи было знакомство с паттернами Command и Factory Method, с чем мы неплохо справились, а заодно провели модернизацию web-приложения.

Создание экземпляров команд на основе файла конфигурации web-приложения, наверняка, не самая хорошая идея: более правильно выносить их настройку в отдельный файл (который кстати, можно будет менять из самого приложения, необходимо лишь реализовать соответствующие функции). Одно из улучшений, которое можно произвести — это изменить сами команды: сделать их stateless, то есть не хранящими внутри себя сведения о состоянии. Это улучшение позволило бы создать экземпляры команд один раз и использовать их и по мере необходимости.

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


Литература

  1. Тейт, Б. Горький вкус Java: Библиотека программиста. — СПб: Питер, 2003. — 333 с.
  2. Мартин, Р. С. Быстрая разработка программ: принципы, примеры, практика. — М.: Издательский дом «Вильямс», 2004. — 752 с.: ил.
Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию