0 x0, introduction

On the first day after the festival, this paper is a concentrated summary of the specification and reconstruction (15-33). The actual combat part (34-37) is divided into the next section. This part is mainly some coding suggestions and specifications.

Secondary knowledge processing inevitably has mistakes, interested in the time can refer to the original, thank you.


0x1. Reconstruct four questions

(1) The purpose of reconstruction → Why?

Software design guru Martin Fowler defines refactoring as:

Refactoring is an improvement to the internal structure of software to make it easier to understand and less costly to modify without changing its visible behavior.

Refactoring can be thought of as:

On the premise of keeping the function unchanged, the use of design ideas, principles, patterns, programming norms and other theories to optimize the code, modify the deficiencies in the design, improve the quality of the code.

Why code refactoring?

  • An effective way to keep code quality at all times, without letting it rot beyond redemption;
  • Good code or architecture is iterative and cannot predict 100% of future requirements. Refactoring is inevitable as the system evolves.
  • It is an effective means to avoid over-design. The code can be reconstructed when there are real problems in the process of maintaining the code, which can effectively avoid spending a lot of time on over-design in the early stage.
  • It is of great significance to the growth of engineers’ own technology, and it is a good training ground to apply the theoretical knowledge learned to practice.

Junior engineers maintain the code, senior engineers design the code, and senior engineers refactor the code (finding problems and keeping code quality under control).


② The object of reconstruction → What is reconstructed?

According to the scale of reconstruction, it can be generally divided into large-scale high-level reconstruction and small-scale low-level reconstruction, referred to as large-scale and small-scale reconstruction.

Large-scale refactoring

The reconstruction of top-level code design → system, module, code structure, and the relationship between classes, etc., means: stratification, modularization, decoupling, abstract reusable components, etc. Tools: Design ideas, principles, and patterns. It involves a lot of changes in the code, has a large impact, is difficult, takes a long time, and has a large risk of introducing bugs.

Small refactoring

Reconstruction of code details → class, function, variable and other code-level reconstruction, such as: standard naming, standard annotation, elimination of oversized classes or functions, extraction of duplicate code, etc. The main means is: coding specification. The modification place is more centralized, relatively simple, maneuverable, time-consuming, and the risk of introducing bugs is relatively small.


③ When to refactor → When?

Not bad to a certain degree to refactor code, such as promoting continuous refactoring, in my spare time to see what is written in the project under the code is not good enough, can be optimized, the initiative to refactoring, or modify, add a function code shoving the refactoring is not in conformity with the coding standards, poor design, is to have the consciousness of continuous refactoring.


④ Reconstruction method → How to reconstruct (How)?

Large-scale refactoring

Complete the reconstruction plan in advance and carry it out in stages. Each stage only completes a small part of the code reconstruction. Then submit, test and run it.

At each stage, control the scope of the code affected by the refactoring, consider how to accommodate older code logic, and write compatible transition code if necessary. This is the only way to ensure that each phase of the refactoring doesn’t take too long (ideally in a single day) and doesn’t conflict with new feature development.

Large refactoring must be organized, planned, and careful, with experienced, business-savvy senior colleagues taking the lead.

Small refactoring

In addition to manually finding low-level code quality problems, you can also use mature static code analysis tools (CheckStyle, FindBugs, PMD, etc.) to automatically find problems in your code and then optimize them for specific refactoring.

As for the refactoring, senior engineers and team leaders should take responsibility for refactoring the code whenever possible to keep the quality of the code in a good state. Otherwise, if you have a “broken window” effect, one person puts some bad code in there, then more people will put more bad code in there, because the cost of putting bad code in there is too low. The best way to maintain code quality is to create a technical climate that drives people to actively focus on code quality and constantly refactor code.


0x2. How to Ensure error-free reconstruction

To make refactoring error-free, you need to be familiar with design principles, ideas, patterns, and a good understanding of the business and code being refactored. Apart from these personal factors, the most implementable and effective technique for ensuring that refactoring is error-free is Unit Testing.

Unit testing and integration testing

  • Unit test is written by r&d engineers to test the correctness of the code they wrote. The test object is a class or function. Whether the test is executed according to the expected logic is tested.
  • Integration Testing is an End To End test for the whole system or a functional module, such as Testing whether user registration and login functions are normal.

â‘¡ Unit test writing example

import java.util.regex.Pattern;

public class Text {
    private String content;
    private final Pattern pattern = Pattern.compile("[0-9] *");

