Day930. Typical bad code smells of five types of legacy systems - system refactoring in practice

Five typical code smells of legacy systems

Hi, I am 阿昌, what I learned to record today is about 五类遗留系统典型的代码坏味道content.

In many legacy systems encountered in the past, there are some similar problems in the code, of which there are five most typical ones:

  • excessive nesting
  • Duplicate code
  • Invalid code and resources
  • Lack of abstractions and arbitrary dependencies

The embankment of a thousand miles collapses in an ant's nest. The legacy system is not created in a day, but is accumulated in daily development. These five typical code smells are actually important culprits that promote the evolution of the system into a legacy system.

So, before refactoring your project's code, let's go through some examples to see the impact of these code smells and how to solve them.


1. Excessive nesting

Excessive nesting refers to the circle of code 复杂度过高, and there is a large amount of nested logic, which is not convenient for developers to maintain and expand functions.

First of all, please think about it, how high is the cyclomatic complexity? How much is low? The suggestion given by the concept of cyclomatic complexity 麦凯布is:

  • If the cyclomatic complexity of a module exceeds 10, it needs to be divided into smaller modules. According to this threshold, if the cyclomatic complexity exceeds 10, the cyclomatic complexity is considered too high;

  • Acceptable if considered moderate between 5-10;

  • If it is lower than 5, it is considered that the cyclomatic complexity is relatively low.

Let's take a look at a piece of code with a cyclomatic complexity of more than 10. You can take a closer look at what it feels like to read this code.

public void case0(){
    
    
    boolean A = true,B=true,C=false,D=true,E=true,F=false,G=true,H=false,I=true,J=false;
    if(A){
    
    
        System.out.println("A");
        if(B){
    
    
            System.out.println("B");
            if(C){
    
    
                System.out.println("C");
            }else if(D){
    
    
                System.out.println("D");
                if(E){
    
    
                    System.out.println("E");
                }
            }
        }else if(F){
    
    
            System.out.println("F");
            if(G){
    
    
                System.out.println("G");
            }
        }
    }else if(H){
    
    
        System.out.println("H");
        if(I){
    
    
            System.out.println("I");
            if(J){
    
    
                System.out.println("J");
            }
        }
    }
}

Do you feel that the logic of this code is nested layer by layer, and the readability is poor?

In fact, this is a major problem caused by excessive nesting:代码阅读性差,不方便维护。

In addition, this kind of code is also very error-prone to modify, and a little carelessness may destroy the previous logic.


So how do you simplify overly nested code?

The best way is to level the logic without switching contexts back and forth in the branch.

There are several ways to level logic, let's look at some examples.

boolean isA,isB,isC;
double getAmount() {
    
    
    double result;
    if (isA) result = adAmount();
    else {
    
    
        if (isB) result = bAmount();
        else {
    
    
            if (isC) result = cAmount();
            else result = otherAmount();
        };
    }
    return result;
}

For the above example, you can use "Return in advance" to level the nesting logic of this example (as shown below), and you can compare the reading experience of the upper and lower codes.

double getAmountRefactor() {
    
    
    double result;
    if (isA) return adAmount();
    if (isB) return bAmount();
    if (isC) return cAmount();
    return otherAmount();
}

Another common way to simplify nested logic is “使用多态 + 路由表”, for example, the following example.

public void login(String type) {
    
    
    if ("wechat".equals(type)) {
    
    
        weChatLogin();
    } else if ("qq".equals(type)) {
    
    
        qqLogin();
    } else if ("phone".equals(type)) {
    
    
        phoneLogin();
    } 
}

For such a situation, you can extract the interface, distinguish each implementation, and then obtain the specific implementation through routing configuration (as shown below).

This method not only simplifies nesting, but also facilitates subsequent code expansion.

The following is the optimized code, let's make a comparison.

HashMap<String,Ilogin> maps = new HashMap<String, Ilogin>(){
    
    
    {
    
    
        put("wechat", new WeChatLogin());
        put("qq", new QQChatLogin());
        put("phone",new PhoneChatLogin());
    }
};

public void login(String type) {
    
    
   maps.get(type).login();
}

Prevention in advance is far better than solution after the event. Excessive code nesting should be avoided in normal project development.

Therefore, it is recommended to do it before the code is merged 静态代码扫描. On the scanning tool 设置合适的圈复杂度阈值(less than 10), once the threshold is found to be exceeded, an error will be prompted and the code will not be merged into the library.

For the selection of scanning tools, Sonarit is recommended to use Sonar as a quality access control into the pipeline to check the cyclomatic complexity of the code.

