This article is a reading note from “Listening to the Wind with the Left Ear: A Legendary Programmer’s Leveling Guide.”
The essence of programming includes Logic, Control, and Data. The programming paradigms and design methods we use mainly revolve around these three aspects.
Logic is the essence of the problem, which implements how to solve the problem.
Control is the strategy for solving the problem, such as loop execution, asynchronous execution, etc.
Data is the representation of the problem.
Business logic is often complex, which is part of the program’s complexity. When intertwined with program control, it constitutes the complete complexity of the program.
Therefore, effectively separating L, C, and D is key to writing good programs.
This involves a critical concept – decoupling: reducing the dependency coupling of modules and eliminating unnecessary coupling.
Taking the example of aggregated payment.
The aggregated payment system receives a payment request, saves the payment data, and then requests the external bank interface.
We simplify it to the following Logic:
pay (PayRequest request) {
PayData data = from(request);
savePayData(data);
invokeBankAPI(data);
return "Payment accepted successfully";
}
Now, we want to change the request to the external bank interface to asynchronous execution. The program will look like this (for readability and explanation, we use Thread here):
pay (PayRequest request) {
PayData data = from(request);
savePayData(data);
new Thread(()-> { invokeBankAPI(data); }).start();
return "Payment accepted successfully";
}
This asynchronous implementation is the above Control.
We only need to start a Thread instance, and the rest of the “how to execute asynchronously” is handled by Java for us.
Now, we need to use transaction control when saving data.
The program will change to the following:
@Transactional
pay (PayRequest request) {
...
}
Then, when saving data, we need to assign values to audit fields, such as creator, record creation time, etc.
@Transactional
pay (PayRequest request) {
PayData data = from(request);
data.setCreateBy(***);
data.setCreateTime(***);
savePayData(data);
new Thread(()-> { invokeBankAPI(data); }).start();
return "Payment accepted successfully";
}
Next, we need to implement idempotency control.
@Transactional
pay (PayRequest request) {
key = createKey(request);
if(!redis.setnx(key)) {
return "Please do not initiate again";
}
PayData data = from(request);
data.setCreateBy(***);
data.setCreateTime(***);
savePayData(data);
new Thread(()-> { invokeBankAPI(data); }).start();
return "Payment accepted successfully";
}
Then, the program needs to handle exceptions and differentiate exception types for different processing.
@Transactional
pay (PayRequest request) {
try {
key = createKey(request);
if(!redis.setnx(key)) {
return "Please do not initiate again";
}
PayData data = from(request);
data.setCreateBy(***);
data.setCreateTime(***);
savePayData(data);
new Thread(()-> { invokeBankAPI(data); }).start();
return "Payment accepted successfully";
} catch(Exception e) {
log.error("Program exception", e);
if (e instanceof ChannelException) {
...
}
}
}
At this point, the program has become quite complex.
Therefore, we need to consider a reasonable design for this code, such as encapsulating idempotency components, global exception handlers, parameter validators, data conversion components, and so on. For example, globally setting audit field values, or migrating the control code for asynchronous requests to the bank interface implementation class.