Once in a distant, distant bank ...
Good day, Habr. Today, finally again got his hands to write here. But unlike previous tutorials - articles today I would like to share my experience and show the power of such a mechanism as generics, which, together with the magic of the spring, becomes even stronger. I just want to warn you that in order to understand the article, you need to know the basics of spring and have an idea about generics more than just “generics is, well, what we specify in ArrayList in quotes”.
Episode 1:
To begin with, at work, I had a task in about the following way: there were a large number of remittances with a certain number of common fields. In addition, each of the translations was associated with classes — requests for transferring from one state to another and redirection to another. Accordingly, there were builders who were engaged in conversion.
The problem with common fields, I decided simply - inheritance. So I got classes:
')
public class Transfer { private TransferType transferType; ... } public enum TransferType { INTERNAL, SWIFT, ...; } public class InternalTransfer extends Transfer { ... } public class BaseRequest { ... } public class InternalRequest extends BaseRequest { ... } ...
Episode 2:
Then there was a problem with the controllers - they all had to have the same methods - checkTransfer, approveTransfer, and so on. This is where the first, but not the last time, generic drugs came in handy: I ​​made a common controller with the necessary methods, and inherited the rest from it:
@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); } }
Well, actually the service:
public interface TransferService<T extends Transfer> { CheckResponse checkTransfer(T payment); ApproveResponse approveTransfer(T payment); ... }
Thus, the copy-paste problem was reduced only to the call of the superconstructor, and in the service we lost it altogether.
But!
Episode 3:
There was still a problem inside the service:
Depending on the type of transfer, different builders had to be called:
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(); } }
generalized builder interface:
public interface RequestBuilder<T extends BaseRequest, U extends Transfer> { T createRequest(U transfer); }
For optimization, the factory method came up here, as a result, switch / case turns out to be in a separate class. It seems to be better, but the problem remains the same - when adding a new translation, you will have to modify the code, and the cumbersome switch / case did not suit me.
Episode 4:
What was the way out? At first, it occurred to me to determine the type of translations by class name and call the desired builder with the help of reflection, which would force developers who would work with the project to meet certain requirements for naming their classes. But there was a better solution. Having wondered about it, one can come to the conclusion that the main aspect of the business logic of the application is the translations themselves. T e if there is no them, there will be no other. So why not get rid of it all? It is enough just to modify our classes. And again generics come to the rescue.
Request classes:
public class BaseRequest<T extends Transfer> { ... } public class InternalRequest extends BaseRequest<InternalTransfer> { ... }
And the builder interface:
public interface RequestBuilder<T extends Transfer> { BaseRequest<T> createRequest(T transfer); }
And here it becomes more interesting. We are faced with a feature of generics that is almost never mentioned and is used mainly in frameworks and libraries. After all, as a BaseRequest, we can substitute its successor, which corresponds to type T, that is:
public class InternalRequestBuilder implements RequestBuilder<InternalTransfer> { @Override public InternalRequest createRequest(InternalTransfer transfer) { return InternalRequest.builder() ... .build(); } }
At the moment we have achieved a good improvement in our application architecture. But the problem of switch / case has not yet solved this. Or …?
Episode 5:
This is where the magic of spring comes into play.
The fact is that we have the ability to get an array of names of bins of the appropriate type using the
getBeanNamesForType (ResolvableType type) method. And in the ResolvableType class there is a static method
forClassWithGenerics (Class <?> Clazz, Class <?> ... generics) where you need to pass a class (interface) as the first parameter that uses the second parameter as a generic and returns the appropriate type. T e:
ResolvableType type = ResolvableType.forClassWithGenerics(RequestBuilder.class, transfer.getClass());
Returns the following:
RequestBuilder<InternalTransfer>
And now a little more magic - the fact is that if you add a sheet, with the interface as a generic, then it will contain all its implementations:
private final List<RequestBuilder<T>> builders;
We just have to go through it and find the appropriate one using the instance check:
builders.stream() .filter(b -> type.isInstance(b)) .findFirst() .get();
Similarly to this variant, it is still possible to add ApplicationContext or BeanFactory, and call their getBeanNamesForType () method where to pass our type as a parameter. But this is considered a sign of bad taste and is not necessary for this architecture (special thanks to
zolt85 for the comment).
As a result, our factory method takes the following form:
@Component @AllArgsConstructor public class RequestBuildersFactory<T extends Transfer> { 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); } }
Episode 6: Conclusion
Thus, we have a mini - framework with a well thought out architecture that obliges all developers to stick to it. And what is important, we got rid of the cumbersome switch / case and the addition of new translations will not affect the existing classes in any way, which is good news.
PS:This article does not call for the use of generics wherever possible and impossible, but with its help I would like to share what powerful mechanisms and architectures they allow you to create.
Thanks:Special thanks to
Sultansoy , without which this architecture would not be brought to mind and, most likely, would not have this article.
References:Github source code