refactor

Original link: https://mengtnt.com/2023/03/20/refactor.html

Refactoring is an old-fashioned problem, and there are also a large number of books introducing refactoring methods. So how to refactor the code? How to ensure that the refactored code does not go wrong? How does refactored code prevent corruption? I first posted how ChatGPT answered, to feel her power.

How to refactor code

  1. Extract method: If you have a long or complex piece of code, you can extract it into a separate method, making it more readable and reusable.
  2. Rename variables and methods: Name variables and methods with descriptive names to make the code easier to understand.
  3. Remove duplicate code: Find duplicate code blocks and extract them into reusable methods.
  4. Simplify conditional statements: If a conditional statement is too complex, break it down into smaller, more manageable parts.
  5. Use a better data structure: If the data structure you are using is inefficient or does not meet your needs, consider using a better data structure.
  6. Break your code into modules: Break your code into smaller modules with specific responsibilities, each with its own set of tests.
  7. Remove dead code: Find code that is no longer used and remove it to simplify the codebase and make it easier to maintain.
    These are just a few examples of refactoring code. The key is to keep finding ways to improve your code to make it more maintainable over time.

The answers on ChatGPT are similar to those in many books and tutorials. In fact, sometimes the description text will be very abstract. Here I illustrate with an example from my own experience. Of course not the example given by ChatGPT ? .

Development of simple functions

This is a very common member list page display function. Let’s look at the first version first.

 classDiagram class MemberListViewController { -MemberListView view -loadView() } class MemberListView{ -renderView(List<MemberData> data) } class MemberData { -String name -Int status } MemberListViewController *-- MemberListView: 1..1 MemberListView ..> MemberData: 1..*

The following shows the calling process in pseudo-code.

 MemberListViewController :: loadView (){ RequestData (){ data in List < MemberData > dataList = List < MemberData > ( data ) view . render ( dataList ) } } MemberListViewController * vc = MemberListViewController () vc . loadView ()

In fact, the logic of this kind of UI development couldn’t be simpler, get the data and render the data to the corresponding UI components. Let’s take a look at the development process of the function.

The formation of bloated classes

With this function, different application scenarios are designed. For example, the data of service A should be obtained from scene A and then displayed, and the data of service B should be obtained from scene B…
Let’s first look at if you follow the original logic, you need to write a lot of code similar to the following.

 switch condition { case A : RequestAData (){ data in List < MemberData > dataList = List < MemberData > ( data ) view . render ( dataList ) } case B : RequestBData (){ data in List < MemberData > dataList = List < MemberData > ( data ) view . render ( dataList ) } }

At this time, you will find more and more data request and rendering codes in Switch Case . With more and more application scenarios, you will find that MemberListViewController class becomes larger and larger. How to refactor at this time?

An important principle of refactoring is used here: single responsibility. The simplified code is to split the large class of MemberListViewController . This class is responsible for rendering the view, and no longer responsible for data requests. Similar functions of network requests are gathered into another class, MemberListDataInteractor , which specializes in data acquisition, which can reduce the size of a single class. , for easy reading.

 classDiagram class MemberData { -String name -Int status } class MemberListDataInteractor { - List<MemberData> dataList - requestData(callback(List<MemberData> list)) } class MemberListViewController { -MemberListView View -MemberListDataInteractor interactor -loadView() } class MemberListView{ -View listView -renderView(List<MemberData> data) } MemberListViewController *-- MemberListView: 1..1 MemberListViewController *-- MemberListDataInteractor: 1..1 MemberListDataInteractor *-- MemberData: 1..*

The calling process at this time may be like this. The following pseudo code:

 MemberListViewController :: loadView () { interactor . requestData () { List < MemberData > list in MemberListView . renderView ( list ) } } MemberListViewController :: loadView () { interactor . requestData () { List < MemberData > list in MemberListView . renderView ( list ) } } MemberListViewController * vc = MemberListViewController () vc . loadView ()

This is actually a typical MVC structure. View rendering, data model, and model assembly are separated, and the core is the cohesion of different functions. Sometimes thinking about reading code is like reading an article. If you don’t divide it into paragraphs, just a 1,000-word paragraph, I believe many people don’t want to read it. The goal of the Single Responsibility Principle is to make people more willing to read your code without being intimidated at first glance.

Optimization of repeated code

With the evolution of functions, the user interface becomes more and more complex, requiring a large amount of data to be frequently rendered to the view. You will find a lot of code for view rendering assembly. This type of code is characterized by a high degree of similarity, but it is rendered to different views. At this time, DRY, another important principle of refactoring, does not write repetitive code, and it will work. We only need to abstract the repeated code to another layer to reduce a large number of similar codes.
This class can be called MemberListDataPresentor , which is specially used to assemble views. Just simply add the data structure of MemberViewModel to the view of MemberListView , and then MemberListDataPresentor is responsible for data assembly. Let’s look at this structure.

 classDiagram class MemberData { -String name -Int status } class MemberViewModel { -List<MemberData> dataList } class MemberListDataPresentor { -MemberListDataInteractor interactor - bind(MemberViewModel model,MemberListView view) } class MemberListViewController { -MemberListView View -MemberListDataPresentor presentor - loadView() } class MemberListView{ -View listView -renderView(MemberViewModel data) } MemberListDataPresentor *-- MemberViewModel : 1..1 MemberViewModel *-- MemberData: 1..* MemberListView *-- MemberViewModel: 1..1 MemberListViewController *-- MemberListDataPresentor: 1..1

