[Java background development specification] - cycle complexity

preface

Most of those who do java development may have read Alibaba's Java background development manual, which contains some contents about Java background development specifications, basically covering some general and universal specifications, but most of them are concise. This paper will mainly use more cases to explain some specifications and supplement them in combination with their own experience!

Other types of specifications

[Java background development specification] - not simple naming
[Java background development specification] - log output
[Java background development specification] - thread and concurrency
[Java background development specification] - long function and long parameter

Cycle complexity

Cycle complexity is a standard to measure code complexity. Its number is expressed as the number of independent paths, which can also be understood as the number of test cases that cover all possible situations. High cycle complexity indicates that the judgment logic of program code is complex, may be of low quality, and difficult to test and maintain.

In the sonar specification, there are often scans for cycle complexity. For example, the cycle complexity of a method should not exceed 5 or 10.

Generally speaking, every time if, for, while, case, catch and other statements appear in the code, the cycle complexity is increased by 1.

Obviously, the purpose of circle complexity is to reduce the abuse of control statements in the code, such as:

Next, let's look at how we can reduce cycle complexity.

Extract Method

This is similar to the long function mentioned earlier. How can there be a general method to extract it? Most of the conditional control statements can also be extracted. In short, one large function becomes multiple small functions.

if,else

When it comes to the optimization of if and else, we must think of the strategy mode. The strategy mode mainly solves the high complexity caused by using if and else when a variety of algorithms are similar. However, in addition to using the strategy mode, there are some other tips. I believe it should be useful both consciously and unconsciously in the process of peacetime development, Let's have a look.

Guard statement

The first way is to use guard statements.

This is normal logic, with nested if control statements

public void test() {
    if(Condition 1 holds){
        if(Condition 2 holds){
            implement xxx logic;
        }
    }
}

After optimization, nesting disappears

public void test() {
    if(!Condition 1 holds){
        return;
    }
    if(!Condition 2 holds){
        return;
    }
    implement xxx logic;
}

This is a classic refactoring method: nested conditional expressions are replaced by guard statements

By reducing nesting, the indentation of the hierarchy is reduced, which makes it easier to read and understand and reduces the complexity of the code.

Go to else

public void test() {
    if (10 < amount && amount < 20) {
        implement xxx logic;
    } else if (21 < amount && amount < 30) {
        implement xxx logic;
    } else if (31 < amount && amount < 40) {
        implement xxx logic;
    } else {
        implement xxx logic;
    }
}

After else is removed

public void test1() {
    if (10 < amount && amount < 20) {
        implement xxx logic;
        return;
    }
    if (21 < amount && amount < 30) {
        implement xxx logic;
        return;
    }
    if (31 < amount && amount < 40) {
        implement xxx logic;
        return;
    }
    implement xxx logic;
}

This is also a common way to reduce complexity levels.

Strategy mode

Finally, let's look at the method of strategic pattern.

The following logic describes that if memberAttr is a VIP, we will give different discounts according to the VIP level of the user.

public BigDecimal test(String memberAttr) {
    BigDecimal amount = BigDecimal.valueOf(10d);
    if ("VIP".equals(memberAttr)) {
        String level = getVipLevel();
        if ("1".equals(level)) {
            return amount.multiply(BigDecimal.valueOf(0.9));
        }
        if ("2".equals(level)) {
            return amount.multiply(BigDecimal.valueOf(0.8));
        }
        return amount.multiply(BigDecimal.valueOf(0.7));
    }
    return amount;
}

This is the simplest implementation.

Guard statement optimization

public BigDecimal test(String memberAttr, String userId) {
    BigDecimal amount = BigDecimal.valueOf(10d);
    if (!"VIP".equals(memberAttr)) {
        return amount;
    }
    String level = getVipLevel(userId);
    if ("1".equals(level)) {
        amount = amount.multiply(BigDecimal.valueOf(0.9));
    } else if ("2".equals(level)) {
        amount = amount.multiply(BigDecimal.valueOf(0.8));
    } else {
        amount = amount.multiply(BigDecimal.valueOf(0.7));
    }
    return amount;
}

Go to else

public BigDecimal test(String memberAttr, String userId) {
    BigDecimal amount = BigDecimal.valueOf(10d);
    if (!"VIP".equals(memberAttr)) {
        return amount;
    }
    String level = getVipLevel(userId);
    if ("1".equals(level)) {
        return amount.multiply(BigDecimal.valueOf(0.9));
    }
    if ("2".equals(level)) {
        return amount.multiply(BigDecimal.valueOf(0.8));
    }
    return amount.multiply(BigDecimal.valueOf(0.7));
}

Strategy mode

public BigDecimal method1(String memberAttr, String userId) {
    BigDecimal amount = BigDecimal.valueOf(10d);
    if (!"VIP".equals(memberAttr)) {
        return amount;
    }
    String level = getVipLevel(userId);
    DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(level);
    return discountStrategy.discount(amount);
}

switch