If the project has some constraints that cannot be used, it can also be used to SonarLint 插件scan and check in the IDE.

insert image description here


2. Duplicate code

Duplicate code means that there are more than two places in the whole project with the same line of code, the same part is as little as 3-5 lines of code, and as many as it may be, except for 2 classes with a small part of the logic, the rest are the same code .

Generally speaking, duplicate code is mostly caused by copying and pasting, so how to solve it?

Let's look at two examples.

The first is duplication of lines of code for parts.

public class DuplicateCodeCopyCase {
    
    
    String name;
    String password;
    public void login(){
    
    
        if(name == null){
    
    
            return;
        }
        if(password == null){
    
    
            return;
        }
        phoneLogin();
    }
    public void Register(){
    
    
        if(name == null){
    
    
            return;
        }
        if(password == null){
    
    
            return;
        }
        phoneRegister();
    }
    private void phoneLogin() {
    
    
    }
    private void phoneRegister() {
    
    
    }
}

Common logic can be used 提取成公共的方法to reduce duplication of code.

In the above example, the judgment of name and password can be extracted into a public method, as shown below.

private boolean isInValid() {
    
    
    if (name == null) {
    
    
        return true;
    }
    if (password == null) {
    
    
        return true;
    }
    return false;
}

And for the case where most of the code is repeated and only a small part is different, let's look at another example.

public class DuplicateCodeCopyCase {
    
    
    String name;
    String password;
    public void login(){
    
    
        if (isInValid()) return;
        phoneLogin();
    }
    private boolean isInValid() {
    
    
        if (name == null) {
    
    
            return true;
        }
        if (password == null) {
    
    
            return true;
        }
        return false;
    }
    public void Register(){
    
    
        if (isInValid()) return;
        phoneRegister();
    }
    private void phoneLogin() {
    
    
    }
    private void phoneRegister() {
    
    
    }
}

At this time, you can combine the difference parts or combine the common parts 提取为超类to reduce duplication of code.

In the above example, different ways of login and registration can be extracted.

public class DuplicateCodeCopyCase {
    
    
    //将差异的实现通过接口注入进来
    IAccountOperator iAccountOperator;
   
    String name;
    String password;
    public void login(){
    
    
        if (isInValid()) return;
        iAccountOperator.login();
    }
    
    public void Register(){
    
    
        if (isInValid()) return;
        iAccountOperator.register();
    }
    //... ...
   }

Because repeated codes often have to modify many different classes when encountering changes, the workload of maintaining the code increases exponentially, so this kind of problem should be avoided in the project in advance.

Similarly, it is recommended to perform static code scanning before code merging, and set an appropriate code repetition rate threshold (generally recommended to be less than 5%) on the scanning tool. If the threshold is found to be exceeded, an error will be prompted to prevent the code from being merged into the library.

insert image description here

In daily development, you can also use the IDE to scan for duplicate code in the development stage in advance, and optimize it in time.

For example, using Locate Duplicatesthe function , select: Code→Analyze Code→Located Duplicates, you can scan, and the scanning result is as follows.

insert image description here

It should be noted that because the latest version of Android Studio does not support the Locate Duplicates function, Intellij is used to use this function.


3. Invalid code and resources

Invalid code and resources refer to class, method, variable or resource problems that are not called by any code in the entire project.

Generally speaking, the compilation tool will automatically remove these invalid codes when packaging, which will not increase the package size of the application. But invalid code and resources will still exist when you write code, which will increase the cost of understanding the code and reduce the maintainability of the code. This can be detected and removed automatically with the aid of tools.

Let's first look at the processing of invalid code. The steps are very simple, that is, select Analyze in Android Studio, then select the command run inspection by name, and then enter unused declaration to scan the code.
insert image description here

According to the scanning result, if the scanning result shows that the code is an invalid code, delete it through Safe delete.
insert image description here
For application resources, you can select the corresponding module in Android Studio, select the refactoring menu, and then select Remove Unused Resources to scan.

insert image description here

According to the scanning result, if the scanned resources are invalid resources, they can be deleted through Do Refactor.

insert image description here

In particular, it should be noted that there is another common situation in the project “僵尸代码”that this type of code is characterized by: the code has references, but from the perspective of the system, the logic of these codes will never be triggered. Usually, it is impossible to identify zombie code simply through tools, and it needs to be analyzed in combination with specific business.

Compared with invalid code, zombie code increases the cost of code understanding and has a greater impact on code maintainability, so it must be timely 定期清理.