    public Text(String content) {
        this.content = content;
    }

    public Integer toInt(a) {
        if (content == null || content.isEmpty()) return null;
        String temp = content.replace(""."");
        if(pattern.matcher(temp).matches()) {
            return Integer.parseInt(temp);
        }
        return null; }}Copy the code

For example, to test the toInt() method of the Text class above, first design the test case (input → expected output) :

  • “123-123
  • Null or empty string → NULL
  • “123 “, “123 “, “123 “, “123 “, “123 “, “123 “→ 123
  • “123a”, “1*23”, “ABC” → null
  • “1234567890-1234567890

Use case design is more of a test of how thoughtful a programmer is to come up with test cases that cover all kinds of normal/abnormal situations to ensure that the code works in any expected or unexpected situation. Once the use case is written, it is then time to translate it to do with it (there is no test framework here)

// Result validation class
public class Assert {
    public static void assertEquals(Integer expectedValue, Integer actualValue) {
        if(actualValue.intValue() ! = expectedValue.intValue()) { System.out.println(String.format("Test failed: Expected: %d, actual: %d", expectedValue, actualValue));
        } else {
            System.out.println("Test successful"); }}public static boolean assertNull(Integer actualValue) {
        boolean isNull = actualValue == null;
        if (isNull) {
            System.out.println("Test successful");
        } else {
            System.out.println("Test failed, actual value not null:" + actualValue);
        }
        returnisNull; }}// Test case class
public class TextTest {
    public void testToNumber(a) {
        Assert.assertEquals(123.new Text("123").toInt());
    }

    public void testToNumber_nullOrEmpty(a) {
        Assert.assertNull(null);
        Assert.assertNull(new Text("").toInt());
    }

    public void testToNumber_containsSpace(a) {
        Assert.assertEquals(123.new Text("123").toInt());
        Assert.assertEquals(123.new Text("123").toInt());
        Assert.assertEquals(123.new Text("123").toInt());
        Assert.assertEquals(123.new Text(23 "1").toInt());
        Assert.assertEquals(123.new Text("1 2 3").toInt());
        Assert.assertEquals(123.new Text(" 1 2 3 ").toInt());
    }

    public void testToNumber_containsInvalidCharacters(a) {
        Assert.assertNull(new Text("123a").toInt());
        Assert.assertNull(new Text("1 * 23").toInt());
        Assert.assertNull(new Text("abc").toInt());
    }

    public void testToNumber_large(a) {
        Assert.assertEquals(Integer.MAX_VALUE, new Text(""+ Integer.MAX_VALUE).toInt()); }}// Run the use case class
public class TestCaseRunner {
    public static void main(String[] args) {
        TextTest test = newTextTest(); test.testToNumber(); test.testToNumber_nullOrEmpty(); test.testToNumber_containsSpace(); test.testToNumber_containsInvalidCharacters(); test.testToNumber_large(); }}Copy the code

The test results are as follows:

From the above example, we can conclude that writing unit tests is the process of designing test cases for code that cover various inputs, exceptions, and boundary conditions, and translating use cases into code.

In addition, unit testing frameworks (such as JUnit, TESTNGINX, Spring Test, etc.) can be used to simplify the writing of Test code when translating code.


â‘¢ Why write unit tests

  • Help you find bugs in the code (save the time to fix low-level bugs, write bug-free code is one of the important criteria to judge the coding ability of engineers);
  • Help you find problems in code design (testability of code is an important criterion of code quality, difficulty in writing unit tests generally indicates that there may be problems with code design);
  • Unit testing is a great complement to integration testing (integration testing can’t cover complex systems very well);
  • The process of writing unit tests is itself a process of code refactoring;
  • Reading unit tests can help you get familiar with the code quickly (unit test cases are really user cases that reflect what the code does and how it is used, and can be used as an alternative in the absence of documentation or comments to understand what the code does without reading the code deeply).
  • Unit tests are implementable improvements to TDD (Test Driven Development, Test cases rather than code writing);

â‘£ Summarize the experience of writing unit tests

Is it really time consuming to write unit tests?

The amount of code, the process of writing cumbersome, but not very time-consuming, because there is no need to consider too much code design problems, most of it is CV operation.

Are there any code quality requirements for unit tests?

Unit tests, after all, don’t run in production. The test code of the class is relatively independent. The code quality requirement can be lower, the naming is not very standard, and the code is a little repetitive.

Is unit testing enough to have high coverage?

