This article is based on notes from the book “Listening to the Wind with the Left Ear: A Legendary Programmer’s Leveling Guide”.
The essence of programming can be summarized into three core elements: Logic, Control, and Data. The various programming paradigms and design methodologies we use essentially revolve around these three aspects.
- Logic: Reflects the essence of the problem and defines the specific methods to solve it.
- Control: Strategies for solving problems, such as loop execution, asynchronous execution, and other flow controls.
- Data: The representation and carrier of the problem.
In actual development, the business logic itself is often quite complex, which is one of the main sources of program complexity. When these complex logics intertwine with control flows, they constitute the overall complexity of the program.
Therefore, effectively separating logic, control, and data becomes key to writing high-quality programs. This involves a core concept in software engineering—decoupling: reducing the degree of dependency between modules to eliminate unnecessary coupling relationships.
Understanding the Evolution of LCD through an Aggregated Payment Case
Let’s understand the separation of logic, control, and data through an example of an aggregated payment system.
Initial Version: Pure Business Logic
The aggregated payment system receives a payment request, saves the payment data, and then requests an 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";
}
This version focuses solely on the core business logic: converting data, saving records, and calling the bank interface.
Introducing Control: Asynchronous Execution (using Thread for readability)
pay(PayRequest request) {
PayData data = from(request);
savePayData(data);
// Control: Asynchronous execution
new Thread(() -> {
invokeBankAPI(data);
}).start();
return "Payment accepted successfully";
}
Here, a control element—asynchronous execution—is introduced. We simply start a thread, and the Java runtime environment handles the specifics of asynchronous execution.
Adding Transaction Control and Audit Fields like Creation Time and Creator
@Transactional
pay(PayRequest request) {
PayData data = from(request);
data.setCreateBy(***); // Data processing
data.setCreateTime(***); // Data processing
savePayData(data);
new Thread(() -> {
invokeBankAPI(data);
}).start();
return "Payment accepted successfully";
}
Transaction control and data processing logic have been added, making the code more complex.
Adding 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";
}
The addition of idempotency control further increases the complexity of the code.
Exception Handling with Different Treatments for Exception Types
@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) {
// Special handling
}
}
}
At this point, the code has become quite complex, containing multiple concerns: business logic, asynchronous control, transaction management, idempotency checks, and exception handling.
Decoupling and Refactoring: Returning to the Essence of LCD
When the code becomes this complex, we need to rethink the design and effectively separate logic, control, and data:
- Encapsulate Idempotency Components: Abstract the idempotency check logic into an independent component.
- Implement Global Exception Handling: Avoid try-catch blocks in business code through a unified exception handling mechanism.
- Parameter Validator: Independently handle parameter validation logic.
- Data Transformation Component: Specifically responsible for data transformation and processing.
- Global Audit Field Handling: Automatically set fields like creator and creation time through AOP or framework mechanisms.
- Control Logic Migration: Migrate the code for asynchronously calling the bank interface to a dedicated bank channel implementation class.
Through this decoupling approach, we can keep the core business logic simple and clear while placing various control concerns and data processing logic in appropriate locations and layers.
Conclusion
The essence of programming lies in the reasonable organization of the relationships between logic, control, and data. Excellent program design does not avoid complexity but manages it through effective separation and decoupling. When we can clearly distinguish “what to do” (logic), “how to do it” (control), and “what to use” (data), the readability, maintainability, and scalability of the code will significantly improve.
This LCD separation concept applies not only to individual methods or classes but also to the design of system architecture. At any level, maintaining clear boundaries between logic, control, and data is a key feature of high-quality software.