[JAVA] What I thought when passing the user input value to the Service class

What I thought when passing the user input value to the Service class

Conclusion

Since the conversion between Form class that receives the input value and Entity is difficult, I thought about four solutions.

  1. Make each input value an argument of Service method
  2. Make objects in meaningful units
  3. Make the Service method a Form class
  4. Use the Service argument as an interface

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.

environment

Assumption: Conversion between Form class that receives input value and Entity is difficult

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.

When the class to be converted has a complex class structure

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
  }
}

It is painful if there are many arguments required for Entity generation

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
  }
}

Possible solutions

1. Make each input value an argument of Service method

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,...);
  }
}

2. Make objects in meaningful units

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.

3. Make the Service method a Form class

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.

4. Use the Service argument as an interface

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.

Supplement

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.

Unexpected side effect

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.

Finally

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

What I thought when passing the user input value to the Service class
What I was addicted to when introducing the JNI library
What I thought about when I started migrating from Java to Kotlin
Memorandum: What I was addicted to when I hit the accounting freee API
Recorded because I was addicted to the standard input of the Scanner class
What I tried when I wanted to get all the fields of a bean
What to do when the value becomes null in the second getSubmittedValue () in JSF Validator
Pay attention to the boundary check of the input value when using the float type
What I did when I converted java to Kotlin
I want to get the value in Ruby
What to do when The SSL certificate has expired
What I thought when I started working as an engineer
What should I do to reload the updated Dockerfile?
What I fixed when updating to Spring Boot 1.5.12 ・ What I was addicted to
What I did in the version upgrade from Ruby 2.5.2 to 2.7.1
What I was addicted to with the Redmine REST API
I want to give a class name to the select attribute
I want to recursively search the class list under the package
I want to return multiple return values for the input argument
The story I was addicted to when setting up STS
I want to judge the necessity of testing by comparing the difference of class files when refactoring Java
What I was addicted to when updating the PHP version of the development environment (Docker) from 7.2.11 to 7.4.x
I want to make the frame of the text box red when there is an input error
When I tried to run my own service, it failed, so I screwed it into the task scheduler
9 Corresponds to the return value
What is the BufferedReader class?
Input to the Java console
[java] What I did when comparing Lists in my own class
I tried to summarize what was asked at the site-java edition-
What to do when the changes in the Servlet are not reflected
How to display 0 on the left side of the standard input value
How to output the value when there is an array in the array
What I did when the DB did not start with docker-compose up
I tried to translate the error message when executing Eclipse (Java)
What I did in the migration from Spring Boot 1.4 series to 2.0 series
Easy way to create a mapping class when using the API
What I was addicted to when implementing google authentication with rails
I want to limit the input by narrowing the range of numbers
What I did in the migration from Spring Boot 1.5 series to 2.0 series
I want to change the value of Attribute in Selenium of Ruby