Книга: Чистый код: создание, анализ и рефакторинг. Библиотека программиста
Назад: 10. Классы
Дальше: 12. Формирование архитектуры

11. Системы

Кевин Дин Уомплер

11_01.tif 

Сложность убивает. Она вытягивает жизненные силы из разработчиков, затрудняя планирование, построение и тестирование продуктов.

Рэй Оззи, технический директор Microsoft Corporation

Как бы вы строили город?

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

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

Группы разработки программного обеспечения тоже организуются по анало­гичным принципам, но системы, над которыми они работают, часто не имеют ана­логичного разделения обязанностей и уровней абстракции. Чистый код помогает достичь этой цели на нижних уровнях абстракции. В этой главе мы поговорим о том, как сохранить чистоту на более высоких уровнях, то есть на уровне системы.

Отделение конструирования системы  от ее использования

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

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

Фаза инициализации присутствует в каждом приложении. Это первая из областей ответственности (concerns), которую мы рассмотрим в этой главе, а сама концепция разделения ответственности относится к числу самых старых и важных приемов нашего ремесла.

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

Типичный пример:

public Service getService() {

  if (service == null)

    service = new MyServiceImpl(...);  // Инициализация по умолчанию,

                                       // подходящая для большинства случаев?

  return service;

}

Идиома ОТЛОЖЕННОЙ ИНИЦИАЛИЗАЦИИ обладает определенными достоинствами. Приложение не тратит времени на конструирование объекта до ­момента его фактического использования, а это может ускорить процесс инициализации. Кроме того, мы следим за тем, чтобы функция никогда не возвращала null.

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

Проблемы могут возникнуть и при тестировании. Если MyServiceImpl представляет собой тяжеловесный объект, нам придется позаботиться о том, чтобы перед вызовом метода в ходе модульного тестирования в поле service был сохранен соответствующий ТЕСТОВЫЙ ДУБЛЕР [Mezzaros07] или ФИКТИВНЫЙ ОБЪЕКТ. А поскольку логика конструирования смешана с логикой нормальной обработки, мы должны протестировать все пути выполнения (в частности, проверку null и ее блок). Наличие обеих обязанностей означает, что метод выполняет более одной операции, а это указывает на некоторое нарушение принципа единой ответственности.

Но хуже всего другое — мы не знаем, является ли MyServiceImpl правильным объектом во всех случаях. Я намекнул на это в комментарии. Почему класс с этим методом должен знать глобальный контекст? Можем ли мы вообще определить, какой объект должен здесь использоваться? И вообще, может ли один тип быть подходящим для всех возможных контекстов?

Конечно, одно вхождение ОТЛОЖЕННОЙ ИНИЦИАЛИЗАЦИИ не создает серьезных проблем. Однако в приложениях идиомы инициализации обычно встречаются во множество экземпляров. Таким образом, глобальная стратегия инициализации (если она здесь вообще присутствует) распределяется по всему приложению, с минимальной модульностью и значительным дублированием кода.

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

Отделение main

Один из способов отделения конструирования от использования заключается в простом перемещении всех аспектов конструирования в main (или модули, вызываемые из main). Далее весь остальной код системы пишется в предположении, что все объекты были успешно сконструированы и правильно связаны друг с другом (рис. 11.1).

22660.png 

Рис. 11.1. Изоляция конструирования в main

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

Фабрики

Конечно, в некоторых ситуациях момент создания объекта должен определяться приложением. Например, в системе обработки заказов приложение должно создать экземпляры товаров LineItem для включения их в объект заказа Order. В этом случае можно воспользоваться паттерном АБСТРАКТНАЯ ФАБРИКА [GOF], чтобы приложение могло само выбрать момент для создания LineItem, но при этом подробности конструирования были отделены от кода приложения (рис. 11.2).

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

22673.png 

Рис. 11.2. Отделение конструирования с применением фабрики

Внедрение зависимостей

Внедрение зависимостей (DI, Dependency Injection) — мощный механизм отделения конструирования от использования, практическое применение обращения контроля (IoC, Inversion of Control) в области управления зависимостями. Обращение контроля перемещает вторичные обязанности объекта в другие объекты, созданные специально для этой цели, тем самым способствуя соблюдению принципа единой ответственности. В контексте управления зависимостями объект не должен брать на себя ответственность за создание экземпляров зависимостей. Вместо этого он передает эту обязанность другому «уполномоченному» механизму. Так как инициализация является глобальной областью ответственности, этим уполномоченным механизмом обычно является либо функция main, либо специализированный контейнер.

