21 Code Duplication the Three Ultimate Moves to Tackle Code Repetition

21 Code Duplication The Three Ultimate Moves to Tackle Code Repetition #

Today, I’m going to talk to you about three tricks to deal with code duplication.

Business colleagues complain that business development lacks technical complexity. They seldom use design patterns, advanced Java features, and object-oriented programming. They usually just focus on CRUD operations, which prevents personal growth. Every time an interviewer asks, “Please tell me about the commonly used design patterns,” they can only mention the Singleton pattern because they have heard of other design patterns but haven’t used them. As for advanced features such as reflection and annotations, they are only aware that they are frequently used in framework development but they don’t write framework code, so they don’t have a chance to apply them.

In fact, I don’t think it’s like that. Design patterns and object-oriented programming are the experiences accumulated by predecessors in large-scale projects. Through these methodologies, they improve the maintainability of large-scale projects. The reason why advanced features such as reflection, annotations, and generics are widely used in frameworks is that frameworks often need to apply the same set of algorithms to different data structures. These features can help reduce code duplication and improve project maintainability.

In my opinion, maintainability is an important indicator of the maturity of a large-scale project, and reducing code duplication is a crucial way to improve maintainability. So why do I say that?

If multiple instances of duplicate code implement the same functionality, it is easy to forget to modify one place after making changes, resulting in bugs.

Some code is not completely duplicated, but has a high degree of similarity. Modifying such similar code is prone to mistakes (copy-paste errors), making originally distinct areas become the same.

Today, I will start with the three most common requirements in business code and talk to you about how to use some advanced features and design patterns in Java, as well as some tools, to eliminate code duplication in an elegant and sophisticated way. Through today’s learning, I also hope to change your perception that business code lacks technical complexity.

Using Factory Pattern + Template Method Pattern to Eliminate if…else and Code Duplication #

Suppose we need to develop a shopping cart ordering function that handles different users in different ways:

  • Regular users need to pay for shipping, which is 10% of the product price, and there is no discount on the products.

  • VIP users also need to pay 10% delivery fee based on the product price, but they enjoy a certain discount when buying two or more of the same product.

  • Internal users can have free shipping, and there is no discount on the products.

Our goal is to implement the business logic for three types of shopping carts, converting the input Map object (with the key as the product ID and the value as the quantity) to the output Cart object.

First, let’s implement the shopping cart processing logic for regular users:

// Shopping Cart
@Data
public class Cart {

    // Item list
    private List<Item> items = new ArrayList<>();
    
    // Total discount
    private BigDecimal totalDiscount;

    // Total item price
    private BigDecimal totalItemPrice;

    // Total delivery price
    private BigDecimal totalDeliveryPrice;

    // Total amount payable
    private BigDecimal payPrice;

}

// Item in the shopping cart
@Data
public class Item {

    // Item ID
    private long id;

    // Item quantity
    private int quantity;

    // Item price
    private BigDecimal price;

    // Item discount
    private BigDecimal couponPrice;

    // Item delivery fee
    private BigDecimal deliveryPrice;

}

// Shopping cart processing for regular users
public class NormalUserCart {

    public Cart process(long userId, Map<Long, Integer> items) {
        Cart cart = new Cart();
        // Convert the shopping cart from Map to Item list
        List<Item> itemList = new ArrayList<>();
        items.entrySet().stream().forEach(entry -> {
            Item item = new Item();
            item.setId(entry.getKey());
            item.setPrice(Db.getItemPrice(entry.getKey()));
            item.setQuantity(entry.getValue());
            itemList.add(item);
        });
        cart.setItems(itemList);
        // Calculate the delivery fee and item discount
        itemList.stream().forEach(item -> {
            // Delivery fee is 10% of the total item price
            item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
            // No discount
            item.setCouponPrice(BigDecimal.ZERO);
        });

        // Calculate the total item price
        cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));

        // Calculate the total delivery price
        cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));

        // Calculate the total discount
        cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));

        // Calculate the amount payable = total item price + total delivery price - total discount
        cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
        return cart;
    }
}

Next, let’s implement the shopping cart logic for VIP users. The difference from the logic for regular users is that VIP users can enjoy discounts when buying multiple items of the same product. So, we only need to handle the additional discount for buying multiple items:

public class VipUserCart {
    public Cart process(long userId, Map<Long, Integer> items) {
        ...

        itemList.stream().forEach(item -> {

            // Delivery fee is 10% of the total item price
            item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
            // Enjoy a discount when buying more than two of the same item
            if (item.getQuantity() > 2) {
                item.setCouponPrice(item.getPrice()
                        .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
                        .multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
            } else {
                item.setCouponPrice(BigDecimal.ZERO);
            }
        });
        ...
        return cart;
    }
}

Finally, for internal users who have free shipping and no discounts, we only need to handle the differences in discount and delivery fee:

public class InternalUserCart {

    public Cart process(long userId, Map<Long, Integer> items) {
        ...
        itemList.stream().forEach(item -> {
            // Free shipping
            item.setDeliveryPrice(BigDecimal.ZERO);
            // No discount
            item.setCouponPrice(BigDecimal.ZERO);
        });
        ...
        return cart;
    }
}

By comparing the code, we can see that 70% of the code for the three shopping carts is duplicated. The reason is simple: although the calculation of delivery fee and discounts for different types of users is different, the logic for initializing the shopping cart, calculating the total price, total delivery price, total discount, and payment price remains the same.

As we mentioned at the beginning, code duplication itself is not scary; what’s scary is the possibility of forgetting to modify or introducing bugs when modifying the duplicated code. For example, if the developer working on the VIP user shopping cart realizes that there is a bug in the calculation of the total item price, that it should not be the sum of all item prices but the sum of item prices multiplied by quantity, they may only modify the code for the VIP user shopping cart and overlook the fact that the duplicated logic in the regular user and internal user shopping carts also has the same bug.

With the three shopping carts implemented, we need to use different shopping carts based on the different user types. The following code shows how to call different process methods of the shopping carts for different user types using three if statements:

@GetMapping("wrong")
public Cart wrong(@RequestParam("userId") int userId) {

    // Get the user type based on the user ID
    String userCategory = Db.getUserCategory(userId);
// Common user processing logic
if (userCategory.equals("Normal")) {
    NormalUserCart normalUserCart = new NormalUserCart();
    return normalUserCart.process(userId, items);
}

// VIP user processing logic
if (userCategory.equals("Vip")) {
    VipUserCart vipUserCart = new VipUserCart();
    return vipUserCart.process(userId, items);
}

// Internal user processing logic
if (userCategory.equals("Internal")) {
    InternalUserCart internalUserCart = new InternalUserCart();
    return internalUserCart.process(userId, items);
}
return null;
}

E-commerce marketing strategies are diverse, and there will inevitably be more user types in the future, requiring more shopping carts. Do we have to repeatedly add more shopping cart classes and write repetitive cart logic and if statements?

Of course not, the same code should only appear in one place!

If we are familiar with the definitions of abstract classes and abstract methods, we may think, can we define the repetitive logic in the abstract class and let the three carts implement the different parts of the logic?

Actually, this pattern is called the Template Method pattern. We implement the template for the cart processing in the abstract class, leaving the parts that need special handling as abstract methods to be implemented by the subclasses. Since the logic in the abstract class is incomplete and cannot work on its own, it needs to be defined as an abstract class.

As shown in the following code, the AbstractCart abstract class implements the common logic for the cart, and defines two abstract methods for the subclasses to implement. The processCouponPrice method is used to calculate the coupon price for each item, and the processDeliveryPrice method is used to calculate the delivery price.

public abstract class AbstractCart {

    // Common cart logic implemented in the parent class
    public Cart process(long userId, Map<Long, Integer> items) {

        Cart cart = new Cart();
        List<Item> itemList = new ArrayList<>();
        items.entrySet().stream().forEach(entry -> {
            Item item = new Item();
            item.setId(entry.getKey());
            item.setPrice(Db.getItemPrice(entry.getKey()));
            item.setQuantity(entry.getValue());
            itemList.add(item);
        });
        cart.setItems(itemList);
        // Let the subclasses handle the coupon price and delivery price for each item
        itemList.stream().forEach(item -> {
            processCouponPrice(userId, item);
            processDeliveryPrice(userId, item);
        });
        // Calculate the total price of the items
        cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));

        // Calculate the total delivery price
        cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));

        // Calculate the total discount
        cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));

        // Calculate the payable price
        cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));

        return cart;
    }

    // The logic for handling item coupons is left for the subclasses to implement
    protected abstract void processCouponPrice(long userId, Item item);

    // The logic for handling delivery prices is left for the subclasses to implement
    protected abstract void processDeliveryPrice(long userId, Item item);
}

