Day936. How to refactor too large class - system refactoring in practice

How to refactor oversized classes

Hi, I am 阿昌, what I learned to record today is about 如何重构过大类content.

In the past code, you will definitely encounter a typical bad code smell, that is " 过大类".

In the process of product iteration, due to the lack of norms and guards, a single class is easy to expand rapidly, and some even reach tens of thousands of lines. A class that is too large will lead to divergent modification problems. As long as the requirements change, the class must be modified accordingly.

That's why there is sometimes a "last resort" approach: in order not to cause new problems due to modification, the function is extended by copying and pasting.


1. Typical problems of "too large category"

The most common case of " too large category所有的业务逻辑都写在同一个界面 " is to be included.

Take a look at the sample code that follows.

public class LoginActivity extends AppCompatActivity {
    
    
    
    //省略相关代码... ...
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        loginButton.setOnClickListener(v -> {
    
    
            String username = usernameEditText.getText().toString();
            String password = passwordEditText.getText().toString();
             //用户登录
            LogUtils.log("login...", username);
            try {
    
    
                //验证账号及密码
                if (isValid(username) || isValid(password)) {
    
    
                    callBack.filed("invalid");
                    return;
                }        
                //通过服务器判断账户及密码的有效性x
                boolean result = checkFromServer(username, password);
                if (result) {
    
    
                    UserController.isLogin = true;
                    UserController.currentUserInfo = new UserInfo();
                    UserController.currentUserInfo.username = username;
                    //登录成功保持本地的信息
                    SharedPreferencesUtils.put(this, username, password);
                } else {
    
    
                    Log.d("login failed");
                }
            } catch (NetworkErrorException networkErrorException) {
    
    
                Log.d("networkErrorException");
            }
        });
    }
    private static boolean isValid(String str) {
    
    
        if (str == null || TextUtils.isEmpty(str)) {
    
    
            return false;
        }
        return true;
    }
    private boolean checkFromServer(String username, String password) {
    
    
        //通过网络请求服务数据
        String result = httpUtil.post(username, password);
        //解析Json对象
        try {
    
    
            JSONObject jsonObject = new JSONObject(result);
            return jsonObject.getBoolean("result");
        } catch (JSONException e) {
    
    
            e.printStackTrace();
        }
        return false;
    }
    public static final String FILE_NAME = "share_data";
    public static void put(Context context, String key, Object object) {
    
    
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
                Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
       //... ...
        editor.apply();
    }

  //省略相关代码... ...
  
}

As can be seen from the sample code above, after the data is initialized when the page is created, when the user clicks the login button to trigger the data verification, the correctness of the data is verified through the network request, and finally the local continuous data storage is performed.

The login page not only carries the initialization and management of UI controls, but also needs to be responsible for login network requests, data verification and result processing, and persistent storage of data.

If the demand for so many products needs to increase now, how should the code be modified for function expansion?

  • UI 上要做一些优化, when the login fails, a prompt box should pop up to remind the user.
  • Data storage needs to be upgraded, all 数据要存储到数据库in.
  • Username rules are upgraded to only support phone and email formats, which need to be in 本地做校验.

It can be seen that based on this design, whether there is a change in UI or verification rules, or a change in data persistence or network framework, the login page needs to be modified.

When 大量的逻辑耦合taken together, without any automated test guards, the risk of modifying the code is greatly increased. Moreover, if you continue to add new features based on this code, you will fall into an endless loop where the code is getting worse and worse, but you are less and less afraid to modify the code.


2. Refactoring strategy

With the continuous expansion of business requirements and code size, the refactoring strategy for overly large categories is to divide and conquer .

By 分层controlling changes in different dimensions in independent boundaries, they can evolve independently, thereby reducing the impact on each other when modifying code.

From the previous examples, typical change scenarios in three different dimensions can be identified:

  • The first is a change in the UI;
    • Changes on the UI, such as theme or layout design, will not affect the data business. If there is any change at this time, 独立的 UI 层the impact on other logic codes can be reduced when expanding and modifying. Generally, in the common layered architecture mode, there will be an independent View layer to carry independent UI changes.
  • The second is the change of business data logic;
    • The same is true for changes in business data logic. Some data verification, calculation, and assembly rules are also dimensions that are prone to change. Also available in common layered architectures 独立的业务逻辑处理层.
  • The third dimension is the change of the infrastructure framework.
    • Infrastructure frameworks, such as persistent frameworks, may evolve from lightweight configuration storage requirements in the early stage to database storage; network request frameworks may be replaced with new frameworks as the technology stack is upgraded. If at this time all calls to the infrastructure are scattered on the entrances of various UIs, the cost of modifying changes will be very high.

Let's take MVPthe layered architecture (Model-View-Presenter) as an example to see how the MVP architecture performs layered design and interaction.

In the MVP pattern, the model layer provides data, the view layer is responsible for display, and the presentation layer is responsible for logic processing.

insert image description here

The MVP architecture will define corresponding interfaces during the interaction process between the view layer and the presentation layer to make the dependencies between them more stable.