Примером «частичной» реализации внедрения зависимостей является запрос JNDI, когда объект обращается к серверу каталоговой информации с запросом на предоставление «сервиса» с заданным именем:

MyService myService = (MyService)(jndiContext.lookup("NameOfMyService"));

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

Истинное внедрение зависимостей идет еще на один шаг вперед. Класс не предпринимает непосредственных действий по разрешению своих зависимостей; он остается абсолютно пассивным. Вместо этого он предоставляет set-методы  и/или аргументы конструктора, используемые для внедрения зависимостей. В процессе конструирования контейнер DI создает экземпляры необходимых объектов (обычно по требованию) и использует аргументы конструктора или set-методы для скрепления зависимостей. Фактически используемые зависимые объекты задаются в конфигурационном файле или на программном уровне в специализированном конструирующем модуле.

Самый известный DI-контейнер для Java присутствует в Spring Framework. Подключаемые объекты перечисляются в конфигурационном файле XML, после чего конкретный объект запрашивается по имени в коде Java. Пример будет рассмотрен ниже.

Но как же преимущества ОТЛОЖЕННОЙ ИНИЦИАЛИЗАЦИИ? Эта идиома иногда бывает полезной и при внедрении зависимостей. Во-первых, большинство DI-контейнеров не конструирует объекты до того момента, когда это станет необходимо. Во-вторых, многие из этих контейнеров предоставляют механизмы использования фабрик или конструирования посредников (proxies), которые могут использоваться для ОТЛОЖЕННОЙ ИНИЦИАЛИЗАЦИИ и других аналогичных оптимизаций.

Масштабирование

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

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

Рост не обходится без проблем. Сколько раз вам приходилось едва ползти в потоке машин вдоль проекта по «расширению дороги», когда вы спрашивали себя: «Почему нельзя было сразу построить дорогу достаточной ширины?!»

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

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

А как же системный уровень? Разве архитектура системы не требует предварительного планирования? Не может же она последовательно расти от простого к сложному?

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

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

Исходные архитектуры EJB1 и EJB2 не обеспечивали должного разделения областей ответственности и поэтому создавали лишние барьеры для естественного роста. Возьмем хотя бы компонент-сущность (Entity Bean) для постоянного (persistent) класса. Компонентом-сущностью называется представление реляционных данных (иначе говоря, записи таблицы) в памяти.

Для начала необходимо определить локальный (внутрипроцессный) или удаленный (на отдельной JVM) интерфейс, который будет использоваться клиентами. Возможный локальный интерфейс представлен в листинге 11.1.

Листинг 11.1. Локальный интерфейс EJB2 для EJB Bank

package com.example.banking;

import java.util.Collections;

import javax.ejb.*;

 

public interface BankLocal extends java.ejb.EJBLocalObject {

  String getStreetAddr1() throws EJBException;

  String getStreetAddr2() throws EJBException;

  String getCity() throws EJBException;

  String getState() throws EJBException;

  String getZipCode() throws EJBException;

  void setStreetAddr1(String street1) throws EJBException;

  void setStreetAddr2(String street2) throws EJBException;

  void setCity(String city) throws EJBException;

  void setState(String state) throws EJBException;

  void setZipCode(String zip) throws EJBException;

  Collection getAccounts() throws EJBException;

  void setAccounts(Collection accounts) throws EJBException;

  void addAccount(AccountDTO accountDTO) throws EJBException;

}

В интерфейс включены некоторые атрибуты адреса Bank, а также коллекция счетов, принадлежащих банку; данные каждого счета представляются отдельным EJB Account. В листинге 11.2 приведен соответствующий класс реализации компонента Bank.

Листинг 11.2. Соответствующая реализация компонента-сущности EJB2

package com.example.banking;

import java.util.Collections;

import javax.ejb.*;

 

public abstract class Bank implements javax.ejb.EntityBean {

  // Бизнес-логика...

  public abstract String getStreetAddr1();

  public abstract String getStreetAddr2();

  public abstract String getCity();

  public abstract String getState();

  public abstract String getZipCode();

  public abstract void setStreetAddr1(String street1);

  public abstract void setStreetAddr2(String street2);

  public abstract void setCity(String city);

  public abstract void setState(String state);