With this abstract class, the implementation of the three subclasses becomes very simple. The NormalUserCart, which represents the cart for a normal user, implements the logic of 0 discount and 10% delivery price:

@Service(value = "NormalUserCart")
public class NormalUserCart extends AbstractCart {

    @Override
    protected void processCouponPrice(long userId, Item item) {
        item.setCouponPrice(BigDecimal.ZERO);
    }

    @Override
    protected void processDeliveryPrice(long userId, Item item) {
        item.setDeliveryPrice(item.getPrice()
                .multiply(BigDecimal.valueOf(item.getQuantity()))
                .multiply(new BigDecimal("0.1")));
    }
}

The VipUserCart, representing the cart for a VIP user, directly extends NormalUserCart and only needs to modify the bulk discount policy:

@Service(value = "VipUserCart")
public class VipUserCart extends NormalUserCart {

    @Override
    protected void processCouponPrice(long userId, Item item) {
        if (item.getQuantity() > 2) {
            item.setCouponPrice(item.getPrice()
                    .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
                    .multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
        } else {
            item.setCouponPrice(BigDecimal.ZERO);
        }
    }
}

The InternalUserCart, representing the cart for an internal user, is the simplest and only requires setting 0 delivery price and 0 discount:

@Service(value = "InternalUserCart")
public class InternalUserCart extends AbstractCart {

    @Override
    protected void processCouponPrice(long userId, Item item) {
        item.setCouponPrice(BigDecimal.ZERO);
    }

    @Override
    protected void processDeliveryPrice(long userId, Item item) {
        item.setDeliveryPrice(BigDecimal.ZERO);
    }
}

The relationship between the abstract class and the three subclasses is shown in the following diagram:

img

Isn’t it much simpler than having three separate cart programs? Next, let’s see how to avoid the three if statements.

Perhaps you have already noticed that when defining the three cart subclasses, we named the beans in the @Service annotation. Since all three carts are named XXXUserCart, we can construct the name of the cart bean by concatenating the user type string with “UserCart”. Then, using Spring’s IoC container, we can directly obtain the AbstractCart by the bean name, and call its process method to achieve the common functionality.

In fact, this is the factory pattern, but implemented using the Spring container:

@GetMapping("right")
public Cart right(@RequestParam("userId") int userId) {
    String userCategory = Db.getUserCategory(userId);
    AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart");
    return cart.process(userId, items);
}

Imagine that in the future, if there are new user types or new user logic, we don’t need to modify any code. We only need to add a new XXXUserCart class that inherits from AbstractCart and implements the special discount and delivery price handling logic.

In this way, we have used the factory pattern + template method pattern to eliminate duplicate code and avoid the risk of modifying existing code. This is the open-closed principle in design patterns: closed for modification, open for extension.

Using Annotations + Reflection to Eliminate Duplicate Code #

Are you excited? Even business code can be written in OOP. Let’s take a look at another case of calling a third-party interface, which is also a typical business logic.

Assume that a bank provides some API interfaces, and the parameter serialization is a bit special. Instead of using JSON, we need to concatenate the parameters together to form a large string.

According to the API documentation provided by the bank, all parameters are concatenated to form fixed-length data, and then combined together as the entire string.

Because each type of parameter has a fixed length, padding is required when the length is not reached:

  • String-type parameters need to be right-padded with underscores for the remaining length, so that the string content is left-aligned;
  • Numeric-type parameters need to be left-padded with 0 for the remaining length, so that the actual number is right-aligned;
  • Currency-type representations need to be rounded down to 2 decimal places to the cent and used as numbers, we also left-pad them with 0 for the remaining length.

All parameters are then MD5 hashed as the signature (for easy understanding, salt is not involved in the demo implementation).

For example, the definitions of the createUser and pay methods are as follows:

img

img

Implementing the code is straightforward - simply perform the padding, add the signature, and make the request call based on the interface definition:

public class BankService {