Since the model is completely separated from the view, the view can be modified without affecting the model.

It is also possible to use one presentation layer for multiple views without changing the logic of the presentation layer. This feature is very useful because views always change more frequently than models.

In addition, the use of interface dependencies can better improve the testability of the code. For example, when testing the presentation layer 分层测试, it is only necessary to verify whether the interface of the view layer is called normally.

A single layer of responsibility makes writing automated tests much easier than testing methods of hundreds of lines.


Taking the new requirement above as an example, you can refer to this table for the code extension method after refactoring.

insert image description here

It can be seen that 分而治之the strategy will change the demand 隔离在了不同的分层, so that the demand change is only within a controllable boundary, yes 减少相互影响.


3. Refactoring process

Going back to the question raised at the beginning, how to complete the reconstruction of the layered architecture within the component more efficiently and with higher quality?

The reconstruction process of the layered architecture within the component is divided into 7 steps according to 3 dimensions.

insert image description here


1. Business analysis

For legacy systems, the more common problem is that there are gaps in the context of requirements, so the first step is 尽可能地了解、分析原有的业务需求.

Only by digging out the original requirement design more clearly will there be no wrong code adjustments due to differences in understanding.

Refer to 3 common ways to understand requirements.

  • The first way is to find people : through the relevant stakeholders (such as product managers, designers, testers) 沟通, to confirm the requirements and answer questions, this is the most direct and effective way.
  • The second way is to read the document , but sometimes you will find that if there is a large turnover of personnel, the relevant stakeholders may not know the original design. At this time, you can refer to the method of reading the document. You can better understand the original requirements by viewing related documents (such as viewing the original requirements documents, design documents, test cases, and design drafts). Of course, there may also be problems with no documentation or outdated content of the documentation.
  • The third method - look at the code . The code must reflect the latest code requirements. If there is automated test code, the input and output of test cases can also be used to assist in understanding the requirements. Generally, you can start from the top-level UI page code, and gradually view the relevant logic according to the call stack of the code. Generally speaking, there are two important scenarios to clarify in the business analysis step:
    • The first one is the normal use scenario of the user;
    • The second is the usage scenario where the user is abnormal. These scenarios will be an important input for subsequent supplementary automated acceptance tests. Still taking the previous login code as an example, the normal use scenarios of users should include:
      • Enter the correct account password and click Login to verify normally.
      • If you enter an incorrect account password, click Login to prompt failure.
      • .……
    • Unusual usage scenarios should include:
      • When the user clicks to log in, but because the mobile phone has a network exception, it needs to prompt the network exception.
      • When the user clicks to log in, but the server returns an abnormal error, the corresponding error code needs to be prompted.
      • ……

2. Code Analysis

After business analysis is code analysis. Through this step, on the one hand, we need to understand the original business, and on the other hand, we need to diagnose the optimization points in the existing code.

Usually, in addition to obvious problems such as "too large classes", there may also be problems such as code specifications, method complexity, circular dependencies, and potential code vulnerabilities.

These problems need to be identified as much as possible as input for subsequent refactoring.


Recommend several commonly used class inspection tools.

  • The first is Lint . Lint is a code scanning analysis tool that comes with Android Studio, which can help us find code structure or quality problems. Each problem found by Lint has description information and a level, so we can easily locate the problem and solve it according to the severity.

  • The second is Sonar . Sonar also provides SonarLint as an IDE plug-in. This plugin can help us identify basic bad smells, code complexity, and potential defects in the code. Regarding the use of Lint, you only need to select the Code->Inspect Code menu in your project and run the inspection, and you can view the specific problem list in the Problems window.

insert image description here

Regarding the SonarLint plugin, you need to search and install the plugin from the IDE first. After the installation is successful, right-click the mouse and select "Analyze with SonarLint" in the menu bar to trigger the scan.

A list of specific issues can be viewed in the SonarLint window.

insert image description here

In this step, it is recommended that at least the Error-level problems detected by the tool be included in the refactoring modification, especially some classes and methods with high cyclomatic complexity, which can be recorded in focus. These are the contents that need to be focused on in subsequent refactorings .


3. Supplementary automated acceptance testing

After the previous business analysis and code analysis, let's look at the third step, which is to supplement the automated acceptance test for the user scenarios sorted out by the first step of business analysis.

Why do we need to supplement automated acceptance testing first?

Because only with the coverage of the test, when the fifth step is to carry out small-step security refactoring, these tests can be frequently used to verify whether the refactoring has damaged the original business logic, so that it can better discover and reduce problems caused by refactoring. Modify the code to cause new problems.

This step usually covers medium and large automated tests, which can be done with the help of Espresso or Robolectric framework .

For example, in the previous login example, I will sort out the user scenarios and turn them into automated acceptance test cases.