  public abstract void setZipCode(String zip);

  public abstract Collection getAccounts();

  public abstract void setAccounts(Collection accounts);

  public void addAccount(AccountDTO accountDTO) {

    InitialContext context = new InitialContext();

    AccountHomeLocal accountHome = context.lookup("AccountHomeLocal");

    AccountLocal account = accountHome.create(accountDTO);

    Collection accounts = getAccounts();

    accounts.add(account);

  }

  // Логика контейнера EJB

  public abstract void setId(Integer id);

  public abstract Integer getId();

  public Integer ejbCreate(Integer id) { ... }

  public void ejbPostCreate(Integer id) { ... }

  // Остальные методы должны быть реализованы, но обычно остаются пустыми:

  public void setEntityContext(EntityContext ctx) {}

  public void unsetEntityContext() {}

  public void ejbActivate() {}

  public void ejbPassivate() {}

  public void ejbLoad() {}

  public void ejbStore() {}

  public void ejbRemove() {}

}

В листинге не приведен ни соответствующий интерфейс LocalHome (по сути — фабрика, используемая для создания объектов), ни один из возможных методов поиска Bank, которые вы можете добавить.

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

Бизнес-логика тесно привязана к «контейнеру» приложения EJB2. Вы должны субклассировать контейнерные типы, а также предоставить многие методы жизненного цикла, необходимые для контейнера.

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

Наконец, такое решение противоречит принципам объектно-ориентированного программирования. Один компонент не может наследовать от другого компонента. Обратите внимание на логику добавления нового счета. В EJB2 компоненты часто определяют «объекты передачи данных» (DTO), которые фактически представляют собой «структуры без поведения». Обычно это приводит к появлению избыточных типов, содержащих по сути одинаковые данные, и необходимости использования стереотипного кода для копирования данных между объектами.

Поперечные области ответственности

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

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

Теоретически возможен модульный, инкапсулированный подход к определению стратегии сохранения объектов. Однако на практике вам приходится повторять по сути одинаковый код, реализующий стратегию сохранения, во многих объектах. Для подобных областей используется термин «поперечные области ответственности». При этом инфраструктура сохранения может быть модульной, и логика предметной области, рассматриваемая в изоляции, тоже может быть модульной. Проблемы возникают в точках пересечения этих областей. Можно сказать, что подход, использованный в архитектуре EJB по отношению к сохранению объектов, безопасности и транзакциям, предвосхитил аспектно-ориентированное программирование (АОП), которое представляет собой универсальный подход к восстановлению модульности для поперечных областей ответственности.

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

В примере с сохранением объектов вы объявляете, какие объекты, атрибуты и т.д. должны сохраняться, а затем делегируете задачи сохранения своей инфраструктуре сохранения. Изменения в поведении вносятся инфраструктурой АОП без вмешательства в целевой код. Рассмотрим три аспекта (или «аспекто-подобных» механизма) в Java.

Посредники

Посредники (proxies) хорошо подходят для простых ситуаций — например, для создания «оберток» для вызова методов отдельных объектов или классов. Тем не менее динамические посредники, содержащиеся в JDK, работают только с интерфейсами. Чтобы создать посредника для класса, приходится использовать библиотеки для выполнения манипуляций с байт-кодом — такие, как CGLIB, ASM или Javassist.

В листинге 11.3 приведена заготовка посредника JDK, обеспечивающего поддержку сохранения объектов в нашем приложении Bank (представлены только методы чтения/записи списка счетов).

Листинг 11.3. Пример посредника JDK

// Bank.java (подавление имен пакетов...)

import java.utils.*;

 

// Абстрактное представление банка.

public interface Bank {

  Collection<Account> getAccounts();

  void setAccounts(Collection<Account> accounts);

}

 

// BankImpl.java

import java.utils.*;

 

// POJO-объект ("Plain Old Java Object"), реализующий абстракцию.

public class BankImpl implements Bank {

  private List<Account> accounts;

  public Collection<Account> getAccounts() {

    return accounts;

  }

  public void setAccounts(Collection<Account> accounts) {

    this.accounts = new ArrayList<Account>();

    for (Account account: accounts) {

      this.accounts.add(account);

    }

  }

}

 

// BankProxyHandler.java

import java.lang.reflect.*;

import java.util.*;

 

// Реализация InvocationHandler, необходимая для API посредника.

public class BankProxyHandler implements InvocationHandler {