    // createUser method
    public static String createUser(String name, String identity, String mobile, int age) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        // strings are left-aligned, fill the remaining spaces with _
        stringBuilder.append(String.format("%-10s", name).replace(' ', '_'));
        // strings are left-aligned, fill the remaining spaces with _
        stringBuilder.append(String.format("%-18s", identity).replace(' ', '_'));
        // numbers are right-aligned, fill the remaining spaces with 0
        stringBuilder.append(String.format("%05d", age));
        // strings are left-aligned, fill the remaining spaces with _
        stringBuilder.append(String.format("%-11s", mobile).replace(' ', '_'));
        // add MD5 hash as signature
        stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
        return Request.Post("http://localhost:45678/reflection/bank/createUser")
                .bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON)
                .execute().returnContent().asString();
    }

    // pay method
    public static String pay(long userId, BigDecimal amount) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();

        // numbers are right-aligned, fill the remaining spaces with 0
        stringBuilder.append(String.format("%020d", userId));

        // round amount down to 2 decimal places to the cent, convert to long as number, and right-align; fill the remaining spaces with 0
        stringBuilder.append(String.format("%010d", amount.setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue()));

        // add MD5 hash as signature
        stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));

        return Request.Post("http://localhost:45678/reflection/bank/pay")
                .bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON)
                .execute().returnContent().asString();
    }
}

As we can see, the granularity of duplicated code in this code is finer:

  • The processing logic of three standard data types is duplicated, which can easily lead to bugs.
  • The logic involved in string concatenation, signature generation, and request sending is duplicated across all methods.
  • The parameter types and order of the actual methods may not match the interface requirements, making it prone to errors.
  • The code hardcodes the processing for each parameter, making it difficult to perform clear checks. If there are dozens or even hundreds of parameters, the probability of error is greatly increased.

So how to refactor this code? Yes, we should use annotations and reflection!

With these two weapons, annotations and reflection, we can use a single set of code to implement all the logic related to bank requests without any duplication.

To separate the interface logic from the implementation logic, we first need to define all interface parameters as POJO classes (data classes with only attributes and no business logic). For example, the parameters of the createUser API can be defined as follows:

@Data
public class CreateUserAPI {

    private String name;
    private String identity;
    private String mobile;
    private int age;
}

With the interface parameter definition, we can add some metadata to the interfaces and all parameters by using custom annotations. For instance, let’s define an annotation BankAPI for the API interfaces, which includes the URL address and interface description:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface BankAPI {

    String desc() default "";
    String url() default "";
}

Next, let’s define another annotation @BankAPIField to describe the field specification of each interface, including the order, type, and length of the parameter:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface BankAPIField {

    int order() default -1;
    int length() default -1;
    String type() default "";
}

Now, annotations can come into play.

As shown below, we define the CreateUserAPI class to describe the information of the createUser interface. By adding the @BankAPI annotation to the interface, we can provide the URL, description, and other metadata. By adding the @BankAPIField annotation to each field, we can provide the order, type, length, and other metadata of the parameters:

@BankAPI(url = "/bank/createUser", desc = "创建用户接口")
@Data
public class CreateUserAPI extends AbstractAPI {

The translation of the code to English is complete. @BankAPIField(order = 1, type = “S”, length = 10) private String name;

    @BankAPIField(order = 2, type = "S", length = 18)
    private String identity;

    @BankAPIField(order = 4, type = "S", length = 11) // Note that the order here needs to follow the sequence in the API table
    private String mobile;

    @BankAPIField(order = 3, type = "N", length = 5)
    private int age;
}

Another class, PayAPI, has a similar implementation:

@BankAPI(url = "/bank/pay", desc = "Payment API")
@Data
public class PayAPI extends AbstractAPI {

    @BankAPIField(order = 1, type = "N", length = 20)
    private long userId;

    @BankAPIField(order = 2, type = "M", length = 10)
    private BigDecimal amount;
}

The class AbstractAPI, which both of these classes inherit from, is an empty implementation because in this case, there is no common data for the interfaces that can be abstracted into a base class.

With these two classes, we can check against the API list table in a matter of seconds. In theory, if our core translation process (i.e. the process of serializing annotations and interface APIs into the required request strings) is working correctly, as long as the annotations and the table are consistent, there should be no issues with the translation of API requests.

Through annotations, we have achieved the description of API parameters. Next, let’s see how reflection works together with annotations to achieve dynamic interface parameter assembly.

In the third line of code, we obtain the BankAPI annotation from the class and retrieve its URL property for subsequent remote calls.

In lines 6 to 9, we use stream to quickly obtain all fields in the class that have the BankAPIField annotation, sort the fields based on their order property, and set the private fields accessible for reflection.

