Disclaimer
Инструмент родился как способ побороть недочеты проектирования малой кровью. Я с трудом могу представить ситуацию, где использование инструмента могло бы быть продиктовано иными причинами.
Предыстория
Случилось мне однажды подключиться к разработке немолодого web-приложения. Код был местами изрядно запутан, хранил следы деятельности нескольких разработчиков разной квалификации, актуальных работоспособных тестов не было. Одно слово — legacy.
Приложение было реализовано по классической трехслойной схеме:
- Persistence: Hibernate.
- Services: Spring.
- Endpoints: Spring MVC: JSP, RESTful.
И на всех уровнях использовались сущности уровня хранения (далее — сущности). Из-за этого изменение имен свойств сущностей влекло за собой необходимость править код на клиенте или в JSP, а добавление чего-нибудь в JSON который отдавали контроллеры или в JSP, если это что-то не лежало в сущности, было крайне неудобным и рискованным из за отсутствия тестов.
Как же протащить на уровень представления значения, которых нет в сущностях?
Я рассмотрел несколько способов.
Listener’ы и Interceptor’ы
Listener’ы и Interceptor’ы позволяют добавить в сущности дополнительные данные. В некоторых случаях их применение оправдано, но предпочитаю не засорять сущности уровня хранения структурами и данными, не имеющими к уровню хранения никакого отношения.
Mapping
Можно пронаследоваться от каждого класса, который требует расширения, и начинить его нужными данными уже на сервисном уровне. Это концептуально верный путь: слой хранения сохраняет чистоту, слой контроллеров не требует модификации. Однако, возникают проблемы с производительностью, ленивая загрузка перестает быть ленивой, т.к. маппер не знает, какие из полей нужны контроллеру, и вынужден перекладывать все. Управление маппером со стороны контроллера теоретически возможно, но это невозможно без модификации кода контроллера.
Dynamic proxy
Немножко магии:
package ru.bdm.reflection; //some imports omitted import static junit.framework.Assert.assertEquals; import static org.apache.commons.beanutils.PropertyUtils.getProperty; public class PropertyJoinerTest { public static class AnyType { public Object getAnyProperty() { return "anyPropertyValue"; } } @Test public void testWithPropertyExtractor() throws Exception { PropertyJoiner propertyJoiner = new PropertyJoiner(new PropertyExtractor() { @Override public Object get(Object o, String property) { return property + "Value"; } }, "first", "second"); AnyType src = new AnyType(); AnyType dst = propertyJoiner.joinProperties(src); assertEquals("firstValue", getProperty(dst, "first")); assertEquals("secondValue", getProperty(dst, "second")); assertEquals("anyPropertyValue", getProperty(dst, "anyProperty")); } }
Что под капотом?
Динамически создаются классы интерфейсов для добавочных свойств:
public interface FirstHolder{ Object getFirst(); } public interface SecondHolder{ Object getSecond(); }
Динамически создается класс proxy, который наследует AnyType
и реализует FirstHolder
и SecondHolder
.
Методы, определенные в AnyType
, proxy перенаправляет к src
, методы, определенные в FirstHolder
и SecondHolder
, перенаправляются в PropertyExtractor
, который содержит логику вычисления добавочных свойств.
Таким образом мы получили возможность расширения представления, не меняя при этом код контроллеров и не засоряя сущности уровня хранения посторонними структурами и данными, не получая падения производительности из-за проблем с ленивой загрузкой.
Плата за это оказалась не очень велика: доступ к свойствам через прокси примерно в 150 раз медленнее, чем непосредственный. Это стоит учитывать при использовании инструмента.
Нагрузка нашего приложения была всего несколько запросов в секунду, за каждый запрос читалось максимум 50 сущностей (размер страницы), так что долей потерь в proxy можно было пренебречь.
Скачать код можно с Google Drive.
ссылка на оригинал статьи http://habrahabr.ru/post/223971/
Добавить комментарий