  private Bank bank;

 

  public BankHandler (Bank bank) {

    this.bank = bank;

  }

 

  // Метод, определенный в InvocationHandler

  public Object invoke(Object proxy, Method method, Object[] args)

      throws Throwable {

    String methodName = method.getName();

    if (methodName.equals("getAccounts")) {

      bank.setAccounts(getAccountsFromDatabase());

      return bank.getAccounts();

    } else if (methodName.equals("setAccounts")) {

      bank.setAccounts((Collection<Account>) args[0]);

      setAccountsToDatabase(bank.getAccounts());

      return null;

    } else {

      ...

    }

  }

 

  // Подробности:

  protected Collection<Account> getAccountsFromDatabase() { ... }

  protected void setAccountsToDatabase(Collection<Account> accounts) { ... }

}

 

// В другом месте...

Bank bank = (Bank) Proxy.newProxyInstance(

  Bank.class.getClassLoader(),

  new Class[] { Bank.class },

  new BankProxyHandler(new BankImpl()));

Мы определили интерфейс Bank, который будет инкапсулироваться посредником, и POJO-объект («Plain  Old Java Object», то есть «обычный Java-объект») BankImpl, реализующий бизнес-логику. (Вскоре мы вернемся к теме POJO-объектов).

Для работы посредника необходим объект InvocationHandler, который вызывается для реализации всех вызовов методов Bank, обращенных к посреднику. Наша реализация BankProxyHandler использует механизм рефлексии Java для отображения вызовов обобщенных методов на соответствующие методы BankImpl.

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

АОП-инфраструктуры на «чистом» Java

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

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

Вся необходимая инфраструктура приложения, включая поперечные области ответственности (сохранение объектов, транзакции, безопасность, кэширование, преодоление отказов и т.д.), определяется при помощи декларативных конфигурационных файлов или API. Во многих случаях вы фактически определяете аспекты библиотек Spring или JBoss, а инфраструктура берет на себя всю механику использования посредников Java или библиотек байт-кода в режиме, прозрачном для пользователя. Объявления управляют контейнером внедрения зависимостей (DI), который создает экземпляры основных объектов и связывает их по мере необходимости.

В листинге 11.4 приведен типичный фрагмент конфигурационного файла Spring V2.5 app.xml .

Листинг 11.4. Конфигурационный файл Spring 2.X

<beans>

  ...

  <bean id="appDataSource"

    class="org.apache.commons.dbcp.BasicDataSource"

    destroy-method="close"

    p:driverClassName="com.mysql.jdbc.Driver"

    p:url="jdbc:mysql://localhost:3306/mydb"

    p:username="me"/>

 

  <bean id="bankDataAccessObject"

    class="com.example.banking.persistence.BankDataAccessObject"

    p:dataSource-ref="appDataSource"/>

 

  <bean id="bank"

    class="com.example.banking.model.Bank"

    p:dataAccessObject-ref="bankDataAccessObject"/>

  ...

</beans>

Каждый компонент напоминает одну из частей русской «матрешки»: объект предметной области Bank «упаковывается» в объект доступа к данным DAO (Data Accessor Object), который, в свою очередь, упаковывается в объект источника данных JDBC (рис. 11.3).

22757.png 

Рис. 11.3. «Матрешка» из декораторов

Клиент полагает, что он вызывает метод getAccounts() объекта Bank, но в дейст­вительности он взаимодействует с внешним объектом из набора вложенных ДЕКОРАТОРОВ [GOF], расширяющих базовое поведение POJO-объекта Bank. Мы могли бы добавить другие декораторы для транзакций, кэширования и т.д.

Чтобы запросить у DI-контейнера объекты верхнего уровня, заданные в файле XML, достаточно включить в приложение несколько строк:

XmlBeanFactory bf =

  new XmlBeanFactory(new ClassPathResource("app.xml", getClass()));

Bank bank = (Bank) bf.getBean("bank");

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

Хотя код XML занимает много места и плохо читается, определяемая в этих конфигурационных файлах «политика» все же проще сложной логики посредников и аспектов, скрытой от наших глаз и создаваемой автоматически. Архитектура выглядит настолько заманчиво, что инфраструктуры вроде Spring привели к полной переработке стандарта EJB для версии 3. EJB3 в значительной мере следует характерной для Spring модели декларативной поддержки поперечных областей ответственности с использованием конфигурационных файлов XML и/или аннотаций Java 5.

