вторник, 21 января 2014 г.

А давайте перепишем DAO с использованием возможностей Java 8

В последние годы складывается следующая тенденция развития объектно-ориентированных языков программирования: к ним добавляют все больше и больше элементов, взятых из мира функционального программирования. C# обзавелся поддержкой анонимных функций несколько лет назад, настала пора и Java-разработчикам приобщиться к прекрасному. Поддержка анонимных функций и основанный на их широком использовании новый API Collection Framework'а ждет нас в Java 8. Интернет уже запестрил статьями вида "Как написать свой Hello World на Java 8". Но писать Hello World не интересно, давайте лучше рассмотрим, как можно использовать новые возможности для решения конкретных задач в нашем суровом "Ынтырпрайз" мире.


В предыдущей заметке я описывал подход к построению слоя доступа к данным с использованием JDBC. В частности было описано решение задачи использования нестандартного API информационной системы, основанного на вызове хранимых процедур, принимающих в качестве параметров несколько коллекций, по которым разложены поля объектов. Внутри процедуры объект набирается из полей данных коллекций, имеющих одинаковый индекс. Таким образом перед вызовом процедуры необходимо разложить коллекцию объектов на массив (или список) коллекций, состоящих из значений параметров данных объектов. Для описания данного разложения нами была составлена структура из классов ArgArray и ArgArrayList, которая строится следующим образом:


new ArgArrayList().add(ArgArray.fill(Type.T_STRING_LIST,
            param, 
            new Mapper<T, String>() {
                @Override
                public String map(T bean) {
                    return bean.getLocalSystemId();
                }
            })).add(ArgArray.fill(Type.T_STRING_LIST, param,
                new Mapper<T, String>() {
                    @Override
                    public String map(T bean) {
                        return bean.getTitle();
                    }
            })).add(ArgArray.fill(Type.T_NUMBER_LIST, param,
                new Mapper<T, String>() {
                    @Override
                    public String map(T bean) throws MapperException {
                        try {
                            return bean.getApp() == null ? null :
                                branches.getCode(bean.getApp().getPkId());
                        }
                        catch (MapNotFoundException e) {
                            throw new MapperException(e.getMessage(), e);
                        }
                    }
            }));

У данного кода есть недостаток. Мы должны передать в хранимую процедуру коллекции полей, извлеченных из одной и той же коллекции объектов. В текущей реализации нам никто не гарантирует, что при очередном вызове метода add класса ArgArrayList не будет передан ArgArray, заполненный на основе элементов какой-нибудь другой коллекции. Более того, никто не гарантирует, что не будет передан экземпляр наследника данного класса, который поломает логику предоставления элементов.

Выполним рефакторинг классов ArgArray и ArgArrayList. Во-первых, сделаем так, чтобы метод add класса ArgArrayList принимал не экземпляр класса ArgArray, а отдельно тип, коллекцию элементов и преобразователь:


public class ArgArrayList {
    ...
    public <T> ArgArrayList add(Type type, Collection<R> collection, 
        Mapper<R, T> mapper) {
        argArraysList.add(ArgArray.fill(type, collection, mapper));
        return this;
    }
}

В результате данного рефакторинга мы скрыли внутреннее устройство класса ArgArrayList от использующего его кода. Код, строящий экземпляры класса ArgArrayList, ничего не знает о классе ArgArray.

Во-вторых, разделим класс ArgArrayList на две составляющие. Одна как и прежде будет отвечать за предоставление списка массивов ArgArray коду, выполняющему хранимые процедуры СУБД Oracle, а другая составляющая будет отвечать за построение экземпляров класса ArgArrayList. Применим для построения объектов паттерн builder. Для чего мы делаем такое разделение? Нам нужно инкапсулировать коллекцию объектов, которую мы разкладываем на составные части, внутри класса ArgArrayList. Данный класс для этого нужно параметризовать. Но! Во всех точках использования данного класса - в DAO - абсолютно не важно по коллекции какого типа объектов он был построен. Есть список массивов примитивных типов и все. Поэтому, чтобы не нарушать принцип SRP, мы вынесем код построения экземпляра класса ArgArrayList в отдельный класс ArgArrayListBuilder.


public class ArgArrayList {
 
 private List<ArgArray<?>> args;
 
 private ArgArrayList(List<ArgArray<?>> args) {
  this.args = args;
 }
 
 public List<ArgArray<?>> arrays() {
  return Collections.unmodifiableList(args);
 } 
 
 public static class ArgArrayListBuilder<R> {
  
  private Collection<R> objects;
  
  private List<ArgArray<?>> args;
  
  private ArgArrayListBuilder(Collection<R> objects) {
   this.objects = objects;
   this.args = new LinkedList<>();
  }
  
  public static <R> ArgArrayListBuilder<R> with(Collection<R> objects) {
   return new ArgArrayListBuilder<R>(objects);
  }
 
  public <T> ArgArrayListBuilder<R> add(Type type, Mapper<R, T> mapper) 
    throws MapperException {
   args.add(ArgArray.fill(type, objects, mapper));
   return this;
  }
  
  public ArgArrayList build() {
   return new ArgArrayList(args);
  }
 }
}