Switch is actually a simplified writing of if and else, which is grammatically optimized. However, it should be noted that if the conditions of switch appear in too many business scenarios, it may need to be optimized.

We can change the method just now to switch

public BigDecimal test(String memberAttr, String userId) {
    BigDecimal amount = BigDecimal.valueOf(10d);
    if (!"VIP".equals(memberAttr)) {
        return amount;
    }
    String level = getVipLevel(userId);
    switch (level) {
        case "1":
            return amount.multiply(BigDecimal.valueOf(0.9));
        case "2":
            return amount.multiply(BigDecimal.valueOf(0.8));
        default:
            return amount.multiply(BigDecimal.valueOf(0.7));
    }
}

Suppose there is another super VIP, it may be like this.

public BigDecimal test(String memberAttr, String userId) {
    BigDecimal amount = BigDecimal.valueOf(10d);
    if (!"VIP".equals(memberAttr)) {
        return amount;
    }
    String level = getSuperVipLevel(userId);
    switch (level) {
        case "1":
            return amount.multiply(BigDecimal.valueOf(0.8));
        case "2":
            return amount.multiply(BigDecimal.valueOf(0.7));
        default:
            return amount.multiply(BigDecimal.valueOf(0.6));
    }
}

The discount intensity has changed, but it is also distinguished according to the user level. If there are more scenarios in which business logic processing is carried out according to the user level, we should consider the abstract and polymorphic way. Otherwise, you need to find all places where switch is used and modify it.

loop nesting

for loop nesting is not easy to optimize, because it may be related to the selected algorithm. for example, the following classic method is to find the intersection of two arrays.

The first version, for loop nesting, high complexity and poor performance, should not be taken.

public int[] intersection(int[] nums1, int[] nums2) {
    Set<Integer> intersectionSet = new HashSet<>();
    for (int i = 0; i < nums1.length; i++) {
        for (int j = 0; j < nums2.length; j++) {
            if (nums1[i] == nums2[j]) {
                intersectionSet.add(nums1[i]);
            }
        }
    }
    return intersectionSet.stream().mapToInt(Integer::valueOf).toArray();
}

In the second version, the API is borrowed, and the loop nesting is gone. The intersection is realized by using the retainAll function, and the performance is better than that in the first version.

public static int[] intersection1(int[] nums1, int[] nums2) {
    Set<Integer> set1 = new HashSet<>();
    Set<Integer> set2 = new HashSet<>();
    for(int num : nums1){
        set1.add(num);
    }
    for(int num : nums2){
        set2.add(num);
    }
    set1.retainAll(set2);
    return set1.stream().mapToInt(Integer::valueOf).toArray();
}

The third version uses the contains function, which is actually a hash function with O(1) time complexity. Its performance is better than that of the second version, but the cycle complexity does rise.

public static int[] intersection2(int[] nums1, int[] nums2) {
    Set<Integer> set1 = new HashSet<>();
    Set<Integer> intersectionSet = new HashSet<>();
    for(int num : nums1){
        set1.add(num);
    }
    for(int num : nums2){
        if(set1.contains(num)){
            intersectionSet.add(num);
        }
    }
    return intersectionSet.stream().mapToInt(Integer::valueOf).toArray();
}

The fourth edition, sorting + double pointer, this is the algorithm of the optimal solution, but the complexity increases greatly.

public static int[] intersection3(int[] nums1, int[] nums2) {
    Arrays.sort(nums1);
    Arrays.sort(nums2);
    int p1 = 0, p2 = 0, index = 0;
    int[] nums = new int[nums1.length + nums2.length];
    while (p1 < nums1.length && p2 < nums2.length) {
        if (nums1[p1] == nums2[p2]) {
            if (index == 0 || nums1[p1] != nums[index - 1]) {
                nums[index++] = nums1[p1];
            }
            p1++;
            p2++;
        } else if (nums1[p1] < nums2[p2]) {
            p1++;
        } else {
            p2++;
        }
    }
    return Arrays.copyOfRange(nums, 0, index);
}

Other recommendations

Try not to attach other business logic to the condition judgment, and the condition judgment shall be as clear as possible.

if(Long high && Handsome && rich){
	...
}

boolean tall, rich and handsome = Long high && Handsome && rich
if(tall, rich and handsome){
	...
}

Try to avoid anti logic

if(x < 100){
	...
}

if(!x >= 100){
	...
}

// Both of them indicate that when x < 100, it is executed, but the anti logic is more troublesome for people to understand.

Learn to wrap long lines of code

StringBuilder sb = new StringBuilder();
sb.append("a").append("b").append("c").append("d").append("e");

// Line feed with "."
sb.append("a").append("b")
        .append("c")
        .append("d")
        .append("e");


new User(String userName, String account, String age, String sex, String phone, String email)

// Line feed with ","
new User(String userName, String account, String age
         , String sex, String phone, String email)

Keywords: Java Design Pattern

Added by mwq27 on Mon, 29 Nov 2021 15:05:55 +0200