В листинге 11.5 приведен объект Bank, переписанный для EJB3.

Листинг 11.5. Компонент Bank для EBJ3

package com.example.banking.model;

import javax.persistence.*;

import java.util.ArrayList;

import java.util.Collection;

 

@Entity

@Table(name = "BANKS")

public class Bank implements java.io.Serializable {

   @Id @GeneratedValue(strategy=GenerationType.AUTO)

   private int id;

 

   @Embeddable // Объект "встраивается" в запись базы данных Bank

   public class Address {

      protected String streetAddr1;

      protected String streetAddr2;

      protected String city;

      protected String state;

      protected String zipCode;

   }

 

   @Embedded

   private Address address;

 

   @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER,

              mappedBy="bank")

   private Collection<Account> accounts = new ArrayList<Account>();

 

   public int getId() {

      return id;

   }

   public void setId(int id) {

      this.id = id;

   }

 

   public void addAccount(Account account) {

      account.setBank(this);

      accounts.add(account);

   }

 

   public Collection<Account> getAccounts() {

      return accounts;

   }

 

   public void setAccounts(Collection<Account> accounts) {

      this.accounts = accounts;

   }

}

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

Часть информации о сохранении объектов, содержащейся в аннотациях, можно при желании переместить в дескрипторы XML, оставив действительно чистый POJO-объект. Если детали сохранения объектов изменяются относительно редко, многие группы отдадут предпочтение аннотациям, но с гораздо меньшими отрицательными последствиями по сравнению с EJB2.

Аспекты AspectJ

Наконец, самым полнофункциональным инструментом для разделения областей ответственности посредством использования аспектов является язык AspectJ — расширение Java, предоставляющее «полноценную» поддержку аспектов как модульных конструкций. Чистых Java-решений на базе Spring и JBoss достаточно для 80–90% ситуаций, в которых применяются аспекты. Тем не менее AspectJ предоставляет очень мощный и разносторонний инструментарий для реализации разделения ответственности. Недостатком AspectJ является необходимость освоения нескольких новых инструментов, а также изучения новых языковых конструкций и идиом.

Эти проблемы отчасти компенсируются появившейся недавно «аннотационной» формой AspectJ, в которой аннотации Java 5 используются для определения аспектов в «чистом» коде Java. Кроме того, Spring Framework также содержит ряд функций, существенно упрощающих внедрение аспектов на базе аннотаций в рабочих группах с ограниченным опытом применения AspectJ.

Полное описание AspectJ выходит за рамки книги. За дополнительной информацией обращайтесь к [AspectJ], [Colyer] и [Spring].

Испытание системной архитектуры

Трудно переоценить потенциал разделения ответственности посредством аспектных решений. Если вы можете написать логику предметной области своего приложения в виде POJO-объектов, отделенных от любых архитектурных областей ответственности на кодовом уровне, то перед вами открывается возможность проведения полноценных испытаний вашей архитектуры. Вы сможете развивать ее от простого к сложному, как потребует ситуация, подбирая новые технологии по мере надобности. Не обязательно создавать Большой Изначальный Проект (BDUF, Big Design Up Front). Более того, это даже вредно, потому что BDUF снижает возможность адаптации к изменениям из-за нашего психологического нежелания расставаться с результатами уже затраченных усилий; кроме того, изначально принятые решения влияют на наши последующие представления об архитектуре.

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

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

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

Ранняя архитектура EJB была всего лишь одним из многих API, которые отличались излишней сложностью, нарушавшей принцип разделения ответственности. Впрочем, даже хорошо спроектированный API может оказаться «перебором» в конкретной ситуации, если его применение не объясняется реальной необходимостью. Хороший API должен исчезать из вида большую часть времени, чтобы большая часть творческих усилий группы расходовалась на реализацию пожеланий пользователей. В противном случае архитектурные ограничения помешают оптимальной реализации интересов клиента.

Подведем итог.

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

Оптимизация принятия решений

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

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

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

Применяйте стандарты разумно,  когда они приносят очевидную пользу

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

Многие группы использовали архитектуру EJB2 только потому, что она считалась стандартом, даже если в их проектах хватило бы более легких и прямолинейных решений. Я видел группы, которые теряли голову от разрекламированных стандартов и забывали о своей главной задаче: реализовывать интересы клиента.

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

Системам необходимы предметно-ориентированные языки