public class LoginActivityTest{
    
    
  public void should_login_sucees_when_input_correct_username_and_password(){
    
    //... ...}
  public void should_login_failed_when_input_error_username_and_password(){
    
    //... ...}
  public void should_show_network_error_tip_when_current_network_is_exception(){
    
    //... ...}
  public void should_show_error_code_when_server_is_error(){
    
    //... ...}
  //... ...
}

Note that this step needs to cover all the business analysis scenarios in the previous step, and all use cases need to be executed.


4. Simple design

After supplementing the automated acceptance tests, the next step is to carry out "simple design".

This step allows us to think about what the refactored code will look like before we start to refactor. Only by starting with the end can we make our goals clearer and the process more measurable.

I often hear a half-joking saying that "the code becomes another legacy system after refactoring". In fact, this is probably because we did not design first and lacked a clear refactoring goal.

So how to do this step?

According to the selected architectural pattern, the core classes, interfaces and data models can be defined, and these key elements can support the entire architectural pattern.


Taking the login as an example, assuming that you want to refactor into an MVP architecture, the first thing is to design the overall core class.

//View
public class LoginActivity implement LoginContract.LoginView 

//Presenter
public class LoginPresenter 

//Model
public class UserInfo 

The second is the core interactive interface.

//interface
public interface LoginContract {
    
    
 interface LoginView  {
    
    
    success(UserInfo userInfo);
    failed(String errorMessage);
  }
}

Through the simple design step, it is necessary to define the core classes, interfaces and data models that support the future architecture.


5. Small step security reconstruction

Next comes small step security refactoring.

In the process of refactoring, it is necessary to maximize the safe refactoring methods of the five types of typical code smells of legacy systems , reduce the frequency of manual direct code modification, make submissions in small steps as much as possible, and use tests for frequent verification. Gradually modify the code to the newly designed architectural pattern.

This can not only improve the efficiency of refactoring, but also effectively avoid potential errors caused by manual code movement through automation.

In performing this step, there are 3 key points that require special attention.

  • The first is 小步to break down the entire refactoring into small steps, such as moving business logic to the Presenter class or replacing the original View implementation with an interface callback in one refactoring. After each small refactoring, it can be saved through the version management tool, so that we can roll back the code in time.
  • The second is 频繁运行测试. Every time a small refactoring is completed, the test needs to be executed frequently. If there is an exception in the test at this time, it proves that our refactoring has destroyed the original function and needs to be checked. Through such feedback, we can detect problems earlier and deal with them in a timely manner.
  • The third is 使用 IDE 的安全重构功能. Using automated refactoring can effectively reduce the risk of artificially modifying the code, and the efficiency will be higher. In this step, all the code needs to be refactored according to the design in the fourth step, and all the code refactoring must be completed, and all the automated acceptance tests written must be guaranteed to pass.

6. Supplementary small and medium-sized tests

After the refactoring is completed, the code is more testable at this time, and it is the best time to supplement small and medium-sized tests.

By supplementing use cases, the refactored code logic can be solidified to prevent subsequent code logic from being destroyed.

In addition, the execution time of small and medium-sized automated tests is faster, and problems can be fed back in advance.

Generally speaking, in this step, the newly added classes after refactoring should be supplemented with tests. Still taking the previous login as an example, a new LoginPresenter class is added after refactoring, so it is necessary to conduct more fine-grained tests on the login method inside, covering the finer branch conditions and exception conditions inside the method.

As demonstrated in the following code, it is necessary to supplement the verification of username and password and the small test of simulating Exception.

class LoginPresenter{
    
    
  boolean booleanString username,String password){
    
    
    if(isValid(username)|| isValid(password)){
    
    
      return false}
    try{
    
    
       XXX.login(username,password);
    }catchNetWorkException e)
    {
    
    
      //... ...
    }
  }
}

In this step, test coverage tools can be used to check whether the core business logic of the refactored code has coverage tests.

Of course, we don't necessarily require 100% coverage here, and we need to combine business and code for evaluation.


7. Integrated acceptance

It is the result of the final inspection of the entire refactoring. Only when it is integrated can the refactoring be truly completed.

In this step, it is not only necessary to ensure that the refactored code passes independent compilation and debugging, but also that all automated tests and integration acceptance tests can also pass.

Generally speaking, if the previous 6 steps are done well, then the final integration stage should not have too many problems.

This is also often referred to as "quality built-in". Although the investment has been increased in the front, it can effectively reduce the rework in the later stage.

In the actual process, care should be taken to avoid refactoring branches with a long life cycle, otherwise there may be a large number of code conflicts in the final integration.

In addition, medium and large-scale refactoring should also split tasks reasonably, so that every small step of refactoring can meet the integration conditions.

If the quality of the process is done well, in fact, I think a better way is to refactor directly based on the trunk to avoid long-term refactoring branches.


Four. Summary

The refactoring process and key points are summarized in the following figure.

  • The two steps in the analysis phase allow the beginning to end to gain an in-depth understanding of the requirements and the status of the code;

  • The four steps in the refactoring phase enable code adjustments to be completed more safely and efficiently;

  • The acceptance phase reminds us that only integration is the real completion of refactoring.

insert image description here

“Talk is cheap, show me the code”


Guess you like

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