Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
186 Глава 11 . Системы Истинное внедрение зависимостей идет еще на один шаг вперед . Класс не пред- принимает непосредственных действий по разрешению своих зависимостей; он остается абсолютно пассивным . Вместо этого он предоставляет set -методы и/или аргументы конструктора, используемые для внедрения зависимостей . В процессе конструирования контейнер DI создает экземпляры необходимых объектов (обычно по требованию) и использует аргументы конструктора или set -методы для скрепления зависимостей . Фактически используемые зависи- мые объекты задаются в конфигурационном файле или на программном уровне в специализированном конструирующем модуле . Самый известный DI-контейнер для Java присутствует в Spring Framework 1 Подключаемые объекты перечисляются в конфигурационном файле XML, по- сле чего конкретный объект запрашивается по имени в коде Java . Пример будет рассмотрен ниже . Но как же преимущества ОТЛОЖЕННОЙ ИНИЦИАЛИЗАЦИИ? Эта идиома иногда бывает полезной и при внедрении зависимостей . Во-первых, большинство DI-контейнеров не конструирует объекты до того момента, когда это станет не- обходимо . Во-вторых, многие из этих контейнеров предоставляют механизмы использования фабрик или конструирования посредников (proxies), которые могут использоваться для ОТЛОЖЕННОЙ ИНИЦИАЛИЗАЦИИ и других аналогичных оптимизаций 2 Масштабирование Города вырастают из городков, которые, в свою очередь, появляются на месте деревень . Дороги сначала узки и едва заметны, но со временем они расширя- ются и покрываются камнем . Мелкие строения и пустые места заполняются более крупными зданиями, часть из которых в конечном итоге будет заменена небоскребами . На первых порах в городе полностью отсутствует инфраструктура: водопровод, электричество, канализация и (о ужас!) Интернет . Все эти возможности добав- ляются позднее, с ростом населения и плотности застройки . Рост не обходится без проблем . Сколько раз вам приходилось едва ползти в по- токе машин вдоль проекта по «расширению дороги», когда вы спрашивали себя: «Почему нельзя было сразу построить дорогу достаточной ширины?!» Но иначе и быть не могло . Кто сможет объяснить затраты на строительство ше- стиполосной магистрали в середине маленького городка, которому предрекают расширение? Да и кто бы захотел иметь такую дорогу в своем городе? 1 См . [Spring] и описание Spring .NET . 2 Не забывайте, что отложенная инициализация — всего лишь разновидность оптимизации… и возможно, преждевременная! 186 Масштабирование 187 Возможность построить «правильную систему с первого раза» — миф . Вместо этого мы сегодня реализуем текущие потребности, а завтра перерабатываем и расширяем систему для реализации новых потребностей . В этом заключается суть итеративной, пошаговой гибкой разработки . Разработка через тестирование, рефакторинг и полученный в результате их применения чистый код обеспечива- ют работу этой схемы на уровне кода . А как же системный уровень? Разве архитектура системы не требует предвари- тельного планирования? Не может же она последовательно расти от простого к сложному? В этом проявляется важнейшее отличие программных систем от физических. Ар- хитектура программных систем может развиваться последовательно, если обе- спечить правильное разделение ответственности. Как вы вскоре убедитесь, нематериальная природа программных систем делает это возможным . Но давайте начнем с контрпримера архитектуры, в которой нормальное разделение ответственности отсутствует . Исходные архитектуры EJB1 и EJB2 не обеспечивали должного разделения об- ластей ответственности и поэтому создавали лишние барьеры для естественного роста . Возьмем хотя бы компонент-сущность (Entity Bean) для постоянного (per- sistent) класса . Компонентом-сущностью называется представление реляционных данных (иначе говоря, записи таблицы) в памяти . Для начала необходимо определить локальный (внутрипроцессный) или удален- ный (на отдельной 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; } 187 188 Глава 11 . Системы В интерфейс включены некоторые атрибуты адреса 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, которые определяют подробности соответствия между объектом 188 Масштабирование 189 и реляционными данными, желаемое транзакционное поведение, ограничения безопасности и т . д . Бизнес-логика тесно привязана к «контейнеру» приложения EJB2 . Вы должны субклассировать контейнерные типы, а также предоставить многие методы жиз- ненного цикла, необходимые для контейнера . Привязка к тяжеловесному контейнеру затрудняет изолированное модульное тестирование . Приходится либо имитировать контейнер, что непросто, либо тратить много времени на развертывание EJB и тестов на реальном сервере . По- вторное использование за пределами архитектуры EJB2 практически невозможно из-за жесткой привязки . Наконец, такое решение противоречит принципам объектно-ориентированного программирования . Один компонент не может наследовать от другого компонен- та . Обратите внимание на логику добавления нового счета . В EJB2 компоненты часто определяют «объекты передачи данных» (DTO), которые фактически пред- ставляют собой «структуры без поведения» . Обычно это приводит к появлению избыточных типов, содержащих по сути одинаковые данные, и необходимости использования стереотипного кода для копирования данных между объектами . Поперечные области ответственности В некоторых областях архитектура EJB2 приближается к полноценному разде- лению ответственности . Например, желательное поведение в области транзак- ционности, безопасности и сохранения объектов объявляется в дескрипторах независимо от исходного кода . Такие области, как сохранение объектов, выходят за рамки естественных границ объектов предметной области . Например, все объекты обычно сохраняются по одной стратегии, с использованием определенной СУБД 1 вместо неструктуриро- ванных файлов, с определенной схемой выбора имен таблиц и столбцов, единой транзакционной семантикой и т . д . Теоретически возможен модульный, инкапсулированный подход к определению стратегии сохранения объектов . Однако на практике вам приходится повторять по сути одинаковый код, реализующий стратегию сохранения, во многих объ- ектах . Для подобных областей используется термин «поперечные области от- ветственности» . При этом инфраструктура сохранения может быть модульной, и логика предметной области, рассматриваемая в изоляции, тоже может быть мо- дульной . Проблемы возникают в точках пересечения этих областей . Можно ска- зать, что подход, использованный в архитектуре EJB по отношению к сохранению объектов, безопасности и транзакциям, предвосхитил аспектно-ориентированное программирование (АОП 2 ), которое представляет собой универсальный подход к восстановлению модульности для поперечных областей ответственности . 1 Система управления базами данных . 2 За общей информацией об аспектах обращайтесь к [AOSD], а за конкретной информацией об AspectJ — к [AspectJ] и [Colyer] . 189 190 Глава 11 . Системы В АОП специальные модульные конструкции, называемые аспектами, опреде- ляют, в каких точках системы поведение должно меняться некоторым после- довательным образом в соответствии с потребностями определенной области ответственности . Определение осуществляется на уровне декларативного или программного механизма . В примере с сохранением объектов вы объявляете, какие объекты, атрибуты и т . д . должны сохраняться, а затем делегируете задачи сохранения своей инфраструк- туре сохранения . Изменения в поведении вносятся инфраструктурой АОП без вмешательства в целевой код 1 . Рассмотрим три аспекта (или «аспекто-подобных» механизма) в Java . Посредники Посредники (proxies) хорошо подходят для простых ситуаций — например, для создания «оберток» для вызова методов отдельных объектов или классов . Тем не менее динамические посредники, содержащиеся в JDK, работают только с интерфейсами . Чтобы создать посредника для класса, приходится использовать библиотеки для выполнения манипуляций с байт-кодом — такие, как CGLIB, ASM или Javassist 2 В листинге 11 .3 приведена заготовка посредника JDK, обеспечивающего под- держку сохранения объектов в нашем приложении Bank (представлены только методы чтения/записи списка счетов) . листинг 11 .3 . Пример посредника JDK // Bank.java (подавление имен пакетов...) import java.utils.*; // Абстрактное представление банка. public interface Bank { Collection void setAccounts(Collection } // BankImpl.java import java.utils.*; // POJO-объект ("Plain Old Java Object"), реализующий абстракцию. public class BankImpl implements Bank { private List public Collection } public void setAccounts(Collection 1 То есть без необходимости ручного редактирования целевого кода . 2 См . [CGLIB], [ASM] и [Javassist] . 190 Посредники 191 this.accounts = new ArrayList 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 setAccountsToDatabase(bank.getAccounts()); return null; } else { } } // Подробности: protected Collection protected void setAccountsToDatabase(Collection } // В другом месте... Bank bank = (Bank) Proxy.newProxyInstance( Bank.class.getClassLoader(), new Class[] { Bank.class }, new BankProxyHandler(new BankImpl())); Мы определили интерфейс Bank , который будет инкапсулироваться посредником, и POJO-объект («Plain Old Java Object», то есть «обычный Java-объект») Ban- kImpl , реализующий бизнес-логику . (Вскоре мы вернемся к теме POJO-объектов) . Для работы посредника необходим объект InvocationHandler , который вызывается для реализации всех вызовов методов Bank , обращенных к посреднику . Наша реа- 191 192 Глава 11 . Системы лизация BankProxyHandler использует механизм рефлексии Java для отображения вызовов обобщенных методов на соответствующие методы BankImpl Код получается весьма объемистым и относительно сложным, даже в этом про- стом случае 1 . Не меньше проблем создает и использование библиотек для ма- нипуляций с байт-кодом . Объем и сложность кода — два основных недостатка посредников . Эти два фактора усложняют создание чистого кода! Кроме того, у посредников не существует механизма определения «точек интереса» обще- системного уровня, необходимых для полноценного АОП-решения 2 АОП-инфраструктуры на «чистом» Java К счастью, большая часть шаблонного кода посредников может автоматически обрабатываться вспомогательными средствами . Посредники используются во внутренней реализации нескольких инфраструктур Java — например, Spring AOP и JBoss AOP — для реализации аспектов непосредственно на уровне Java 3 В Spring бизнес-логика записывается в форме POJO-объектов . Такие объекты полностью сосредоточены на своей предметной области . Они не имеют зави- симостей во внешних инфраструктурах (или любых других областях); соответ- ственно им присуща большая концептуальная простота и удобство тестирования . Благодаря относительной простоте вам будет проще обеспечить правильную реализацию соответствующих пожеланий пользователей, а также сопровождение и эволюцию кода при появлении новых пожеланий . Вся необходимая инфраструктура приложения, включая поперечные области ответственности (сохранение объектов, транзакции, безопасность, кэширование, преодоление отказов и т . д .), определяется при помощи декларативных конфи- гурационных файлов или API . Во многих случаях вы фактически определяете аспекты библиотек Spring или JBoss, а инфраструктура берет на себя всю ме- ханику использования посредников Java или библиотек байт-кода в режиме, прозрачном для пользователя . Объявления управляют контейнером внедрения зависимостей (DI), который создает экземпляры основных объектов и связывает их по мере необходимости . В листинге 11 .4 приведен типичный фрагмент конфигурационного файла Spring V2 .5 app.xml 4 1 Более подробные примеры API посредников и его использования можно найти, например, в [Goetz] . 2 Методологию АОП иногда путают с приемами, используемыми для ее реализации напри- мер перехватом методов и «инкапсуляцией» посредников . Подлинная ценность АОП- системы заключается в способности модульного, компактного определения системного поведения . 3 См . [Spring] и [JBoss] . «Непосредственно на уровне Java» в данном случае означает «без применения AspectJ» . 4 По материалам http://www .theserverside .com/tt/articles/article .tss?l=IntrotoSpring25 . 192 АОП-инфраструктуры на «чистом» Java 193 листинг 11 .4 . Конфигурационный файл Spring 2.X p:driverClassName="com.mysql.jdbc.Driver" p:url="jdbc:mysql://localhost:3306/mydb" p:username="me"/> Каждый компонент напоминает одну из частей русской «матрешки»: объект предметной области Bank «упаковывается» в объект доступа к данным DAO (Data Accessor Object), который, в свою очередь, упаковывается в объект источника данных JDBC (рис . 11 .3) . Рис . 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 . 193 |