×
Community Blog Clean Code - Be a Thinking Programmer Instead of a Code Farmer

Clean Code - Be a Thinking Programmer Instead of a Code Farmer

This article discusses code cleanliness in terms of coding, refactoring, and design patterns, and provides insights on how to become a thoughtful programmer.

By Kejia Xu (Yemo)

1
Code cleanliness is the foundation of long-term stability and extensibility in software development. In this article, the author discusses code cleanliness in terms of coding, refactoring, and design patterns, and provides insights on how to become a thoughtful programmer.

The Ideal Code in My Mind

Software development is a collaborative and long-term maintenance process, where code cleanliness serves as the basis for long-term extensibility and scalability. Clean code exhibits a clear structure and follows standard naming conventions, ensuring readability and maintainability. This facilitates quick problem identification and resolution, and supports rapid iteration of new features. Effective collaboration among team members reduces communication costs and enhances overall project research and development efficiency.

To begin, let's look at the definition of good code by Bjarne Stroustrup, the creator of C++.

"I want my code to be elegant and efficient. The logic should be simple and straightforward, making bugs easy to find. Dependencies should be minimized for easier maintenance. Error handling should adhere to clear strategies. Performance should be optimized to avoid code corruption caused by unnecessary user optimizations. Clean code excels at performing a single task well."

In general, elegant code has the following characteristics:

• I Pleasing to read and easy to modify.
• Concise and straightforward, minimizing redundancy and complexity.
• Modular and exhibits high cohesion and low coupling, facilitating maintenance and extension.
• Testable, with unit tests (UT) and end-to-end (E2E) tests ensuring modifiability.
• Includes appropriate comments and documentation, explaining the intent and implementation details of the code.

Code in Reality

Cyclomatic Complexity

Cyclomatic complexity is a metric that measures the complexity of code by counting the number of control flow paths and their complexity. This metric is calculated by counting the decision points in the code, such as conditional statements and loops.

The value of cyclomatic complexity can be used to determine the complexity of the code and the extent of test coverage. A higher cyclomatic complexity indicates that there are more paths and potential executions in the code, which makes it more difficult to understand, maintain, and test the code.

Cyclomatic Complexity Code status Testability Maintenance costs
1~10 Clear High Low
10~20 Complex Medium Medium
20~30 Very complex Low High
>30 Unreadable Unmeasurable Very high

iLogtail Code Complexity Discussion

iLogtail, developed by the Alibaba Cloud Simple Log Service (SLS) team, is an observable data collector that is now open-source on GitHub. Its primary purpose is to assist developers in constructing a unified data collection layer. It not only excels in functionality and performance but also emphasizes clean and elegant code. Additional information can be found in the article Learn Design Patterns with iLogtail

In some key modules of iLogtail, the number of code lines and cyclomatic complexity are kept within a reasonable range.

2

However, there are a few code sections that have reached an extremely complex level, which severely impacts the code's extensibility. For instance, consider the following two functions: the first consists of 563 lines of code with a cyclomatic complexity of 169 [1], and the second has 332 lines of code with a cyclomatic complexity of 71 [2].

3

Note: The preceding cyclomatic complexity is obtained based on the VSCode plug-in Codalyze [3].

In summary, it is often easier to control the structure of simple code. However, code with more complex business logic tends to be less elegant, and its complexity increases over time.

Why does this occur? There are several main reasons:

Complex business logic: If code with complex business logic is not well-designed from the beginning, it becomes difficult to extend. As new features are continuously introduced, code complexity escalates.

Time constraints during the development phase: Due to the need for rapid development, developers sometimes resort to implementing repetitive or poorly structured code first and then addressing it later.

Lack of code refactoring: When code becomes less maintainable, developers may fail to recognize its issues in a timely manner and neglect effective refactoring. Consequently, the code becomes increasingly complex.

Insufficient unit testing and integration testing: This leads to a situation where no one dares to modify the existing code, and therefore, the status quo is maintained.

Next, we will introduce how to use refactoring and design patterns to avoid code corruption in a practical way.

Refactoring

Refactoring is a process of making adjustments to code in order to improve its extensibility and understandability, and reduce maintenance costs without altering the observable behavior of the software.

Identifying Code Issues

As mentioned in the broken window effect, developers are impressed by clean and elegant code. However, if minor issues or bad smells are not addressed in a timely manner, they can accumulate and lead to code deterioration. Therefore, to ensure code quality, it is necessary to have the ability to identify these code issues. Some common code issues include:

• Long functions
• Duplicate code
• Lengthy parameter lists
• Excessive global variables
• Shotgun modification
• Redundant comments

Way of Refactoring

• Reorganize functions