Test coverage is an easy metric to quantify and is often used as a gauge of how well unit tests are written. There are many off-the-shelf tools for doing coverage statistics (JaCoCo, Cobertura, etc.). Coverage is also calculated in many ways, from the simplest statement coverage to slightly more advanced ones: conditional coverage, decision coverage, and path coverage.

Blindly pursuing high coverage is not desirable; it is more important to see if the test cases cover all possible cases, especially some boundary conditions.

Focusing too much on coverage can lead to developers writing unnecessary tests (like GET and SET) to improve coverage. As a rule of thumb, a project with unit test coverage of 60-70% can go live, although if the project has strict code quality requirements, coverage requirements can be appropriately increased.

Do I need to know the implementation logic of the code to write unit tests?

No, unit tests only care about what the function under test does. You can’t just read code line by line and write unit tests for implementation logic in pursuit of coverage.

How do I choose a unit testing framework?

The code written by the team can not be tested with the selected unit test framework. Most of the reasons are that the code is not written well enough and the testability is poor. At this time, we should reconstruct our code to make it easier to test, rather than looking for another more advanced unit test framework.

Why are unit tests difficult to execute?

  • Writing unit tests itself is tedious and technically challenging, and many programmers are reluctant to do it.
  • Domestic research and development tends to be “fast, rough and fierce”, which leads to the anticlimactic unit test execution due to the tight development schedule.
  • Team members do not establish a correct understanding of unit tests, feel dispensable, relying on supervision alone is difficult to perform well;

0x3 Testability of code

The testability of the code, roughly speaking is: easy it is to write unit tests for code, for it is very difficult to write unit tests, a piece of code or being hard to write, need to rely on a unit test framework is advanced features, so often means that the code design is not reasonable, the testability of the code is not high.

If the code that rely on the external system or uncontrolled components (such as database, network communication, file systems, etc.), you need to code under test and the external system solutions for rely on, rely on this solution method called “Mock”, which is a “fake” service instead of real service, Mock services under our control, analog output data we want. There are two types of Mock, manual and frame Mock.

Common examples of test-unfriendly code are:

  • The code contains pending behavior logic (input is random or uncertain, such as time and random number-related code, which should be extracted into the method for easy test and replacement)
  • Misuse of mutable global variables (test cases may interact);
  • Abusing static methods (ditto, static methods can be bad mocks)
  • Use complex inheritance relationships (the parent class mocks a dependency for unit testing, and all subclasses mock that dependency);
  • Highly coupled code (a class depends on a dozen external objects to get its work done, and you might mock a dozen of those dependencies in unit testing);

0x4. How to decouple code through encapsulation, abstraction, modularization, mid-tier, etc

Why is decoupling so important?

  • Refactoring is an effective way to ensure that code quality does not deteriorate beyond redemption;
  • Decoupling is an effective means of code refactoring to ensure that the code is not too complex to control.

The “high cohesion, low coupling” feature allows us to focus on one module or class without having to know too much about the code of other modules or classes, making it less distracting and easier to read and modify the code. The dependencies are simple, the coupling is small, and the code modification is not necessary. The code changes are concentrated, the risk of introducing bugs is less, and the testability is better. It is easy to Mock or only requires a small number of external dependencies.

(2) How to determine whether the code needs decoupling

In addition to seeing whether the code will change the whole body, you can also draw the dependencies between modules and between classes, according to the complexity of the dependency diagram to determine whether the need for decoupling refactoring. If the dependencies are complex and confusing, the code structure is not very readable and maintainable.

â‘¢ How to decouple the code?

  • Encapsulation and abstraction (hiding complexity, isolating change, and providing a stable and easy-to-use interface);
  • Middle layer (simplifies dependencies between modules or classes);
  • Modularization (a module only provides an excuse to encapsulate the internal implementation details for other modules to use, so as to reduce the coupling degree between different modules);
  • Other design ideas and principles (single responsibility, interface-based programming rather than implementation, dependency injection, mix more and inheritance less, Demeter’s Law);

0x5. The top 20 Programming specifications for Improving Code Quality most quickly

(1) named

  • Named length → to be able to accurately express meaning as the goal, in the case of being able to express meaning as short as possible, the default or we are familiar with the word, recommended with abbreviation;
  • Using context shorthand naming → small-scoped temporary variables, using the context member variables and function parameters of the class, you can simplify naming (such as name in the User class);
  • Name to be readable, searchable, do not use uncommon, difficult to pronounce words to name, unified naming habits (such as using selectXXX table query, you do not use queryXXX);
  • Interface and Abstract class naming → Interface prefix with “I” or suffix with Impl, Abstract class prefix with or without Impl.

