Однажды в одном далёком, далёком банке ...
Доброго времени суток, хабр. Сегодня наконец-то вновь дошли руки написать сюда. Но в отличие от предыдущих туториалов — статей сегодня хотелось бы поделиться своим опытом и показать мощь такого механизма как дженерики, который вместе с магией спринга становится ещё сильнее. Сразу хочу предупредить, что для понимания статьи нужно знать основы спринга и иметь представления о дженериках большие чем просто “Дженерики это, ну, то что в ArrayList в ковычках указываем”.
Эпизод 1:
Начнём с того, что на работе у меня стояла задача примерно таким образом: имелось большое количество денежных переводов с определенным количеством общих полей. Помимо этого каждому из переводов соответствовали классы — запросы для перевода из одного состояния в другое и перенаправления на другое апи. Соответственно были билдеры, которые и занимались преобразованием.
Проблему с общими полями я решил просто — наследованием. Таким образом у меня появились классы:
public class Transfer { private TransferType transferType; ... } public enum TransferType { INTERNAL, SWIFT, ...; } public class InternalTransfer extends Transfer { ... } public class BaseRequest { ... } public class InternalRequest extends BaseRequest { ... } ...
Эпизод 2:
Дальше стояла проблема с контроллерами — у них у всех должны были быть одинаковые методы — checkTransfer, approveTransfer и тд. Вот тут то в первый, но не в последний раз мне пригодились дженерики: я сделал общий контроллер с нужными методами, и унаследовал от него остальные:
@AllArgsConstructor public class TransferController<T extends Transfer> { private final TransferService<T> service; public CheckResponse checkTransfer(@RequestBody @Valid T payment) { return service.checkTransfer(payment); } ... } public class InternalTransferController extends TransferController<InternalTransfer> { public InternalTransferController(TransferService<InternalTransfer> service) { super(service); } }
Ну и собственно сервис:
public interface TransferService<T extends Transfer> { CheckResponse checkTransfer(T payment); ApproveResponse approveTransfer(T payment); ... }
Таким образом проблема копипаста сводилась только к вызову суперконструктора, а в сервисе мы вообще её лишились.
Но!
Эпизод 3:
Внутри сервиса всё ещё стояла проблема:
В зависимости от типа перевода нужно было вызывать различные билдеры:
RequestBuilder builder; switch (type) { case INTERNAL: { builder = beanFactory.getBean(InternalRequestBuilder.class); break; } case SWIFT: { builder = beanFactory.getBean(SwiftRequestBuilder.class); break; } default: { log.info("Unknown payment type"); throw new UnknownPaymentTypeException(); } }
обобщенный интерфейс билдера:
public interface RequestBuilder<T extends BaseRequest, U extends Transfer> { T createRequest(U transfer); }
Для оптимизации тут подошёл фабричный метод, в итоге switch/case — ы оказываются в отдельном классе. Вроде стало получше, но проблема осталась прежней — при добавлении нового перевода придётся модифицировать код, да и громоздкий switch/case меня не устраивал.
Эпизод 4:
Каков был выход? Вначале мне пришло на ум определять тип переводов по имени класса и вызывать нужный билдер с помощью рефлексии, что заставило бы разработчиков, которые будут работать с проектом соответствовать определенным требованиям по наименования своих классов. Но было лучшее решение. Пораскинув мозгами можно придти к тому, что основной аспект бизнес — логики приложения — это сами переводы. Т е если не будет их, не будет и всего остального. Так почему бы не завязать всё на этом? Достаточно лишь немного модифицировать наши классы. И снова на помощь приходят дженерики.
Классы запросов:
public class BaseRequest<T extends Transfer> { ... } public class InternalRequest extends BaseRequest<InternalTransfer> { ... }
И интерфейс билдера:
public interface RequestBuilder<T extends Transfer> { BaseRequest<T> createRequest(T transfer); }
А вот тут становится всё интереснее. Мы сталкиваемся с особенностью дженериков которая практически нигде не упоминается и используется в основном во фреймворках и библиотеках. Ведь в качестве BaseRequest мы можем подставить его наследника, который соответствует типу T, т е:
public class InternalRequestBuilder implements RequestBuilder<InternalTransfer> { @Override public InternalRequest createRequest(InternalTransfer transfer) { return InternalRequest.builder() ... .build(); } }
В данный момент мы добились неплохого улучшения нашей архитектуры приложения. Но проблему switch/case — ов это пока так и не решило. Или …?
Эпизод 5:
Вот тут то в дело и вступает магия спринга.
Дело в том, что в у нас есть возможность получить массив имен бинов соответствующих нужному типу с помощью метода getBeanNamesForType(ResolvableType type). И в классе ResolvableType имеется статический метод forClassWithGenerics(Class<?> clazz, Class<?>… generics), куда в качестве первого параметра нужно передать класс(интерфейс) который в качестве дженерика использует второй параметр и возвращает соответствующий тип. Т е:
ResolvableType type = ResolvableType.forClassWithGenerics(RequestBuilder.class, transfer.getClass());
Возвращает следующее:
RequestBuilder<InternalTransfer>
А теперь ещё немного магии — дело в том, что если заавтовайрить лист, с интерфейсом в качестве дженерика, то в нём будут содержаться все его реализации:
private final List<RequestBuilder<T>> builders;
Нам остаётся только пройтись по нему и найти соответствующий с помощью проверки на инстанс:
builders.stream() .filter(b -> type.isInstance(b)) .findFirst() .get();
Аналогично этому варианту ещё есть возможность заавтовайрить ApplicationContext либо BeanFactory, и вызвать у них метод getBeanNamesForType() куда передать наш тип в качестве параметра. Но это считается признаком дурного тона и в данной архитектуре в этом нет необходимости (отдельное спасибо zolt85 за комментарий).
В итоге наш фабричный метод приобретает следующий вид:
@Component @AllArgsConstructor public class RequestBuildersFactory<T extends Transfer> { private final List<RequestBuilder<T>> builders; public BaseRequest<T> transferToRequest(T transfer) { ResolvableType type = ResolvableType.forClassWithGenerics(RequestBuilder.class, transfer.getClass()); RequestBuilder<T> builder = builders.stream() .filter(b -> type.isInstance(b)) .findFirst() .get(); return builder.createRequest(transfer, stage); } }
Эпизод 6: Заключение
Таким образом у нас получился мини — фреймворк с продуманной архитектурой, обязующей всех разработчиков ей придерживаться. И что немаловажно мы избавились от громоздкого switch/case и добавление новых переводов никак не затронет уже существующие классы, что не может не радовать.
PS:
Данная статья не призывает использовать дженерики где только можно и нельзя, но с её помощью хочется поделиться тем, какие мощные механизмы и архитектуры они позволяют создавать.
Благодарности:
Отдельное спасибо Sultansoy, без которого данная архитектура не была бы доведена до ума и, скорее всего, не было бы этой статьи.
Ссылки:
Исходный код на github