When used in this way, as long as MemberListDataPresentor assembles the data, there is no need to call rendering, and it can be directly mapped to MemberListView through MemberViewModel . You can see the calling process in the pseudocode below.

 interactor . loadAllUserData () { List < MemberData > list in presentor . bindData ( list , listView ) }

This is actually the process of the evolution of the MVVM architecture. The DRY principle is not to write repetitive code, just like writing an article, no one wants to read the same text for a reason. Let’s continue to look at the development of this function.

lots of coupling

As the functions become more and more complex, you will find that the MemberListDataPresentor class will call more and more interfaces. It not only needs to call a large number of request interfaces MemberListDataInteractor to obtain data, but also needs to assemble various Model data, which will inevitably cause a large number of interface exposures. . The invocation of various complex relationships makes reading more and more difficult. At this time, one of the best principles of software engineering: any engineering problem can be solved by adding an intermediate layer. We need to add tool class decoupling here, and the essence of decoupling is split through tool classes. Make the dependency relationship into the following structure.

 classDiagram MemberListDataPresentor ..> ColdObserval MemberListDataInteractor ..> ColdObserval MemberListViewController ..> ColdObserval

Classes with a large number of interface dependencies can use a similar method to directly call each other.

 presentor . addObserval () { result in // bind data } interactor . postMessage ();

In fact, this is the use of observer mode to split. The advantage of observing mode is decoupling and reducing interface dependencies, so that when we want to define different presenter classes, we don’t have to rely on various specific interactors, we only need to listen to messages. For example, TaskQueue, an important thread tool class in WebRTC, is a good tool for decoupling and splitting. The codec, collection, and transmission are well decoupled and separated. With this foreshadowing, let’s finally look at how to expand the function.

Extend new features

Imagine that we need to add new functions to MemberListViewController view, not only to display the MemberListView view, but also to insert various other business views.

It is precisely because of the splitting of tool classes that all classes do not have any dependencies, and extensions are easy without exposing new interfaces. Specifically, it can be split horizontally through the proxy mode. Define the proxy class Plugin to be extended. As long as our new functions implement the interface functions defined by Plugin, all functions can be extended horizontally. Let’s look at the class structure.

 classDiagram class MemberListViewPlugin { -View subView -loadView() } class MemberListDataInteractorPlugin { -List<MemberData> dataList -requestData() } class MemberListDataPresentorPlugin { -List<MemberData> dataList -bindData() } MemberListDataPresentor *-- MemberListDataPresentorPlugin : 1..* MemberListDataInteractor *-- MemberListDataInteractorPlugin : 1..* MemberListView *-- MemberListViewPlugin : 1..*

Then our plugin calling process is as follows:

 MemberListDataPresentorPlugin * presentorPlugin = MemberListDataPresentorPlugin () presentor . registPlugin ( presentorPlugin ); MemberListDataInteractorPlugin * interactorPlugin = MemberListDataInteractorPlugin () interactor . registPlugin ( interactorPlugin )

In this way, every time a new function is added, there is no need to change any code of the original MemberListDataPresentor and MemberListDataInteractor , only the implementation of the plug-in needs to be added. This is actually the architectural pattern of many software plug-ins.

From the above example, we can see the evolution of the code, how to go from MVC to MVVM and finally plug-in. These processes make the code structure clearer and easier to read, and prevent code corruption.

Review of Refactoring

We summarize several key nodes in the above reconstruction process.

  1. When you find that a class is getting bigger and bigger.

    If it exceeds 1000 lines of code, it must be necessary to split the function.

  2. When you develop a function, you find that you need to change the interface of the original class a lot to achieve it, so we need to use tool classes to expose the ability to expand.
  3. When a class needs to be changed frequently when new functionality is added.

    At this time, you need to use proxy mode plug-ins to extend your class, so that you can avoid a lot of modification logic and ensure code stability. For example, some pluggable plug-in systems we often see are implemented in this way.

  4. Be especially careful when there are too many performance-optimized codes, and try not to expose them, because performance-optimized codes are often less readable.

    For performance-optimized code, when refactoring, try to encapsulate it as an internal function instead of exposing it to external use. For example, a class that defines a Cache resource. In order to optimize memory, it is best not to expose the API to the outside, and it is best to digest it internally.

  5. Find useless function codes and delete them in time to prevent further corruption

    As a result of not deleting in time, you will find that the new function calls the method of the previously removed function class. At this time, when you want to delete the old function code, you will find that you want to cry but have no tears.

Compared with the summary of ChatGPT reconstruction, the general principle is the same, but it will be more specific. Finally, I would like to talk about some principles on how to ensure stability when refactoring code. I can sum it up by walking fast in small steps to ensure stability. During the refactoring process, a certain amount of redundant code is allowed to increase the grayscale capability. When a problem is found, it can be rolled back in time and wait for the refactored code to be tested and then delete it.

The code of many excellent open source projects not only has high requirements on code performance, but also code quality and maintainability. Reading it is like admiring beautiful poems. A mountain of shit never produces great work. I believe that every good programmer is unwilling to turn his code into a mountain of shit, but Rome cannot be built in a day, and learning to refactor is a necessary skill.

This article is transferred from: https://mengtnt.com/2023/03/20/refactor.html
This site is only for collection, and the copyright belongs to the original author.