Теперь опасность передать разные коллекции для построения списка массивов параметров миновала. В классе ArgArrayBuilder сначала с помощью метода with мы замыкаем коллекцию объектов, а затем с помощью вызовов метода add накапливаем преобразователи элементов данной коллекции. Код построения экземпляра класса ArgArrayList будет выглядеть следующим образом:


ArgArrayList argarray = ArgArrayListBuilder.with(param)
    .add(Type.T_STRING_LIST,
            new Mapper<T, String>() {
                @Override
                public String map(T bean) {
                    return bean.getLocalSystemId();
                }
            })
    .add(Type.T_STRING_LIST,
            new Mapper<T, String>() {
                @Override
                public String map(T bean) {
                    return bean.getTitle();
                }
            })
    .add(Type.T_NUMBER_LIST,
            new Mapper<T, String>() {
                @Override
                public String map(T bean) throws MapperException {
                    try {
                        return bean.getApp() == null ? null :
                           branches.getCode(bean.getApp().getPkId());
                    }
                    catch (MapNotFoundException e) {
                        throw new MapperException(e.getMessage(), e);
                    }
                }
            })
    .build();

Давайте еще раз посмотрим на данный код. Определение каждого преобразователя (new Mapper) содержит как минимум пять строк. Пять строк! Из которых полезной является только одна. КПД составляет 20%, это чуть больше чем у паровоза. Причем четыре из этих пяти строк всегда одинаковы: это определение анонимного класса, аннотация @Override и определение метода.

В Java 8 определено понятие функционального интерфейса - интерфейса, который может быть помечен аннотацией @FunctionalInterface. Функциональный интерфейс отличается тем, что содержит строго один абстрактный метод, причем данный метод не должен быть унаследован от класса Object. Например, используемый нами интерфейс ArgArray.Mapper попадает под определение функционального интерфейса.


public class ArgArray<T> {
    
    ...
    
    /**
     * Трансформатор для бинов типа <code>R</code> в элементы типа <code>T</code>
     * @param <T> тип результата трансформации
     * @param <R> тип трансформируемого бина
     */
    public static interface Mapper<R, T> {
        public T map(R bean) throws MapperException;
    }
    ...
}

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

Можно сделать так:


ArgArray.Mapper<Person, String> titler = (Person p) -> p.getTitle();

А затем передать получившееся выражение в качестве аргумента метода add:


ArgArrayList argarrays = ArgArrayListBuilder.with(param)
    .add(Type.T_STRING_LIST, titler)

В библиотеку классов Java 8 добавлен пакет java.util.function, в состав которого входят наиболее часто используемые функциональные интерфейсы, такие как Function - представление функции одной переменной, BiFunction - представление функции двух переменных, основанные на них операторы UnaryOperator и BinaryOperator, а так же комбинации функций над переменными примитивных типов, например интерфейс LongToDoubleFunction, содержащий метод applyAsDouble, принимающий на вход значение типа long и возвращающий double.

Можно убрать определение класса Mapper из ArgArray, а так же переписать метод fill в функциональном стиле с использованием появившегося в Java 8 Stream API:


public static <R, T> ArgArray<T> fill(Type type, Collection<R> collection,
        Function<R, T> mapper) throws MapperException {
    ArgArray<T> array = new ArgArray<T>(type);
    if (collection != null) {
        try {
     array.values = collection.stream().collect(
          Collectors.mapping((bean) -> mapper.apply(bean), Collectors.toList()));
 } catch (Exception e) {
     // перехватываем все проверяемые и непроверяемые исключения
     throw new MapperException(e.getMessage(), e);
 }
    }
    
    return array;
}

Интерфейс Mapper отличался от функционального интерфейса Function тем, что в определении его метода map декларировалось бросание проверяемого исключения. Однако метод fill определен как бросающий такое же исключение, поэтому можно делать это непосредственно в нем. Такой подход избавляет от необходимости использовать конструкции try/catch в преобразователях, что ведет к уменьшению их размеров, но накладывает ограничение на реализацию: в методах apply можно оперировать только непроверяемыми исключениями. В нашем случае пришлось поменять определение класса BranchesMap, используемого в преобразователях для приведения кодов систем. Исключения, бросаемые методами данного класса, пришлось переопределить как непроверяемые (RuntimeException).

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


ArgArrayList argarrays = ArgArrayListBuilder.with(param)
    .add(Type.T_STRING_LIST, (Person p) -> p.getLocalSystemId())
    .add(Type.T_STRING_LIST, (Person p) -> (p.getTitle() + p.getSername()))
    .add(Type.T_NUMBER_LIST, (Person p) -> p.getApp() == null ? null :
        branches.getCode(p.getApp().getPkId()))
    .build();
  
dao.call(OPERATION_NAME, argarrays);

Несложно заметить, что объем кода уменьшился примерно в четыре раза. При этом он перестал загромождаться кучей фигурных скобочек, определениями анонимных классов, методов, а так же блоками try/catch в преобразователях. При этом очень важно, что код классов, непосредственно взаимодействующих с СУБД Oracle, никак не изменился.

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

Понравилось сообщение - подпишитесь на блог

3 комментария:

goncharenko комментирует...

Так мы скоро и до adt4j дойдём :)

Pavel Samolisov комментирует...

Только Core Java, только хардкор.

Pavel Samolisov комментирует...

Кстати, посмотрел adt4j на github'е. С JSR 308 это никак не пересекается?

Отправить комментарий

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