Reading through many lines of code in Golang and Java makes for an interesting observation from the codebase which I was taking a look.
There were different styles of coding and it was obvious from reading code that is written by experienced gophers.
People on team who do both Java and Golang use the same Java style in Golang as well.
While most Java programmers come from Clean code school of thought where functions are decomposed into many smaller functions which results in function explosion in the same file.
Also I see a tendency to introduce many layers of abstraction which are not necessary for the problem that is being solved and many are over engineered solutions. It has become a norm and any changes to deviate from the existing pattern invites lots of comments which requires careful explanations.
There is an interesting conversation from Uncle Bob and John and came to realise that
- Clean Code optimizes for discipline and local readability
- A Philosophy of Software Design optimizes for global simplicity and cognitive load.
I am more inclined to think in terms of using the ideas from APOSD than blindly following Clean code. The primary objective must be to reduce complexity in the system, so methods that abstracts an idea can be long enough to implement it fully to contain the whole idea. This makes the method as containers of a concept or an idea instead of splitting it into smaller tasks that does a basic steps.
The main mental model to use is
APOSD: “What design minimizes the amount a human must think about?”
CleanCode: “What discipline keeps code clean over time?”
To illustrate a point here is a sample program to contrast the different styles
Sample Usecase
Process an order checkout:
- Validate order
- Calculate total
- Charge payment
- Save order
- Send confirmation email
Cleancode
public class CheckoutService {
public void checkout(Order order) {
validateOrder(order);
Money total = calculateTotal(order);
chargePayment(order, total);
saveOrder(order);
sendConfirmation(order);
}
private void validateOrder(Order order) {
if (order == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Invalid order");
}
}
private Money calculateTotal(Order order) {
return order.getItems().stream()
.map(Item::price)
.reduce(Money.ZERO, Money::add);
}
private void chargePayment(Order order, Money total) {
paymentGateway.charge(order.getPaymentMethod(), total);
}
private void saveOrder(Order order) {
orderRepository.save(order);
}
private void sendConfirmation(Order order) {
emailService.send(order.getEmail());
}
}
APOSD
public class CheckoutService {
public void checkout(Order order) {
// Validate order upfront to avoid partial processing
if (order == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Invalid order");
}
// Calculate total price of all items
Money total = Money.ZERO;
for (Item item : order.getItems()) {
total = total.add(item.price());
}
// Charge payment before persisting order
paymentGateway.charge(order.getPaymentMethod(), total);
// Persist order and notify customer
orderRepository.save(order);
emailService.send(order.getEmail());
}
}
What I think:
For most organisations that uses Cleancode it is better if we take a middle ground
public class CheckoutService {
public void checkout(Order order) {
validateOrder(order);
Money total = calculateTotal(order);
processPaymentAndFinalize(order, total);
}
private void validateOrder(Order order) {
if (order == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Invalid order");
}
}
private Money calculateTotal(Order order) {
Money total = Money.ZERO;
for (Item item : order.getItems()) {
total = total.add(item.price());
}
return total;
}
// Groups steps that conceptually belong together
private void processPaymentAndFinalize(Order order, Money total) {
paymentGateway.charge(order.getPaymentMethod(), total);
orderRepository.save(order);
emailService.send(order.getEmail());
}
}
Before splitting a method:
We could follow some rules, making it rule based as well and some of them are
- Does this extraction hide complexity or create indirection? It should hide complexity split only if it does
- Will the reader understand the flow faster or slower? It should be faster to understand
- Is the abstraction deep or just a renamed code block? It should be deep, just don’t extract and rename a code block that could easily stay on the same method.
Reference
Link APOSD vs CleanCode