///////////////////////// Original code ///////////////////////////
void printOwing() {
  Enumeration e = _orders.elements();
  double outstanding = 0.0;

  // print banner
  System.out.println("***************************");
  System.out.println("******** Custor Owes ******");
  System.out.println("***************************");

  // calculate outstanding
  while (e.hasMoreElements()) {
    Order each = (Order) e.nextElement();
    outstanding += each.getAmount();
  }
  
  // print details
  System.out.println("name:" + _name);
  System.out.println("amount" + outstanding);
}
///////////////////////// After refactoring ///////////////////////////
void printBanner() {
  System.out.println("***************************");
  System.out.println("******** Custor Owes ******");
  System.out.println("***************************");
}

void printDetails(double outstanding) {
  System.out.println("name:" + _name);
  System.out.println("amount" + outstanding);
}

double getOutstanding() {
  Enumeration e = _orders.elements();
  double outstanding = 0.0;
  
  while (e.hasMoreElements()) {
    Order each = (Order) e.nextElement();
    outstanding += each.getAmount();
  }

  return outstanding;
}

void printOwing() {
  printBanner();
  double outstanding = getOutstanding();
  printDetails(outstanding);
}

• Avoid excessive function parameters

/////////////////// Before refactoring ///////////////////////
// 1
public User getUser(String username, String telephone, String email);
// 2
public void postBlog(String title, String summary, String keywords, String content, String category, long authorId);

/////////////////// After refactoring ///////////////////
// Consider whether the function has a single responsibility and whether it can be split into multiple functions to reduce the number of parameters. 
public User getUserByUsername(String username);
public User getUserByTelephone(String telephone);
public User getUserByEmail(String email);

// Encapsulate the parameters of a function as objects.
public class Blog {
  private String title;
  private String summary;
  private String keywords;
  private Strint content;
  private String category;
  private long authorId;
}
public void postBlog(Blog blog);

• Learn to use explanatory variables

/////////////////// Before refactoring ///////////////////////
if (date.after(SUMMER_START) && date.before(SUMMER_END)) {
  // ...
} else {
  // ...
}

/////////////////// After refactoring ///////////////////
// The logic is clearer after the introduction of explanatory variables.
boolean isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);
if (isSummer) {
  // ...
} else {
  // ...
}

• Remove deep nesting levels. It is best to have no more than two layers of nesting. If there are more than two layers, think about whether nesting can be reduced.

// Code before refactoring
public List<String> matchStrings(List<String> strList,String substr) {
  List<String> matchedStrings = new ArrayList<>();
  if (strList != null && substr != null) {
    for (String str : strList) {
      if (str != null) {
        if (str.contains(substr)) {
          matchedStrings.add(str);
        }
      }
    }
  }
  return matchedStrings;
}

// Code after refactoring: Execute the null logic first, and then execute the normal logic.
public List<String> matchStrings(List<String> strList,String substr) {
  if (strList == null || substr == null) { //先判空
    return Collections.emptyList();
  }

  List<String> matchedStrings = new ArrayList<>();
  for (String str : strList) {
    if (str != null && str.contains(substr)) {
      matchedStrings.add(str);
    }
  }
  return matchedStrings;
}

• There are many methods of refactoring, which cannot be listed here one by one.

Time to Refactor

• Micro-refactoring

• When developing new features, aim to make them easily extensible. Generally, following the three-strike policy is recommended.

• When reading a piece of code, strive to make it easier to understand.

• When fixing bugs and reviewing code, identify and address quality issues in the existing code as planned.

• Module-level refactoring

Perform module-level refactoring when the code becomes overwhelming and its maintainability is poor, significantly impeding development progress.

• Architecture-level refactoring

• Perform architecture-level refactoring when the code structure no longer meets the needs of architectural development.

Refactoring Starts with "Conquering the Fear of Old Code"

Developers often feel intimidated by complex and unwieldy old code. However, as long as you adhere to the following principles, refactoring will no longer be daunting.

• Before refactoring, it is important to have a clear and comprehensive understanding of architecture design reviews, module interactions, and peripheral interactions. This understanding will guide the subsequent refactoring process.

4

• Testing first is crucial, and a robust testing system is key to successful refactoring. Ideally, tests such as unit tests, end-to-end tests, and benchmarks should be in place from the initial development phase.

• Review core scenarios, especially how reliability scenarios, exceptional scenarios, and small details are handled. Omitting any detail can lead to a bug, and some small detail issues may take a long time to surface.

Finally, always remember that thoroughness and attentiveness are essential qualities for code refactoring.

Design Patterns

Abstraction and Layering

As we all know, programmers often jokingly refer to themselves as code farmers. However, I believe there is an essential difference between code farmers and programmers, and that difference is abstract thinking. Code farmers can only perform CRUD operations and solve problems from a narrow perspective, which often leads to repetitive work. In contrast, programmers can solve problems through abstract thinking, summarizing product and technology implementations, and addressing multiple common requirements at once. Software technology is essentially an abstract art.

Abstract thinking is the most important cognitive ability for programmers. The process of abstraction involves identifying commonalities, summarizing, comprehensively analyzing, and refining relevant concepts.

Abstraction disregards details. Abstract classes are the most abstract and overlook most details, much like abstract drawings of cattle with only a few lines. This can be likened to abstract classes or interfaces in code.