В области строительства, как и в большинстве технических областей, сформировался богатый язык со своим словарем, идиомами и паттернами, позволяющими четко и лаконично передать важную информацию. В области разработки программного обеспечения в последнее время снова возобновился интерес к предметно-ориентированным языкам (DSL, Domain-Specic Languages) — отдельным маленьким сценарным языкам или API стандартных языков, код которых читается как структурированная форма текста, написанного экспертом в данной предметной области.

Хороший предметно-ориентированный язык сводит к минимуму «коммуникационный разрыв» между концепцией предметной области и кодом, реализующим эту концепцию — по аналогии с тем, как гибкие методологии оптимизируют обмен информацией между группой и ключевыми участниками проекта. Реализация логики предметной области на языке, используемом экспертом в этой области, снижает риск неверного представления предметной области в коде.

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

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

Заключение

Чистым должен быть не только код, но и архитектура системы. Агрессивная, «всепроникающая» архитектура скрывает логику предметной области и снижает гибкость. Первое приводит к снижению качества: ошибкам проще спрятаться в коде, а разработчику труднее реализовать пожелания пользователей. Второе оборачивается снижением производительности, а также потерей всех преимуществ TDD.

Намерения разработчика должны быть четко выражены на всех уровнях абстракции. Это произойдет только в том случае, если он создает POJO-объекты, и использует аспекты (или другие аналогичные механизмы) для неагрессивного воплощения других сторон реализации.

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

Литература

[Alexander]: Christopher Alexander,  A Timeless Way of Building, Oxford University Press, New York, 1979.

[AOSD]: Aspect-Oriented Software Development port,

[ASM]: ASM Home Page,

[AspectJ]:

[CGLIB]: Code Generation Library,

[Colyer]: Adrian Colyer, Andy Clement, George Hurley, Mathew Webster,  Eclipse

AspectJ, Person Education, Inc., Upper Saddle River, NJ, 2005.

[DSL]: Domain-specific programming language,

[Fowler]: Inversion of Control Containers and the Dependency Injection pattern,

[Goetz]: Brian Goetz, Java Theory and Practice: Decorating with Dynamic Proxies,

[Javassist]: Javassist Home Page,

[JBoss]: JBoss Home Page,

[JMock]: JMock — A Lightweight Mock Object Library for Java,

[Kolence]: Kenneth W. Kolence, Software physics and computer performance measurements, Proceedings of the ACM annual conference—Volume 2, Boston, Massachusetts, pp. 1024–1040, 1972.

[Spring]: The Spring Framework,

[Mezzaros07]: XUnit Patterns, Gerard Mezzaros, Addison-Wesley, 2007.

[GOF]: Design Patterns: Elements of Reusable Object Oriented Software, Gamma et al., Addison-Wesley, 1996.

Например, см. [Fowler].

См. [Spring] и описание Spring.NET.

Не забывайте, что отложенная инициализация — всего лишь разновидность оптимизации… и возможно, преждевременная!

1 Система управления базами данных.

За общей информацией об аспектах обращайтесь к [AOSD], а за конкретной информацией об AspectJ — к [AspectJ] и [Colyer].

То есть без необходимости ручного редактирования целевого кода.

См. [CGLIB], [ASM] и [Javassist].

Более подробные примеры API посредников и его использования можно найти, например, в [Goetz].

Методологию АОП иногда путают с приемами, используемыми для ее реализации например перехватом методов и «инкапсуляцией» посредников. Подлинная ценность АОП-системы заключается в способности модульного, компактного определения системного поведения.

См. [Spring] и [JBoss]. «Непосредственно на уровне Java» в данном случае означает «без применения AspectJ».

По материалам .

Приведенный пример можно упростить — существуют специальные механизмы, использующие правила конфигурации и аннотации Java 5 для сокращения объема явно определяемой «связующей» логики.

По материалам .

См. [AspectJ] и [Colyer].

Не путайте с полезной практикой упреждающего проектирования. BDUF — привычка проектировать заранее все без исключения, до написания какого-либо кода реализации.

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

Выражение «физика программного продукта» впервые было использовано в [Kolence].

Работа [Alexander] оказала особенно заметное влияние на сообщество разработчиков ПО.

Например, см. [DSL]. [JMock] — хороший пример Java API, создавшего свой предметно-ориентированный язык.

Назад: 10. Классы
Дальше: 12. Формирование архитектуры