Since the conversion between Form class that receives the input value and Entity is difficult, I thought about four solutions.
I think the solutions that could be used are 2 and 4. This time, we solved it in 4 based on the culture of the company.
I was converting with Entity for persistence in Form class as follows.
PurchaseForm.java
@Data
public class PurchaseForm {
@NotNull
private Integer commodityId;
@NotNull
private Integer count;
/**
*Generate Entity based on input value
*/
public Purchase toEntity() {
//abridgement
}
}
If it is a simple conversion, there is no problem at all, but if it is the following pattern, it will die.
As the amount of code in the program for converting to Entity increases, the visibility of the source code becomes worse. Especially when trying to generate a child class of the class to be generated, it is quite painful. In the projects I'm involved in, I often rely on AUTO INCREMENT for the primary key numbering, so it is inevitably necessary to supplement the primary key and foreign key with the Service class. One concern is divided into multiple classes, such as initializing a part with Form and initializing the rest with Service class, and the source code becomes difficult to follow.
PurchaseForm.java
@Data
public class PurchaseForm {
//abridgement
/**
*Generate Entity based on input value
*/
public Purchase toEntity() {
//This
//This
//But
//Me
//Tsu
//Chi
//Ya
//Long
//I
//When
//Conclusion
//Structure
//Tsu
//Et al.
//I
}
}
If you need a persistent object as an argument so far, you shouldn't convert it in the Form class anymore.
PurchaseForm.java
@Data
public class PurchaseForm {
//abridgement
/**
*Generate Entity based on input value
*If you take a lot of persisted objects as arguments, you wonder if you need to create an Entity here.
*/
public Purchase toEntity(Hoge hoge, Fuga Futa, ... etc) {
//abridgement
}
}
I tried this but it's painful. Because as the input value of Form increases, the number of arguments increases.
PurchaseController.java
@Controller
@RequireArgsConstructor
public class PurchaseController {
public final PurchaseService purchaseService;
public ModelAndView create(@ModelAttribute @Valid PurchaseForm form, BindingResult result) {
// check
// Ah...!
purchaseService.purchase(form.input1, form.input2, form.input3, form.input4, form.input5,...);
}
}
PurchaseController.java
@Controller
@RequireArgsConstructor
public class PurchaseController {
public final PurchaseService purchaseService;
public ModelAndView create(@ModelAttribute @Valid PurchaseForm form, BindingResult result) {
// check
purchaseService.purchase(new Hoge(form.input1, form.input2), new Fuga(form.input3, form.input4, form.input5),...);
}
}
The advantage of this is that you can precondition the argument object to ensure value integrity. Preconditions are checked in the constructor.
It's very simple, but it's a really bad pattern! It is difficult for the dependency direction to be Form ← Service. Since Form is tightly coupled with screen specifications, we want to avoid that the business logic side depends on Form. It is strange that the Service class must be changed because the screen specifications have changed even though the business logic has not changed.
PurchaseController.java
@Controller
@RequireArgsConstructor
public class PurchaseController {
public final PurchaseService purchaseService;
public ModelAndView create(@ModelAttribute @Valid PurchaseForm form, BindingResult result) {
// check
// Fuck...!
purchaseService.purchase(form);
}
}
This was solved with the fourth plan.
First, I defined Service and the interface of the argument in a package close to the domain.
PurchaseService.java
@Service
public class PurchaseService {
public void purchase(PurchaseRequest request) {}
}
PurchaseRequest.java
public interface PurchaseRequest {
String getInput1();
Integer getInput2();
// etc...
}
Then I wrote the value validation in another implementation class.
PurchaseForm.java
public class PurchaseForm implements PurchaseRequest {
@NotEmpty
private String input1;
@NotNull
private Integer input2;
public String getInput1() {
return input1;
}
public Integer getInput2() {
return input2;
}
}
The nice point of this implementation method is that it declares the value used in the Service class at the layer close to the domain, so it is resistant to changes on the screen side. Of course, if Input and Output change as a business concept, it needs to be reviewed, but some differences can be controlled on the screen side.
Better yet, I think it's good to bind the input value from the screen to the type defined on the domain side. I feel that it is often talked about as the optimal solution. The above is the solution that we did in our project without the culture of implementing validation on domain side entities and implementing by capturing and implementing value types semantically.
After that, I decided to create an API to calculate the purchase price based on the information on the purchased products and the number of items. If you define a method called PurchaseService # calculatePrice (CalculatePriceRequest)
and let PurchaseRequest
inheritCalculatePriceRequest
, you can easily calculate the purchase price even with an argument of type PurchaseRequest
. That's right, since the product purchase request has information on the product and the number of items, it is quite natural that the purchase price can be calculated based on these information.
I'm doing a lot of trial and error, but I feel like I should be able to increase the number of implementation methods by analyzing the patterns that worked and the patterns that didn't work. In particular, I often develop WEB applications, so I would like to think about various IOs with the screen.
Recommended Posts