英文:
Ideas to simplify generic usage in Spring Boot Java architecture
问题
I have a Java standard Spring Boot service which receives some items from an external service, saves them in a database and then offers some operations to retrieve and aggregate them. The problem I'm having is that I'm trying to use generics as much as possible but my intermediate classes are getting too verbose with tens of generic parameter types and I'd like to know if someone has a better idea how to approach it.
Let's say that the application gets some types of items and each one of them can have subitems and changes. This is received from the external service, so I have the DTO types ItemDto, SubitemDto, and ChangeDto:
public class ItemDto<S extends SubitemDto, C extends ChangeDto> {
private List<S> subitems;
private List<C> changes;
}
And their corresponding subclasses:
public class Item1Dto<SubItem1Dto, Change1Dto> {}
public class Item2Dto<SubItem2Dto, Change2Dto> {}
I receive a JSON file from the external service with a list of these items and I parse them with Jackson successfully. Now I want to save them in the database using JPA, so I've created their JPA entity counterparts:
@MappedSuperclass
public class Item {
@Id
private int id;
}
@MappedSuperclass
public class SubItem<I extends Item> {
@Id
private int id;
@ManyToOne
private I item;
}
@MappedSuperclass
public class Change<I extends Item> {
@Id
private int id;
@ManyToOne
private I item;
}
For performance reasons, I don't add the @OneToMany relationship part to the Item entities since I will be filtering and retrieving them in their own repository. Now, I have the concrete implementation ones, each one in their table:
@Entity("item1")
public class Item1 extends Item {
}
@Entity("subitem1")
public class SubItem1 extends SubItem<Item1> {
}
@Entity("change1")
public class Change1 extends Change<Item1> {
}
Then I have created the Spring Data repositories. Since some operations (but not all) are common to the different types, I have created a generic repository:
@NoBeanRepository
public class ItemRepository<I extends Item> extends CrudRepository<Integer, I> {
}
@NoBeanRepository
public class SubItemRepository<I extends Item, S extends SubItem<I>> extends CrudRepository<Integer, S> {
}
@NoBeanRepository
public class ChangeRepository<I extends Item, C extends Change<I>> extends CrudRepository<Integer, C> {
}
And of course, the concrete ones with the operations that are specific to each subtype:
@NoBeanRepository
public class Item1 extends ItemRepository<Item1> {
}
@NoBeanRepository
public class SubItem1 extends SubItemRepository<Item1, SubItem1> {
}
@NoBeanRepository
public class Change1Repository extends ChangeRepository<Item1, Change1> {
}
In here you can start to see the problem. Although the subclasses of the repository are quite concise, the generic implementations start to get very verbose.
I receive DTOs that I have to transform to their entity counterparts, so I have:
public class ItemMapper<I extends ItemDto, E extends Item> {}
public class SubItemMapper<S extends SubItemDto, E extends SubItem<I>, I extends Item> {}
...
And now we go to the service layer. Here, we get the DTOs, transform them to their entity counterparts before saving them. To make it shorter, I will go with the worst case, which is the subitems:
public class SubItemService<S extends SubItemDto, E extends SubItem<I>, I extends Item, M extends SubItemMapper<S, E>, R extends SubItemRepository<I, E>> {
private M mapper;
private R repository;
public E saveSubItems(I item, List<S> subItems) {
return repository.saveAll(mapper.mapAll(subItems, item));
}
}
Here you can see the problem: The service logic is trivial, we save lots of code because each concrete implementation doesn't have to implement the methods that are common (like saving a list of objects) but the generics part becomes very complex.
... (The rest of the content is omitted to keep the response concise) ...
英文:
I have a Java standard Spring Boot service which receives some items from an external service, saves them in a database and then offers some operations to retrieve and aggregate them. The problem I'm having is that I'm trying to use generics as much as possible but my intermediate classes are getting too verbose with tens of generic parameter types and I'd like to know if someone has a better idea how to affront it.
Let's say that the application gets some types of items and each one of them can have subitems and changes. This are received from the external service so I have the DTO types ItemDto, SubitemDto and ChangeDto:
public class ItemDto<S extends SubitemDto, C extends ChangeDto> {
private List<S> subitems;
private List<C> changes;
}
And their corresponding subclasses:
public class Item1Dto<SubItem1Dto, Change1Dto> {}
public class Item2Dto<SubItem2Dto, Change2Dto> {}
I receive a json file from the external service with a list of this items and I parse them with Jackson successfully. Now I want to save them in the database and I'm using JPA so I've created their JPA entity counterparts:
@MappedSuperclass
public class Item {
@Id
private int id;
}
@MappedSuperclass
public class SubItem<I extends Item> {
@Id
private int id;
@ManyTo
private I item;
}
@MappedSuperclass
public class Change<I extends Item> {
@Id
private int id;
@ManyTo
private I item;
}
For performance reasons I don't add the @OneToMany relationship part to the Item entities since I will be filtering and retrieving them in their own repository. Now, I have the concrete implementation ones, each one in their table:
@Entity("item1)
public class Item1 extends Item {
}
@Entity("subitem1")
public class SubItem1 extends SubItem<Item1>{
}
@Entity("change1")
public class Change1 extends Change<Item1> {
}
Then I have created the Spring Data repositories. Since some operations (but not all) are common to the different types, I have created a generic repository:
@NoBeanRepository
public class ItemRepository<I extends Item> extends CrudRepository<Integer, I> {
}
@NoBeanRepository
public class SubItemRepository<I extends Item, S extends SubItem<I>> extends CrudRepository<Integer, S> {
}
@NoBeanRepository
public class ChangeRepository<I extends Item, C extends Change<I>> extends CrudRepository<Integer, C> {
}
And of course the concrete ones with the operations which are specific to each subtype:
@NoBeanRepository
public class Item1 extends ItemRepository<Item1> {
}
@NoBeanRepository
public class SubItem1 extends SubItemRepository<SubItem1> {
}
@NoBeanRepository
public class Change1Repository extends ChangeRepository<Change1> {
}
In here you can start to see the problem. Although the subclasses of the repository are quite concise, the generic implementations start to get very verbose but it gets worse.
I receive Dtos that I have to transform to their entity counterparts, so I have:
public class ItemMapper<I extends ItemDto, E extends Item> {}
public class SubItemMapper<S extends SubItemDto, E extends SubItem<I>, I extends Item> {}
...
And now we go to the service layer. In here, we get the Dtos, transform them to their entity counterpart before saving it. To make it shorter I will go with the worse case which is the subitems:
public class SubItemService<S extends SubItemDto, E extends SubItem<I>, I extends Item, M extends SubItemMapper<S, E>, R extends SubItemRepository<I, E> {
private M mapper;
private R repository;
public E saveSubItems(I item, List<S> subItems) {
return repository.saveAll(mapper.mapAll(subItems, item));
}
}
Here you can see the problem: The service logic is trivial, we save lots of code because each concrete implementation don't have to implement the methods which are common (like saving a list of objects) but the generics part becomes very complex. Like a puzzle that you have to pay attention with now having 5 generic types. And it can get worse. Since Item, SubItem and Change are separated by table but closely related, I'd have an ItemFacade with the ItemService, SubItemService and ChangeService which becomes a nightmare:
public class ItemFacade<I extends Item, S extends SubItem<I>, C extends Change<I>, D extends ItemDto, S1 extends SubItemDto, C1 extends ChangeDto, IM extends Mapper<D, I>, SM extends Mapper<S1, S>, CM extends Mapper<C1, C>, IR extends ItemRepository<I>, SR extends SubItemRepository<S, I>, CR extends ChangeRepository<C, I>, IS extends ItemService<D, I, IM, IR>, SS extends SubItemService<S1, S, SM, SR>, CS extends ChangeService<C1, C, CM, CR> {
private IS itemService;
private SS subItemService;
private CS changeService;
public List<I> addItems(List<D> items) {
for (D itemDto : items) {
I item = itemService.save(itemDto);
List<S> subItems = subItemService.saveAll(item, itemDto.getSubItems());
List<C> changes = changeService.saveAll(item, itemDto.getChanges());
}
........
}
}
And although the code is highly reusable, since the logic will be shared by all the Item subtypes and very simple due to the separation of concerns between the services, mappers and repositories, the generics part becomes a nightmare for two reasons:
-
It needs 15!!!! parameters. It's impossible to read and prone to errors. The subclasses are simpler but still they are 15 concrete parameters.
-
It exposes things which are not necessary to be known to the facade like mappers and repositories. They are internal to the services and they are not used at all in the facade business logic but yet they have to be provided as types in the generic part.
I'd like to know if there's a way to simplify this so the Facade, instead of having 15 generic parameters can be implemented with only 3 (the service types) and at the same time, conserve the type safety, since I can ignore the generics part of the services but the compiler will warn about the unchecked types and I want it to be as type safe as possible.
Thank you very much and sorry for the long exposition.
专注分享java语言的经验与见解,让所有开发者获益!
评论