Generally speaking, for invalid codes and resources, you can save them 静态代码扫描工具(Sonar、Lint 等)加入到流水线中及时检查. For zombie code, you need 加强人工的代码监视to deal with it.


4. Lack of abstraction

The code problem is called " missing abstraction ". It is more common in projects to write all the logic in one interface, which includes UI operations, business data processing, network operations, data caching, and so on.

This is the case for the code testability example in How to Improve the Testability of Legacy System Code .

public class LoginActivity extends AppCompatActivity {
    
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        final EditText usernameEditText = findViewById(R.id.username);
        final EditText passwordEditText = findViewById(R.id.password);
        final Button loginButton = findViewById(R.id.login);
        loginButton.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                boolean isLoginSuccess = false;
                String username = usernameEditText.getText().toString();
                String password = passwordEditText.getText().toString();
                boolean isUserNameValid;
                if (username == null) {
    
    
                    isUserNameValid = false;
                } else {
    
    
                    Pattern pattern = Pattern.compile("\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*");
                    Matcher matcher = pattern.matcher(username);
                    if (username.contains("@")) {
    
    
                        isUserNameValid = matcher.matches();
                    } else {
    
    
                        isUserNameValid = !username.trim().isEmpty();
                    }
                }
                if (!isUserNameValid || !(password != null && password.trim().length() > 5)) {
    
    
                    isLoginSuccess = false;
                } else {
    
    
                    //通过服务器判断账户及密码的有效性
                    if (username.equals("[email protected]") && password.equals("123456")) {
    
    
                        //登录成功保存本地的信息
                        SharedPreferencesUtils.put(LoginActivity.this, username, password);
                        isLoginSuccess = true;
                    }
                }
                if (isLoginSuccess) {
    
    
                    //登录成功跳转主界面
                    startActivity(new Intent(LoginActivity.this, MainActivity.class));
                } else {
    
    
                    //对登录失败进行提示
                    Toast.makeText(LoginActivity.this, "login failed", Toast.LENGTH_LONG).show();
                }
            }
        });
    }
}

This kind of code lacking abstract design has poor scalability, and all requirements changes must be modified in one class, which is prone to errors.

The solution to this problem is also very clear, that is 分而治之, will 不同维度的代码独立开来,使其职责更加单一,这样有需求变化时就能独立演进了.

If you want to avoid this kind of code that lacks abstract design in advance, you Sonarcan check the large class and large method before the code is put into the library.


5. Reliance at will

Random dependency直接依赖 refers to the specific implementation of different layers of code in the project , resulting in coupling between them.

It is more common that different business modules are directly dependent, or some underlying components depend on the implementation of upper-level business modules.

“低耦合高内聚”It is a design idea that is often mentioned, because the low degree of code coupling can reduce the risk caused by modifying the code, and it is also one of the important conditions for componentization.

If the code is directly coupled, after splitting into independent modules, the compilation will not pass directly. There are two situations to solve this kind of "random dependence" problem.


The first is 底层组件依赖上层业务模块the implementation, for example, some log tool classes will directly depend on the personal data of some user modules.

public class LogCodeCase {
    
    
    public void log(){
    
    
        //... ...
        Log.d("log",User.id);
    }
}

In this regard, a better solution is to remove specific dependencies by extracting parameters or injecting constructors.

public class LogCodeCase {
    
    
    String id;
    public LogCodeCase(String id) {
    
    
        this.id = id;
    }
    
    public void log() {
    
    
        //... ...
        Log.d("log", id);
    }
}

Another situation is 业务模块之间的直接依赖, for example, that the message module directly depends on the file distribution of the file module.

public void sendMessage(String text){
    
    
    //依赖具体的实现
    String url=new FileManager().upload();
    send(url,text);
}

At this time, you can extract the interface, 依赖稳定的抽象接口来进行解耦.

After decoupling, inject the implementation of the interface into the calling class of the interface through the injection framework.

IUpload iUpload;
public void sendMessage(String text) {
    
    
    String url = iUpload.upload();
    send(url, text);
}

In order to avoid randomly dependent code, the guard tool ArchUnit can also be used to check the architectural constraints before the code is merged.


6. Summary

If you don't accumulate steps, you can reach thousands of miles. Only by paying attention to basic code specifications and code quality in daily development can you more effectively avoid the generation of legacy systems.

Five typical bad code smells common in legacy systems, including excessive nesting, repetitive code, invalid code and resources, lack of abstraction and random dependencies, can refer to the sorted table.

insert image description here


Guess you like

Origin blog.csdn.net/qq_43284469/article/details/129846811