Abstractions represent common properties. A class represents the common properties of a group of instances, while an abstract class represents the common properties of a group of classes.

Abstraction is hierarchical. The higher the level of abstraction, the narrower the connotation and the broader the extension. In other words, the narrower the meaning, the greater the generalization ability. For example, cattle are more abstract than buffaloes because cattle can encompass all bovines, while buffaloes are just one type of cattle (class).

5

Design patterns are important summaries of abstract thinking in software development. They can generally be categorized into three types:

Creational patterns: These patterns focus on how objects are created and initialized, addressing the complexities of object creation. Creational patterns include singleton pattern, factory pattern, abstract factory pattern, builder pattern, and prototype pattern.

Structural patterns: These patterns focus on the relationships between objects, solving problems related to object composition and properties. Structural patterns include adapter pattern, bridge pattern, composite pattern, decorator pattern, facade patterns, flyweight pattern, and proxy pattern.

Behavioral patterns: These patterns focus on the communication, interaction, and allocation of responsibilities between objects, addressing complex object interactions. Behavioral patterns include chain of responsibility pattern, command pattern, interpreter pattern, iterator pattern, mediator pattern, memento pattern, observer pattern, state pattern, strategy pattern, template method pattern, and visitor pattern.

In the article Learn Design Patterns with iLogtail, the practical applications of various design patterns are extensively discussed. Here, we will not introduce them one by one, but instead explore how to solve several scenarios.

Scenario 1: How to Avoid Lengthy if-else

I'm sure you have come across the following code, which combines the definition, creation, and use of processing logic. The code becomes unnecessarily lengthy.

public class OrderService {
  public double discount(Order order) {
    double discount = 0.0;
    OrderType type = order.getType();
    if (type.equals(OrderType.NORMAL)) { // Normal order
      //... omit the discount calculation algorithm code
    } else if (type.equals(OrderType.GROUPON)) { // Group purchase order
      //... omit the discount calculation algorithm code
    } else if (type.equals(OrderType.PROMOTION)) { // Promotion order
      //... omit the discount calculation algorithm code
    }
    return discount;
  }
}

Use the strategy pattern to avoid lengthy if-else/switch branches: Deploy the discount strategies for different types of orders into strategy classes, and use the factory class to create strategy objects.

// Define the strategy interface
public interface DiscountStrategy {
  double calDiscount(Order order);
}

// Implement the specific strategy
// Omit NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy class code...

// Create a strategy factory
public class DiscountStrategyFactory {
  private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>();

  static {
    strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
    strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
    strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
  }

  public static DiscountStrategy getDiscountStrategy(OrderType type) {
    return strategies.get(type);
  }
}

// Use the strategy
public class OrderService {
  public double discount(Order order) {
    OrderType type = order.getType();
    DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
    return discountStrategy.calDiscount(order);
  }
}

Scenario 2: Make Good Use of Combinations

Suppose there is a geometry shape class. It extends two subclasses: circle and square. Now we need to introduce the color factor. How to achieve it?

6

The bridge pattern splits a large class or a series of closely related classes into two separate hierarchies of abstraction and implementation that can be used separately during development. If a class has two (or more) independently changing dimensions, these dimensions can be combined so that they can be extended independently.

7

Scenario 3: Use the Adapter Pattern to Improve System Extensibility

The adapter pattern converts one type of interface into another desired type, enabling objects with incompatible interfaces to work together. The adapter receives calls initiated by the client through the adapter interface and converts them into calls that are applicable to the encapsulated service objects.

8

In our system, we often rely on various external systems. By properly utilizing the adapter pattern, we can achieve the following results:

Substitutability of external systems: When it becomes necessary to replace one external system that a project relies on with another external system (for example, switching the log system from Elasticsearch to SLS), the adapter pattern can minimize code changes and testing complexity.

Unification of multiple external system interfaces: The implementation of a function depends on multiple external systems. Through the adapter pattern, their interfaces are adapted to a unified interface definition, allowing code logic to be reused using polymorphism.

Compatibility with deprecated interface versions: During a version upgrade, instead of directly deleting deprecated interfaces, they are temporarily retained, marked as deprecated, and the internal implementation logic is delegated to the new interface implementation. This ensures a transition period for projects using the adapter pattern, rather than forcing code changes.

Reference

• [1] https://github.com/alibaba/ilogtail/blob/main/core/config_manager/ConfigManagerBase.cpp#L376C25-L376C45

• [2] https://github.com/alibaba/ilogtail/blob/main/core/event_handler/EventHandler.cpp#L460

• [3] https://marketplace.visualstudio.com/items?itemName=selcuk-usta.code-complexity-report-generator

• [4] Learn Design Patterns with iLogtail

Disclaimer: The views expressed herein are for reference only and don't necessarily represent the official views of Alibaba Cloud.

0 1 0
Share on

Alibaba Cloud Community

876 posts | 198 followers

You may also like

Comments