In lines 12 to 38, we obtain the values of the annotations through reflection, and then based on the BankAPIField parameters, format them according to the three types, consolidating the formatting logic for all parameters in this single location.

In lines 41 to 48, we implement parameter signing and request invocation.

private static String remoteCall(AbstractAPI api) throws IOException {

    // Get the request URL from the BankAPI annotation
    BankAPI bankAPI = api.getClass().getAnnotation(BankAPI.class);
    bankAPI.url();

    StringBuilder stringBuilder = new StringBuilder();
    Arrays.stream(api.getClass().getDeclaredFields()) // Get all fields
            .filter(field -> field.isAnnotationPresent(BankAPIField.class)) // Find fields marked with the annotation
            .sorted(Comparator.comparingInt(a -> a.getAnnotation(BankAPIField.class).order())) // Sort the fields based on the order in the annotation
            .peek(field -> field.setAccessible(true)) // Set private fields accessible
            .forEach(field -> {
                // Get the annotation
                BankAPIField bankAPIField = field.getAnnotation(BankAPIField.class);
                Object value = "";
                try {
                    // Get the field value through reflection
                    value = field.get(api);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
                // Format the string according to the field type and the filling method
                switch (bankAPIField.type()) {
                    case "S": {
                        stringBuilder.append(String.format("%-" + bankAPIField.length() + "s", value.toString()).replace(' ', '_'));
                        break;
                    }
                    case "N": {
                        stringBuilder.append(String.format("%" + bankAPIField.length() + "s", value.toString()).replace(' ', '0'));
                        break;
                    }
                    case "M": {
                        if (!(value instanceof BigDecimal))
                            throw new RuntimeException(String.format("{}'s {} must be BigDecimal", api, field));
                        stringBuilder.append(String.format("%0" + bankAPIField.length() + "d", ((BigDecimal) value).setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue()));
                        break;
                    }
                    default:
                        break;
                }
            });
    // Signature logic
    stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
    String param = stringBuilder.toString();
    long begin = System.currentTimeMillis();
    // Send the request
    String result = Request.Post("http://localhost:45678/reflection" + bankAPI.url())
            .bodyString(param, ContentType.APPLICATION_JSON)
            .execute().returnContent().asString();
    log.info("Called bank API {} url:{} param:{} took:{}ms", bankAPI.desc(), bankAPI.url(), param, System.currentTimeMillis() - begin);
    return result;

}

As we can see, all the core logic for handling parameter sorting, filling, signing, and invoking requests is consolidated in the remoteCall method. With this core method, the implementation of each interface in the BankService becomes very simple, only involving parameter assembly and calling remoteCall.

// Method for creating a user
public static String createUser(String name, String identity, String mobile, int age) throws IOException {

    CreateUserAPI createUserAPI = new CreateUserAPI();

    createUserAPI.setName(name);

    createUserAPI.setIdentity(identity);

    createUserAPI.setAge(age);

    createUserAPI.setMobile(mobile);

    return remoteCall(createUserAPI);
}

// Payment method
public static String pay(long userId, BigDecimal amount) throws IOException {

    PayAPI payAPI = new PayAPI();

    payAPI.setUserId(userId);

    payAPI.setAmount(amount);

    return remoteCall(payAPI);
}

In fact, many common processing tasks that involve class structure can be reduced by following this pattern. Reflection allows us to handle class members based on a fixed logic without knowing the class structure beforehand. Annotations, on the other hand, give us the ability to add metadata to these members, allowing us to obtain more data of interest from external sources when using reflection to implement general logic.

Using Property Copying Tools to Eliminate Duplicate Code #

Finally, let’s take a look at another common code logic that often appears in business code - copying entities between layers.

For a three-tier architecture system, considering the decoupling and different data requirements between layers, each layer usually has its own POJO as a data entity. For example, the entity used in the data access layer is usually called DataObject or DO, the entity used in the business logic layer is usually called Domain, and the entity used in the presentation layer is usually called Data Transfer Object or DTO.

Here, we need to be aware that manually writing the assignment code between these entities can also lead to errors.

For complex business systems, it is normal for entities to have dozens or even hundreds of attributes. For example, let’s consider a DTO called ComplicatedOrderDTO that describes dozens of attributes in an order. If we want to convert this DTO to a similar DO, copying a majority of the fields and then storing the data, it will inevitably require many attribute mapping assignment operations. Does the dense code already make your head spin?

ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO();
    
ComplicatedOrderDO orderDO = new ComplicatedOrderDO();
    
orderDO.setAcceptDate(orderDTO.getAcceptDate());
    
orderDO.setAddress(orderDTO.getAddress());
    
orderDO.setAddressId(orderDTO.getAddressId());
    
orderDO.setCancelable(orderDTO.isCancelable());
    
orderDO.setCommentable(orderDTO.isComplainable()); // attribute error
    
orderDO.setComplainable(orderDTO.isCommentable()); // attribute error
    
orderDO.setCancelable(orderDTO.isCancelable());
    
orderDO.setCouponAmount(orderDTO.getCouponAmount());

// and so on...

If it weren’t for the comments in the code, could you identify the many problems in it?

If the original DTO has 100 fields, and we need to copy 90 fields into the DO, while leaving 10 fields empty, how do we verify its correctness in the end? Counting them? Even if we count 90 lines of code, it is not necessarily correct, as properties can be overwritten.

Sometimes field names are similar, such as “complainable” and “commentable”, which are easy to mix up (lines 7 and 8), or we may accidentally assign the same source field to two target fields (e.g., line 28).

We were supposed to assign values from the DTO to the DO, but during the set operation, we fetched the value from the DO itself (e.g., line 20), resulting in an ineffective assignment.

This code snippet was not written by me on the spot, but is a real example. A student actually made a mistake in assigning the latitude and longitude, because there were too many fields to store. This bug went unnoticed for a long time until the latitude and longitude stored in the database were used for calculations and it was discovered that they had been stored incorrectly all along.

The fix is very simple: we can use mapping tools like BeanUtils to perform bean conversion. The copyProperties method also allows us to provide the properties to be ignored:

ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO();
ComplicatedOrderDO orderDO = new ComplicatedOrderDO();
BeanUtils.copyProperties(orderDTO, orderDO, "id");
return orderDO;

Key Review #

As the saying goes, “if you often walk by the river, your shoes will get wet.” Repeat the code too much and it will eventually lead to errors. Today, I have shared with you several common dimensions and examples of repetitive issues that may occur in practical business scenarios, as well as methods to eliminate duplication.

The first type of code duplication occurs when multiple parallel classes implement similar code logic. We can consider extracting the common logic and implementing it in a parent class, while leaving the different logic to be implemented by sub-classes through abstract methods. Use a template method to fix the same process and logic as a template, while avoiding code duplication as much as possible. In addition, you can use the IoC feature of Spring to inject the corresponding sub-classes to avoid the need for a large amount of if…else code when instantiating sub-classes.

The second type of code duplication is when the same data processing algorithm is repeated by hard coding. We can consider converting the rules into custom annotations, which serve as metadata to describe classes or fields and methods. Then, through reflection, dynamically read these metadata, fields, or invoke methods to achieve the separation of rule parameters and rule definitions. In other words, put the variable part, which is the rule parameters, into the annotation, and handle the definition of the rule uniformly.

The third type of code duplication is commonly seen in business code when performing DO, DTO, and VO conversions, where numerous fields need to be assigned manually. When dealing with complex types with hundreds of properties, it is very prone to errors. My suggestion is to avoid manual assignment and consider using bean mapping tools. In addition, you can also consider using unit tests to verify the correctness of all fields’ assignments.

Finally, I want to mention that I consider code duplication as an important indicator for evaluating project quality. If a project hardly has any duplicate code, then its internal abstraction must be very good. When doing project refactoring, you can also consider eliminating duplication as the primary goal of implementation.

The code used today is all placed on GitHub. You can click on this [link](GitHub link) to view it.

Reflection and Discussion #

In addition to the Template Method design pattern being a good way to reduce duplicated code, the Observer pattern is also commonly used for code reuse (and in a loosely coupled way). Spring also provides similar tools (click here to view), can you think of any application scenarios?

Regarding the Bean property copying tool, in addition to the simple usage of Spring’s BeanUtils class, do you know any other object mapping libraries? What functionalities do they have?

Do you have any insights or methods for eliminating duplicated code? I’m Zhu Ye, feel free to leave a comment in the comment section to share your thoughts with me. You’re also welcome to share today’s content with your friends or colleagues to exchange ideas together.