(2) comments

What should I write in a comment? (What to do, why, how to do), the following code example:


/**
* (what) Bean factory to create beans. 
* 
* (why) The class likes Spring IOC framework, but is more lightweight. 
*
* (how) Create objects from different sources sequentially:
* user specified object > SPI > configuration > default object.
*/
public class BeansFactory {
  // ...
}
Copy the code

Although what to do and how to do it can be reflected in the code, it is recommended to specify it in comments for the following reasons:

  • Comments carry more information than the code – the class contains more information, simple naming may not be detailed, in the comments to indicate what to do is easier to read;
  • Comments play the role of summary, documentation, so that people who read the code through comments can probably understand the implementation of the code ideas;
  • Some summative comments can make the code structure clearer → the logic is complex, long and difficult to extract and split the function, with summative comments to make the code structure clearer;

Of course, it’s not always the case that more comments are better. Too many comments mean that the code is not well-written and can interfere with the reading of the code itself, which can be costly to maintain later.

Generally speaking: class and function must write comments, and as far as possible comprehensive, detailed, and the internal function of the comments should be relatively few, generally rely on good clarity, refined function, explanatory variables, summary comments to improve the readability of the code.


â‘¢ Code style (team/project unity)

Class and function size is appropriate?

It’s hard to give an exact value, but it’s an indirect measure: a class has too many functions when the code reads too big, when you have to search for a function, when you have to use a small function to introduce the entire class (the class contains many functions that don’t implement that function).

How long a line of code should be?

A single line of code can’t be longer than the width of the IDE display, and scrolling to see the full line of code is obviously not conducive to code reading.

Use blank lines to split cell blocks

For longer functions that are not easily abstracted into smaller functions, empty lines can be used to separate code blocks in addition to summative comments. In addition, you can add blank lines between class members and functions, between static member variables and normal member variables, between functions, and so on.

Four indent or two indent?

It is recommended not to use Tab indent, because different IDE Tab indent display width is different, it is also ok to use, must be unified configuration!

Whether braces should start on another line?

It is recommended that the parentheses should be on the same line as the statement, so as to save the number of lines of code. It is also recommended that the left and right parentheses should be vertically aligned, so that it is easy to see which code belongs to which code block.

The order in which the members of a class are arranged

Suggestion: Package name of class → import imported dependent classes (alphabetical order from small to large) → member variables → functions. Variables and functions are arranged in static order from large to small. In fact, there is a convention of grouping functions that are related to each other.


â‘£ Programming skills

Divide code into smaller unit blocks

When the code logic is complex, it is recommended to refine the class or function. If the extracted function contains only two or three lines of code, you have to skip over the code, which increases the reading cost.

Avoid excessive function parameters

If the number of parameters is greater than or equal to 5, it will affect the feasibility of the code, and it is not convenient to use. First consider whether the responsibility is single disassembly, or encapsulate the parameters of the function into objects to pass.

Do not use function arguments to control logic

Do not use Boolean identifiers in functions to control internal logic. True takes one piece of logic and false takes another. This violates the principle of single responsibility and interface isolation. There is also a case to determine whether the parameter is null control logic, also recommended disassembly.

Function design should have a single responsibility

Function Settings will be more consistent with the single responsibility principle, as single as possible.

Remove too deep nesting levels

Too deep nesting itself is difficult to understand, nesting over two layers should consider whether to reduce nesting, several solutions: Remove redundant if or else statements, use the continue, break, return keywords provided by the programming language to exit nesting early, adjust the execution order, encapsulate part of the logic into function calls, polymorphisms, etc.

Learn to use explanatory variables

Constants instead of magic numbers use explanatory variables to interpret complex expressions as shown in the following example:

public double CalculateCircularArea(double radius) {
  return (3.1415) * radius * radius;
}

// Constant replaces magic number
public static final Double PI = 3.1415;
public double CalculateCircularArea(double radius) {
  return PI * radius * radius;
}

if (date.after(SUMMER_START) && date.before(SUMMER_END)) {
  // ...
} else {
  // ...
}

// The logic is clearer after introducing explanatory variables
boolean isSummer = date.after(SUMMER_START) && date.before(SUMMER_END);
if (isSummer) {
  // ...
} else {
  // ...
} 
Copy the code