Blog

Задача: есть слоистая структура, в каждом слое по определенному пути может находится 0, 1 или много элементов. Надо все эти элементы сгруппировать по определенному правилу и в каждой группе слить в один и вернуть список того, что получилось.

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

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

Вот как это выглядит с применением функциональных элементов:

Пример
/**
 * Merge node lists grouping nodes by provided classifier and then merging each group independently
 *
 * @param nodeName   the name of node
 * @param classifier grouping function
 * @param collector  used to each group
 * @param <A>        intermediate collector accumulator type
 * @param <K>        classifier key type
 * @return
 */
public <A, K> Collection<Meta> collectNodes(String nodeName, Function<? super Meta, ? extends K> classifier, Collector<Meta, A, Meta> collector) {
    return layers().stream()
            .filter(layer -> layer.hasMeta(nodeName))
            .flatMap(layer -> layer.getMetaList(nodeName).stream())
            .collect(Collectors.groupingBy(classifier, () -> new LinkedHashMap<>(), collector)).values();
    //linkedhashmap ensures ordering
}

Ну и несколько утилок для удобства:

Утилки
/**
 * Same as above, but uses fixed replace rule to merge meta
 *
 * @param nodeName
 * @param classifier
 * @param <K>
 * @return
 */
public <K> Collection<Meta> collectNodes(String nodeName, Function<? super Meta, ? extends K> classifier) {
    return collectNodes(nodeName, classifier, MergeRule.replace());
}

/**
 * Same as above but uses fixed meta value with given key as identity
 * @param nodeName
 * @param key
 * @return
 */
public Collection<Meta> collectNodes(String nodeName, String key) {
    return collectNodes(nodeName, meta -> getValue(key, Value.NULL));
}

Первый пост в публичном пространстве группы, попробую сделать его коротким (поскольку длинные писать не умею) и полезным. Речь пойдет о механизме dependency injection и переломе моего мозга. Сам шаблон ныне очень популярен особенно в энетерпрайзной среде, но я даже с довольно большим стажем программирования очень не с первого раза въехал в то, что это такое. После прочтения довольно обширной статьи в википедии на ум приходит только:

Я понял - это намёк,
Я всё ловлю на лету
Но непонятно
Что конкретно ты имела в виду

Группа "Несчастный случай"

В смысле, все вроде правильно, но где тут вся суть-то?

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

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

Теперь подумаем, а каким образом наш транспортный код будет генерировать эти вспомогательные объекты.

  1. Навсегда зашитым в коде способом. Если вдруг надо что-то поменять, лезем в код и перекомпиллируем. Если вдруг есть два различных кода для трэккинга, путаемся. Если вдруг вызов кода треккинга происходит не в одном месте, а в двух, трех, пяти и так далее, попадаем в ад той же степени (поменяли ссылку в одном месте и не поменяли в другом счастливо отлавливаем ошибку в течение пары месяцев). В программе больше, чем на пару сотен строк лучше так не делать.
  2. Интерфейсом. Создаем интерфейс, описывающий набор поведений нужного нам класса. Реализуем этот интерфейс одним или несколькими способами (да, часто имеет смысл делать интерфейс с ровно одной реализацией). Далее мы можем получить объект, реализующий этот интерфейс разными способами:
    1. Генерировать его на ходу, используя конфигурацию материнского класса. Это весьма распространенный способ, но главная проблема в том, что при этом надо поддерживать весьма развесистую конфигурационную структуру, в которой легко запутаться. Если вдруг надо добавить для дочернего класса какую-нибудь дополнительную степень свободы, то ее надо будет протаскивать через всю структуру, что довольно затруднительно, если структура сложная.
    2. Определять нужные объекты в конструкторе материнского класса. То есть сначала создать треккер и генератор случайных чисел, а потом из них собрать транспорт. Проблема тут в том, что если таких элементов нужно 10, то конструктор превращается в сущего монстра. В таких случаях довольно успешно используются всяческого рода builder-ы.
    3. Сделать внешний сервис, который будет по запросу нашего материнского класса (транспорта) генерировать нужные элементы (треккер и так далее). При изменении конфигурации мы можем модифицировать только этот сервис, и ничего более.

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

А теперь к тому, с чего все это началось. Я сижу, изучаю тут код одних товарищей, которые очень любят этот самый dependency injection, и используют его вообще везде. При этом эксплуатируется очень ныне популярная библиотека Guice. Я сперва, читая их код, вообще ничего не понял, поскольку в коде вообще нет никаких запросов к сервису, управляющему зависимостей. Оказывается, guice занимается тем, что при создании объекта, проводит поиск всех полей этого объекта, помеченных специальной аннотацией (в Java и всяческих наследниках, аннотации кода - это своеобразная метапрограмма). После чего, система ищет в себе генератор для объектов того типа, который нужен клиенту и подставляет его. В процессе писания этого поста, я наконец понял, как оно работает и после этого, все кажется довольно изящным, но перед этим мозг был сломан.


Вообще, интерес к dependency injection у меня возник до этого. Дело в том, что при разработке контекстов для DataForge, я сам того не зная, эту концепцию переизобрел. Одна из функций контекста - это как раз "внедрение зависимостей", он тащит в себе набор некоторых сервисов, которые могут быть вызваны любыми клиентами, знающими об этом контексте. Конечно, там еще много всяческих прелестей вроде наследования и динамического переключения, но в смысле архитектуры они вторичны.