Однажды в одном далёком, далёком банке ...


Доброго времени суток, хабр. Сегодня наконец-то вновь дошли руки написать сюда. Но в отличие от предыдущих туториалов — статей сегодня хотелось бы поделиться своим опытом и показать мощь такого механизма как дженерики, который вместе с магией спринга становится ещё сильнее. Сразу хочу предупредить, что для понимания статьи нужно знать основы спринга и иметь представления о дженериках большие чем просто “Дженерики это, ну, то что в 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(onConstructor = @__(@Autowired))
    public class TransferController<T extends Transfer> {
    
        private final TransferService service;
        
        public CheckResponse checkTransfer(@RequestBody @Valid T payment) {
            return service.checkTransfer(payment);
        }
        
        ...
    }


    public class InternalTransferController extends TransferController<InternalTransfer> {
    
        @Autowired
        public InternalTransferController(TransferService 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>

А теперь если передать его в getBeanNamesForType то нам вернётся массив только с одним именем — internalRequestBuilder, т к только данный бин соответствует нужному типу. Ну и собственно осталось только получить бин из апликейшн контекста по имени:

 RequestBuilder<T> builder = (RequestBuilder<T>) context.getBean(beanNamesForType[0]);

В итоге наш фабричный метод приобретает следующий вид:

    @SuppressWarnings("unchecked")
    @Component
    public class RequestBuildersFactoryImpl implements RequestBuildersFactory {
    
        @Setter(onMethod = @__(@Autowired))
        private ApplicationContext context;
    
        public <T extends Transfer> BaseRequest<T> transferToRequest(T transfer) {
    
            ResolvableType type = ResolvableType.forClassWithGenerics(RequestBuilder.class, transfer.getClass());
            String[] beanNamesForType = context.getBeanNamesForType(type);
            RequestBuilder<T> builder = (RequestBuilder<T>) context.getBean(beanNamesForType[0]);
    
            return builder.createRequest(transfer, stage);
    
        }
    
    }

Эпизод 6: Заключение


Таким образом у нас получился мини — фреймворк с продуманной архитектурой, обязующей всех разработчиков ей придерживаться. И что не мало важно мы избавились от громоздкого switch/case и добавление новых переводов никак не затронет уже существующие классы, что не может не радовать.

PS:
Данная статья не призывает использовать дженерики где только можно и нельзя, но с её помощью хочется поделиться тем, какие мощные механизмы и архитектуры они позволяют создавать.

Благодарности:
Отдельное спасибо Sultansoy, без которого данная архитектура не была бы доведена до ума и скорее всего не было бы этой статьи.

Комментарии (4)


  1. MisterParser
    18.09.2018 22:18

    Джошуа Блох советует в данном случае со switch использовать внутренний класс-стратегию внутри вашего enum, чтобы избавиться от switch.


    1. Rusya_2_0 Автор
      19.09.2018 11:06

      Не спорю, данный вариант тоже подходит, но суть архитектуры в том, что она завязана на дженериках и трансферах, а в случае, который советует Блох получается сильная привязка к enum — у, чего мне совсем не хотелось, т к данное поле изначально не являлось обязательным для бизнес логики.


  1. akurilov
    19.09.2018 10:40

    Проблема также решается без Spring с помощью Java Service Providers.
    https://docs.oracle.com/javase/tutorial/ext/basics/spi.html
    Можно динамически подгружать реализации некоторого интерфейса и его билдера и выбирать их по какой-нибудь метаинформации без switch (итерируя доступные в рантайме и выбирая нужную).


  1. zolt85
    19.09.2018 16:00

    Посмею сделать небольшое замечание, по поводу автовайринга ApplicationContext-а в фабрику. Это, как заверяют евангелисты, признак плохого тона. Не должен отдельный компонент знать про весь контекст. Думаю фабрику можно переписать иначе:

        @SuppressWarnings("unchecked")
        @Component
        public class RequestBuildersFactoryImpl implements RequestBuildersFactory {
        
            @Setter(onMethod = @__(@Autowired))
            private List<RequestBuilder> builders;
        
            public <T extends Transfer> BaseRequest<T> transferToRequest(T transfer) {
        
                ResolvableType type = ResolvableType.forClassWithGenerics(RequestBuilder.class, transfer.getClass());
    
                RequestBuilder<T> builder = builders.stream().filter(b -> type::isInstance).findFirst().get();
        
                return builder.createRequest(transfer, stage);
            }    
        }
    


    В части получения билдера из списка могут быть варианты, но основной посыл надеюсь понятен.