Very practical code refactoring skills

Posted by classix16 on Tue, 04 Jan 2022 05:46:23 +0100

Reprinted from: https://mp.weixin.qq.com/s/NAe8DU6a55HzbbFTHkooLQ

About refactoring

Why refactoring

1_ Code refactoring cartoon jpeg

As the project evolves, the code keeps piling up. If no one is responsible for the quality of code, code will always evolve in a more and more chaotic direction. When the confusion reaches a certain degree, quantitative change causes qualitative change. The maintenance cost of the project has been higher than the cost of redeveloping a new set of code. No one can do it again.

This is often caused by the following reasons:

  1. Lack of effective design before coding
  2. Cost considerations, stack programming in the original function
  3. Lack of effective code quality supervision mechanism

For such problems, the industry has a good solution: eliminate the "bad smell" in the code through continuous refactoring.

What is refactoring

Martin Fowler, author of refactoring, defines Refactoring:

Refactoring (noun): an adjustment to the internal structure of software. The purpose is to improve its comprehensibility and reduce its modification cost without changing the observable behavior of software.
Refactoring (verb): use a series of refactoring techniques to adjust the structure of software without changing its observable behavior.

According to the scale of reconstruction, it can be roughly divided into large-scale reconstruction and small-scale reconstruction:

Large scale Refactoring: the refactoring of top-level code design, including the refactoring of system, module, code structure and the relationship between classes. The means of refactoring include layering, modularization, decoupling, abstract reusable components, etc. Such refactoring tools are the design ideas, principles and patterns we have learned. This kind of refactoring will involve more code changes and have a large impact, so it will be more difficult, time-consuming, and the risk of introducing bug s will be relatively large.

Small Refactoring: refactoring of code details, mainly for code level refactoring of classes, functions and variables, such as standardizing naming and annotation, eliminating super large classes or functions, extracting duplicate code, etc. Small refactoring uses more uniform coding specifications. This kind of refactoring needs to be modified in a centralized way, which is relatively simple, operable, time-consuming, and the risk of introducing bugs is relatively small. When a "bad code smell" occurs in the development of new functions, bug repair or code review, we should refactor in time. Continuous small refactoring in daily development can reduce the cost of refactoring and testing.

Bad smell of code

[the external chain picture transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-vst4trmy-1628497473590) (data: image / GIF; Base64, ivbow0kgoaaansuheugaaaaaabcayaaaaffsjaaaaduleqvqimwngygbgaaaaabqabh6fo1aaaaaabjru5erkjggg = =)]_ Code FAQs png

Duplicate code

  • The implementation logic is the same and the execution process is the same

Long Method

  • The statements in the method are not at the same level of abstraction
  • The logic is difficult to understand and requires a lot of comments
  • Process oriented programming, not object-oriented

Too large class

  • Class does too much
  • Contains too many instance variables and methods
  • Class naming is not enough to describe what you do

Logical dispersion

  • Divergent change: a class often changes in different directions for different reasons
  • Shotgun modification: when a change occurs, it needs to be modified in multiple classes

Serious complex attachment

  • Methods of a class use too many members of other classes

Data muddle / basic type paranoia

  • Two class and method signatures contain the same fields or parameters
  • Classes should be used, but basic types should be used, such as the Money class representing values and currencies, and the Range class representing start and end values

Unreasonable inheritance system

  • Inheritance breaks the encapsulation, and subclasses depend on the implementation details of specific functions in their parent classes
  • A subclass must evolve with the update of its parent class, unless the parent class is specifically designed for extension and well documented

Excessive conditional judgment

Too long parameter column

Too many temporary variables

Confusing temporary fields

  • An instance variable is set only for a specific situation
  • Extract the instance variables and corresponding methods into the new class

Pure data class

  • Contains only fields and methods to access (read and write) them
  • These are called data containers and should maintain minimum variability

Improper naming

  • Naming doesn't accurately describe what you do
  • Naming does not conform to the Convention of common name

Too many comments

Bad code problem

  • Difficult to reuse
  • Too many system correlations make it difficult to separate reusable parts
  • Difficult to change
  • One change leads to the modification of many other parts, which is not conducive to the stability of the system
  • Difficult to understand
  • The naming is messy, the structure is chaotic, and it is difficult to read and understand
  • Difficult to test
  • There are many branches and dependencies, so it is difficult to cover all aspects

What is good code

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-3pkr1yhr-1628497473591) (data: image / GIF; Base64, ivborw0kggoaaaansuheugaaaaaabcayaaaafcsjaaaaduleqvqimwngygbgaaaaabqabh6fo1aaaaaabjru5erkjggg = =)]3_ How to measure code quality jpg

The evaluation of code quality has strong subjectivity, and there are many words to describe code quality, such as readability, maintainability, flexibility, elegance and simplicity. These words evaluate code quality from different dimensions. Among them, maintainability, readability and scalability are the three most mentioned and important evaluation criteria.

To write high-quality code, we need to master some more detailed and landing programming methodologies, including object-oriented design ideas, design principles, design patterns, coding specifications, refactoring skills, etc.

How to refactor

SOLID principle

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-ys0b1pmw-1628497473592) (data: image / GIF; Base64, ivborw0kgoaaaaansuheugaaaaaabcaaaafcsjaaaaduleqvqimwngygbgaaaaabqabh6fo1aaaaaabjru5erkjgg = =)]_ Solid principle png

Single responsibility principle

A class is only responsible for completing one responsibility or function. There should be no more than one reason for class change.

The single responsibility principle improves the cohesion of classes by avoiding the design of large and complete classes and the coupling of irrelevant functions. At the same time, the class responsibility is single, and there will be fewer class dependencies and other dependent classes, reducing the coupling of the code, so as to achieve high cohesion and loose coupling of the code. However, if you split it too carefully, it will actually backfire, reduce cohesion and affect the maintainability of the code.

Open close principle

Adding a new function should be done by extending the code (adding modules, classes, methods, attributes, etc.) on the basis of the existing code, rather than modifying the existing code (modifying modules, classes, methods, attributes, etc.).

The opening and closing principle does not mean to completely eliminate modification, but to complete the development of new functions at the cost of minimum code modification.

Many design principles, design ideas and design patterns aim at improving the scalability of the code. In particular, most of the 23 classic design patterns are summarized to solve the problem of code scalability, and they all take the open and close principle as the guiding principle. The most commonly used methods to improve code extensibility are polymorphism, dependency injection, programming based on interface rather than implementation, and most design patterns (such as decoration, strategy, template, responsibility chain, state).

Richter substitution principle

The subclass object (object of subtype/derived class) can replace any place where the parent object (object of base/parent class) appears in the program, and ensure that the logical behavior of the original program is unchanged and the correctness is not damaged.

Subclasses can extend the functions of the parent class, but cannot change the original functions of the parent class

All implemented methods in the parent class (compared with abstract methods) are actually setting a series of specifications and contracts. Although it does not force all subclasses to comply with these contracts, if subclasses modify these non abstract methods arbitrarily, it will destroy the whole inheritance system.

Interface isolation principle

The caller should not rely on interfaces it does not need; The dependence of one class on another should be based on the smallest interface. The interface isolation principle provides a standard to judge whether the responsibility of the interface is single: it is determined indirectly by how the caller uses the interface. If the caller only uses some interfaces or some functions of the interface, the design of the interface is not single enough.

Dependency Inversion Principle

High level modules should not rely on low-level modules, and both should rely on their abstraction; Abstract should not rely on details, details should rely on abstraction.

Dimitt's law

One object should have minimal knowledge of other objects

Synthetic Reuse Principle

Try to use composition / aggregation instead of inheritance.

The single responsibility principle tells us that the implementation class should have a single responsibility; Richter's substitution principle tells us not to destroy the inheritance system; The dependency inversion principle tells us to face interface programming; The principle of interface isolation tells us to simplify and simplify the interface design; Demeter's law tells us to reduce coupling. The opening and closing principle is the general outline, which tells us to open to expansion and close to modification.

Design pattern

Design pattern: the solution to the general problems faced by software developers in the process of software development. These solutions are summarized by many software developers after a long period of experiments and errors. Each pattern describes a recurring problem around us and the core solution to the problem.

  • Creation type: it mainly solves the problem of object creation, encapsulates the complex creation process, and decouples the object creation code and use code
  • Structural type: decoupling the coupling of different functions mainly through different combinations of classes or objects
  • Behavioral: it mainly solves the coupling of interaction behavior between classes or objects

[the external chain picture transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-hgn2jqja-1628497473593) (data: image / GIF; Base64, ivborw0kggoaaaansuheugaaaaaabcaaaaaafcsjaaaaduleqvqimwngygbgaaabqabh6fo1aaaaaabjru5erkjggg = =)] [the external chain picture transfer fails, and the source station may have an anti-theft chain mechanism, so it is recommended to save the picture and upload it directly (img-jeudled5-1628497473595) (data: image / GIF; Base64, ivborw0kgoaaansuheugaaaaaaaaabcayaaaafcsjaaaaduleqvqimwngygbgaaaaabqabh6fo1aaaaaabjru5erkjgggg = =)] [the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-uggtjrtk-1628497473595) (data: image / GIF; Base64, ivborw0kgoaaansuheugaaaaaaaabcayaaaaffcsjaaaaaduleqvqimwngygbgaaabqabh6fo1aaaaaabjru5erkjgggg = =)] [the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-s0FIz4nj-1628497473596)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)]

Code layering

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-eabeoawm-1628497473597) (data: image / GIF; Base64, ivborw0kgoaaansuheugaaaeaaabcayaaaafcsjaaaaduleqvqimwngygbgaaaaabqabh6fo1aaaaaabjru5erkjggg = =)] png

Module structure description

  • server_main: configuration layer, responsible for module management, maven configuration management and resource management of the whole project;
  • server_application: application access layer, which undertakes external traffic entry, such as RPC interface implementation, message processing, timing tasks, etc; Do not include business logic here;
  • server_biz: core business layer, use case service, domain entity, domain event, etc
  • server_irepository: resource interface layer, which is responsible for exposing resource interfaces
  • server_repository: the resource layer, which is responsible for proxy access to resources, unified access to external resources, and isolated changes. Note: weak business and strong data are emphasized here;
  • server_common: common layer, vo, tools, etc

Code development should comply with the specifications of each layer and pay attention to the dependencies between layers.

Naming conventions

A good naming should satisfy the following two constraints:

  • Describe exactly what you did
  • The format complies with general conventions

If you find it difficult to name a class or method, it may carry too many functions and need to be further split.

Convention commonly known as convention

Class naming

Class names use the form of big hump, and class names usually use nouns or noun phrases. In addition to nouns and noun phrases, the interface name can also use adjectives or adjective phrases, such as clonable, Callable, etc., to indicate that the class implementing the interface has some function or capability.

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-jpvpx0nw-1628497473598) (data: image / GIF; Base64, ivborw0kggoaaaansuheugaaaaaabcaaaaaafcsjaaaaduleqvqimwngygbgaaaaabqabh6fo1aaaaaabjru5erkjggg = =)]

Method naming

The method is named in the form of a small hump, the first word is lowercase, and the first letter of each subsequent word should be capitalized. Different from class names, method names are generally verbs or verb phrases, which together with parameters or parameter names form verb object phrases, that is, verbs + nouns. A good function name can generally know what function the function implements directly through the name.

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-nvoalhjc-1628497473599) (data: image / GIF; Base64, ivborw0kggoaaaansuheugaaaaaabcaaaaaafcsjaaaaduleqvqimwngygbgaaaaabqabh6fo1aaaaaabjru5erkjggg = =)]

Refactoring skills

Extract Method

Multiple method codes are repeated, the code in the method is too long, or the statements in the method are not at an abstraction level.
The method is the minimum granularity of code reuse. The method is too long, which is not conducive to reuse and has low readability. Refining the method is often the first step of refactoring.

Intention oriented programming: separate the process of dealing with something from the implementation of specific things.

  • Decompose a problem into a series of functional steps and assume that these functional steps have been implemented
  • We just need to organize the functions together to solve this problem
  • After organizing the whole function, we implement each method function separately
/** 
  * 1,The transaction information starts with a string of standard ASCII strings. 
  * 2,The information string must be converted into an array of strings, which stores the lexical elements (tokens) contained in the domain language of the transaction. 
  * 3,Every word must be standardized. 
  * 4,Transactions with more than 150 lexical elements should be submitted in a way different from small transactions (different algorithms) to improve efficiency. 
  * 5,If the submission is successful, the API returns "true"; "false" is returned if it fails. 
  */
public class Transaction {    
  public Boolean commit(String command) {        
    Boolean result = true;        
    String[] tokens = tokenize(command);        
    normalizeTokens(tokens);        
    if (isALargeTransaction(tokens)) {            
      result = processLargeTransaction(tokens);        
    } else {            
      result = processSmallTransaction(tokens);        
    }        
    return result;    
  }
}

Replace function with function object

Place the function in a separate object so that the local variable becomes a field within the object. Then you can decompose this large function into multiple small functions in the same object.

Import parameter object

When there are many method parameters, the parameters are encapsulated as parameter objects

Remove Assignments to Parameters

public int discount(int inputVal, int quantity, int yearToDate) {
  if (inputVal > 50) inputVal -= 2;
  if (quantity > 100) inputVal -= 1;
  if (yearToDate > 10000) inputVal -= 4;
  return inputVal;
}

public int discount(int inputVal, int quantity, int yearToDate) { 
  int result = inputVal;
  if (inputVal > 50) result -= 2; 
  if (quantity > 100) result -= 1; 
  if (yearToDate > 10000) result -= 4; 
  return result; 
}

Separate query from modification

Any method that has a return value should not have side effects

  • Do not call write operations in convert to avoid side effects.
  • Common exception: caching query results locally

Remove unnecessary temporary variables

When temporary variables are used only once or the value logic cost is very low

Introduction of explanatory variables

Put the result of a complex expression (or part of it) into a temporary variable, and use the variable name to explain the purpose of the expression

if ((platform.toUpperCase().indexOf("MAC") > -1) 
    && (browser.toUpperCase().indexOf("IE") > -1) && wasInitialized() && resize > 0) {   
  // do something 
} 
  
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1; 
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1; 
final boolean wasResized = resize > 0; 
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {   
  // do something 
}

Replace nested condition judgment with guard statement

Split complex conditional expressions into multiple conditional expressions to reduce nesting. Several layers of if - then-else statements are nested and converted into multiple if statements

//Statement not used
public void getHello(int type) {
    if (type == 1) {
        return;
    } else {
        if (type == 2) {
            return;
        } else {
            if (type == 3) {
                return;
            } else {
                setHello();
            }
        }
    }
} 

//Using guard statements
public void getHello(int type) {
    if (type == 1) {
        return;
    }
    if (type == 2) {
        return;
    }
    if (type == 3) {
        return;
    }
    setHello();
}

Using polymorphic substitution conditions to judge fault

When there is such a kind of conditional expression, it selects different behaviors according to different object types. You can put each branch of this expression into a replication function in a subclass, and then declare the original function as an abstract function.

public int calculate(int a, int b, String operator) {
    int result = Integer.MIN_VALUE;
 
    if ("add".equals(operator)) {
        result = a + b;
    } else if ("multiply".equals(operator)) {
        result = a * b;
    } else if ("divide".equals(operator)) {
        result = a / b;
    } else if ("subtract".equals(operator)) {
        result = a - b;
    }
    return result;
}

When a large number of type checks and judgments occur, If else (or switch) statements are bulky, which undoubtedly reduces the readability of the code. In addition, if else (or switch) itself is a "change point". When we need to extend new types, we have to add if else (or switch) statement blocks and corresponding logic, which undoubtedly reduces the scalability of the program and violates the object-oriented opening and closing principle.

Based on this scenario, we can consider using "polymorphism" instead of lengthy conditional judgment, Encapsulate the "change point" in if else (or switch) into subclasses. In this way, there is no need to use if else (or switch) statements. Instead, there are polymorphic instances of subclasses, so as to improve the readability and scalability of the code. Many design patterns use this routine, such as policy mode and state mode.

public interface Operation { 
  int apply(int a, int b); 
}

public class Addition implements Operation { 
  @Override 
  public int apply(int a, int b) { 
    return a + b; 
  } 
}

public class OperatorFactory {
    private final static Map<String, Operation> operationMap = new HashMap<>();
    static {
        operationMap.put("add", new Addition());
        operationMap.put("divide", new Division());
        // more operators
    }
 
    public static Operation getOperation(String operator) {
        return operationMap.get(operator);
    }
}

public int calculate(int a, int b, String operator) {
    if (OperatorFactory .getOperation == null) {
       throw new IllegalArgumentException("Invalid Operator");
    }
    return OperatorFactory .getOperation(operator).apply(a, b);
}

Use exception instead of return error code

For the processing of abnormal business status, throw an exception instead of returning an error code

  • Do not use exception handling for normal business process control

    • The performance cost of exception handling is very high
  • Try to use standard exceptions

  • Avoid throwing exceptions in finally statement blocks

    • If two exceptions are thrown at the same time, the call stack of the first exception will be lost
    • Finally, only things like closing resources should be done in the finally block
//Use error code
public boolean withdraw(int amount) {
    if (balance < amount) {
        return false;
    } else {
        balance -= amount;
        return true;
    }
}

//Use exception
public void withdraw(int amount) {
    if (amount > balance) {
        throw new IllegalArgumentException("amount too large");    
    }
    balance -= amount;
}

introduce assertion

A piece of code needs to make some assumptions about the program state in order to express this assumption explicitly.

  • Don't abuse assertions. Don't use them to check "should be true" conditions. Just use them to check "must be true" conditions
  • If the constraints indicated by the assertion cannot be met, can the code still run normally? Remove assertions if you can

Introducing Null or special objects

When an object returned by a method is used, and the object may be null, it is necessary to judge the null before operating the object, otherwise a null pointer will be reported. When this judgment appears frequently in various codes, it will affect the beauty and readability of the code, and even increase the probability of bugs.

The problem of null references is unavoidable in Java, but it can be improved by code programming techniques (Introducing null objects).

//Example of an empty object
public class OperatorFactory { 
  static Map<String, Operation> operationMap = new HashMap<>(); 
  static { 
    operationMap.put("add", new Addition()); 
    operationMap.put("divide", new Division()); 
    // more operators 
  } 
  public static Optional<Operation> getOperation(String operator) { 
    return Optional.ofNullable(operationMap.get(operator)); 
  } 
} 
public int calculate(int a, int b, String operator) { 
  Operation targetOperation = OperatorFactory.getOperation(operator) 
     .orElseThrow(() -> new IllegalArgumentException("Invalid Operator")); 
  return targetOperation.apply(a, b); 
}

//Examples of special objects
public class InvalidOp implements Operation { 
  @Override 
  public int apply(int a, int b)  { 
    throw new IllegalArgumentException("Invalid Operator");
  } 
}

Refining class

According to the principle of single responsibility, a class should have a clear responsibility boundary. But in practice, classes will continue to expand. When you add a new responsibility to a class, you don't think it's worth separating a separate class. Therefore, with the increasing responsibility, this class contains a large number of data and functions, and the logic is complex and difficult to understand.

At this point, you need to consider which parts are separated into a separate class, which can be based on the principle of high cohesion and low coupling. If some data and methods always appear together, or some data often changes at the same time, it indicates that they should be placed in a class. Another signal is the subclassing method of a class: if you find that subclassing only affects some of the properties of a class, or that the properties of a class need to be subclassed in different ways, it means that you need to decompose the original class.

//Primitive class
public class Person {
    private String name;
    private String officeAreaCode;
    private String officeNumber;

    public String getName() {
        return name;
    }

    public String getTelephoneNumber() {
        return ("(" + officeAreaCode + ")" + officeNumber);
    }

    public String getOfficeAreaCode() {
        return officeAreaCode;
    }

    public void setOfficeAreaCode(String arg) {
        officeAreaCode = arg;
    }

    public String getOfficeNumber() {
        return officeNumber;
    }

    public void setOfficeNumber(String arg) {
        officeNumber = arg;
    }
}

//Newly refined class (replacing data values with objects)
public class TelephoneNumber {
    private String areaCode;
    private String number;

    public String getTelephnoeNumber() {
        return ("(" + getAreaCode() + ")" + number);
    }

    String getAreaCode() {
        return areaCode;
    }

    void setAreaCode(String arg) {
        areaCode = arg;
    }

    String getNumber() {
        return number;
    }

    void setNumber(String arg) {
        number = arg;
    }
}

Composition takes precedence over inheritance

Inheritance is a powerful means to realize code reuse, but it is not always the best tool to complete this work. Improper use will lead to software vulnerability. Unlike method calls, inheritance breaks encapsulation. The subclass depends on the implementation details of specific functions in its parent class. If the implementation of the parent class changes with the release version, the subclass may be damaged, even if its code has not changed at all.

For example, suppose a program uses HashSet. In order to tune the performance of the program, you need to count how many elements have been added to the HashSet since it was created. To provide this functionality, we write a variant of HashSet.

// Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
    // The number of attempted element insertions
    private int addCount = 0;

    public InstrumentedHashSet() { }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

By adding a private domain to the new class, which references an instance of the existing class, this design is called composition, because the existing class becomes a component of the new class. The resulting class will be very stable and independent of the implementation details of existing classes. Even if a new method is added to an existing class, the new class will not be affected. Many design patterns use this routine, such as agent mode and decorator mode

// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }
  
    @Override
    public int size() { return s.size(); }
    @Override
    public boolean isEmpty() { return s.isEmpty(); }
    @Override
    public boolean contains(Object o) { return s.contains(o); }
    @Override
    public Iterator<E> iterator() { return s.iterator(); }
    @Override
    public Object[] toArray() { return s.toArray(); }
    @Override
    public <T> T[] toArray(T[] a) { return s.toArray(a); }
    @Override
    public boolean add(E e) { return s.add(e); }
    @Override
    public boolean remove(Object o) { return s.remove(o); }
    @Override
    public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
    @Override
    public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
    @Override
    public boolean retainAll(Collection<?> c) { return s.retainAll(c); }
    @Override
    public boolean removeAll(Collection<?> c) { return s.removeAll(c); }
    @Override
    public void clear() { s.clear(); }
}

// Wrappter class - uses composition in place of inheritance
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public InstrumentedHashSet1(Set<E> s) {
        super(s);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

How to choose between inheritance and combination

  • Inheritance is appropriate only when the subclass is really A subtype of the parent class. For two classes A and B, class B should inherit A only if there is an "is-a" relationship between them;
  • It is very safe to use inheritance inside the package. The implementation of subclasses and parent classes are under the control of the same programmer;
  • It is also very safe to use inheritance for classes designed specifically for inheritance and well documented;
  • In other cases, priority should be given to combination

Interfaces are better than abstract classes

Java provides two mechanisms for defining types that allow multiple implementations: interfaces and abstract classes. Since Java8 adds a default method to the interface, both mechanisms allow the implementation of instance methods. The main difference is that in order to implement the types defined by abstract classes, classes must be called a subclass of abstract classes. Because Java only allows single inheritance, the use of abstract classes as type definitions is limited.

Advantages of interfaces over abstract classes:

  • Existing classes can be easily updated to implement new interfaces.
  • Interfaces are ideal for defining mixed types, such as Comparable.
  • Interfaces allow the construction of non hierarchical type frameworks.

Although the interface provides default methods, the interface still has the following limitations:

  • The variable modifier of an interface can only be public static final
  • The method modifier of an interface can only be public
  • Interface does not have a constructor, nor does this exist
  • You can add default methods to existing interfaces, but you can't ensure that these methods work well in previous implementations.
  • Because these default methods are injected into existing implementations, their implementers do not know or license them

The design purpose and advantages of the interface default method are:

For interface evolution

  • We knew before Java 8, All methods of an interface and their subclasses must be implemented (of course, this subclass is not an abstract class), but the default method of the interface after Java 8 can be selected not to be implemented. The above operations can be compiled during compilation. This avoids the project compilation error when upgrading from Java 7 to Java 8. Java 8 adds many new default methods to the core collection interface, mainly to facilitate the use of lambda.

You can reduce the creation of third-party tool classes

  • For example, there are some default methods in collection interfaces such as List. The List interface provides default methods such as replaceAll(UnaryOperator), sort(Comparator),, splitter () by default. These methods are created inside the interface to avoid creating corresponding tool classes for these methods.

You can avoid creating base classes

  • Before Java 8, we may need to create a base class to realize code reuse. With the emergence of the default method, it is not necessary to create a base class.

Due to the limitations of the interface and different design purposes, the interface can not completely replace the abstract class. However, by providing an abstract skeleton implementation class for the interface, the advantages of the interface and the abstract class can be combined. The interface is responsible for defining types and perhaps providing some default methods, while the skeleton implementation class is responsible for implementing the remaining non basic type interface methods in addition to the basic type interface methods. The extension skeleton implementation accounts for most of the work outside the implementation interface. This is the Template Method design pattern.

Image [5].png

Interface protocol: defines two main methods of RPC Protocol layer, export exposure service and refer reference service

Abstract class AbstractProtocol: encapsulates the Exporter after exposing the service and the Invoker instance after referencing the service, and implements the logic of service destruction

The specific implementation class XxxProtocol: implements the specific logic of export exposure service and refer reference service

Give preference to generics

A class or interface with one or more type parameter s in the declaration is a generic class or interface. Generic classes and interfaces are collectively referred to as generic type s. Generics introduced from Java 5 , provides a compile time type safety detection mechanism. The essence of generics is a parameterized type, which represents the operated data type through a parameter, and can limit the type range of this parameter. The benefit of generics is compile time type detection to avoid type conversion.

// Compares three values and returns the maximum value
public static <T extends Comparable<T>> T maximum(T x, T y, T z) {   
  T max = x; 
  // Suppose x is the initial maximum   
  if ( y.compareTo( max ) > 0 ) {      
    max = y; //y is bigger  
  }   if ( z.compareTo( max ) > 0 ) {     
    max = z; // Now z is bigger              
  }   return max; // Returns the largest object
}

public static void main( String args[] ) {   
  System.out.printf( "%d, %d and %d The maximum number in is %d\n\n",  3, 4, 5, maximum( 3, 4, 5 ));   
  System.out.printf( "%.1f, %.1f and %.1f The maximum number in is %.1f\n\n",  6.6, 8.8, 7.7,  maximum( 6.6, 8.8, 7.7 ));   
  System.out.printf( "%s, %s and %s The maximum number in is %s\n","pear", "apple", "orange", maximum( "pear", "apple", "orange" ) );
}

Do not use primitive types

In order to maintain compatibility of Java code, support and primitive type conversion, and use erasure mechanism to implement generics. However, using primitive types will lose the advantage of generics and will be warned by the compiler.

Every non inspected warning should be eliminated as much as possible

Each warning indicates that a ClassCastException exception may be thrown at runtime. Do your best to eliminate these warnings. If the code causing the warning cannot be eliminated but can be proved to be safe, you can use the @ SuppressWarnings("unchecked") annotation to suppress the warning in as small a range as possible, but record the reasons for the prohibition.

Use restricted wildcards to improve API flexibility

Parameterized types do not support covariance, that is, for any two different types Type1 and Type2, List is neither a subtype of List nor its superclass. In order to solve this problem and improve flexibility, Java provides a special parameterization type called restricted wildcard type, that is, List <? Extensions E > and List <? super E>. The use principle is producer extends, consumer super (PECS). If you are both a producer and a consumer, there is no need to use wildcards.

There is also a special infinite wildcard list <? >, Represents a type but is not sure. It is often used as a reference to a generic type, and no object other than Null can be added to it.

//List<? extends E>
// Number can be considered as a "subclass" of number
List<? extends Number> numberArray = new ArrayList<Number>(); 
// Integer is a subclass of Number
List<? extends Number> numberArray = new ArrayList<Integer>(); 
// Double is a subclass of Number
List<? extends Number> numberArray = new ArrayList<Double>();  

//List<? super E>
// Integer can be considered the "parent" of integer
List<? super Integer> array = new ArrayList<Integer>();,
// Number is the parent of Integer
List<? super Integer> array = new ArrayList<Number>();
// Object is the parent class of Integer
List<? super Integer> array = new ArrayList<Object>();

public static <T> void copy(List<? super T> dest, List<? extends T> src) {    
  int srcSize = src.size();    
  if (srcSize > dest.size())        
   throw new IndexOutOfBoundsException("Source does not fit in dest");    
  if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) {        
    for (int i=0; i<srcSize; i++)            
    dest.set(i, src.get(i));    
  } else {        
    ListIterator<? super T> di=dest.listIterator();        
    ListIterator<? extends T> si=src.listIterator();        
    for (int i=0; i<srcSize; i++) {            
      di.next();            
      di.set(si.next());        
    }    
  }
}

Static member classes are better than non static member classes

Nested class refers to a class defined inside another class. The purpose of a nested class is to provide services for its external class. If it is also used in other environments, it should become a top-level class. There are four kinds of nested classes: static member class and non static member class (nonstatic member class), anonymous class and local class. Except the first, the other three are called inner class es.

anonymous class

There is no name. It can be instantiated at the same time as the declaration. It can only be used once. When it appears in a non static environment, it holds a reference to an external class instance. It is usually used to create function objects and procedure objects, but lambda will be preferred for now.

local class

Local classes can be declared wherever local variables can be declared, and the same scope rules are followed. Unlike anonymous classes, there are names that can be reused. However, local categories are rarely used in practice.

static member class

The simplest nested class, declared inside another class, is a static member of this class and follows the same accessibility rules. The common usage is as a public auxiliary class, which makes sense only when used with its external class.

Non static member class

Although syntactically, the only difference from static member classes is that the class declaration does not contain static, they are very different. Each instance of a non static member class is implicitly associated with an instance of an external class, and you can access the member properties and methods of the external class. In addition, you must create an instance of an external class before you can create an instance of a non static member class.

All in all, these four nested classes have their own uses. Assuming that this nested class belongs to the interior of a method, if you only need to create an instance in one place, and you already have a preset type that can describe the characteristics of this class, you should make it an anonymous class. If a nested class needs to be visible outside a single method, or it is too long to fit inside a method, you should use a member class. If each instance of a member class needs a reference to its peripheral instance, make the member class non static, otherwise make it static.

Template / tool class is preferred

By abstractly encapsulating the code logic of common scenarios and forming corresponding template tool classes, you can greatly reduce duplicate code, focus on business logic and improve code quality.

Create and use separate objects

Compared with process oriented programming, object-oriented programming has more instantiation, and the creation of objects must specify specific types. Our common practice is "create where you use it". The code that uses the instance and creates the instance is the same piece of code. This seems to make the code more readable, but in some cases it creates unnecessary coupling.

public class BusinessObject {
 public void actionMethond {
     //Other things
     Service myServiceObj = new Service();
       myServiceObj.doService();
       //Other things
    }
}

public class BusinessObject {
 public void actionMethond {
     //Other things
     Service myServiceObj = new ServiceImpl();
       myServiceObj.doService();
       //Other things
    }
}

public class BusinessObject {
   private Service myServiceObj;
   public BusinessObject(Service aService) {
       myServiceObj = aService;
    }
 public void actionMethond {
     //Other things
       myServiceObj.doService();
       //Other things
    }
}

public class BusinessObject {
   private Service myServiceObj;
   public BusinessObject() {
       myServiceObj = ServiceFactory;
    }
 public void actionMethond {
     //Other things
       myServiceObj.doService();
       //Other things
    }
}

The creator of an object couples the specific type of the object, while the user of the object couples the interface of the object. In other words, the creator is concerned about what the object is, and the user is concerned about what it can do. These two should be regarded as independent considerations, and they often change for different reasons.

When the object type involves polymorphism and the object creation is complex (there are many dependencies), it can be considered to separate the object creation process, so that users do not pay attention to the details of object creation. This is the starting point of creation mode in design mode. Factory mode, builder and dependency injection can be used in actual projects.

Accessibility minimization

A very important factor to distinguish whether a component is well designed or not is whether it hides its internal data and implementation details for external components. Java provides an access control mechanism to determine the accessibility of classes, interfaces and members. The accessibility of an entity is determined by the location of the entity declaration and the access modifiers (private, protected, public) in the entity declaration.

For top-level (non nested) classes and interfaces, there are only two access levels: package level private (without public modification) and public (public modification).

For members (instance / domain, method, nested class and nested interface), there are four access levels, and the accessibility increases as follows:

  • Private (private modification) – the member can be accessed only inside the top-level class that declares the member;
  • Package level private (default) – any class within the package that declares the member can access the member;
  • protected modification - subclasses of the class declaring the member can access the member, and any class within the package declaring the member can also access the member;
  • Public (public modification) – the member can be accessed anywhere;

The correct use of these modifiers is very critical to the implementation of information hiding. The principle is to make every class and member not accessed by the outside world (private or package level private) as much as possible. This advantage is that it can be modified, replaced or deleted in future releases without worrying about affecting the existing client programs.

  • If a class or interface can be made package level private, it should be made package level private;
  • If a package level private top-level class or interface is only used inside a class, it should be considered to make it a private nested class of that class;
  • Public classes should not directly expose the instance domain, but should provide corresponding methods to retain the flexibility of changing the internal representation of the class in the future;
  • When the public API of the class is determined, other members should be made private;
  • If there are more accesses between classes in the same package, redesign should be considered to reduce this coupling;

Variability minimization

Immutable classes are classes whose instances cannot be modified. All the information contained in each instance must be provided when the instance is created and fixed throughout the life cycle of the object. Immutable classes are easy to use, thread safe, freely shared and not error prone. The Java platform class library contains many immutable classes, such as String, basic type wrapper class, BigDecimal, etc.

To make a class immutable, follow the following five rules:

  • Declare that all domains are private

  • Declare that all fields are final

    • If a reference to a newly created instance is passed from one thread to another without synchronization, you must ensure the correct behavior
  • No method is provided that will modify the state of the object

  • Ensure that the class will not be extended (prevent subclassing and declare the class as final)

    • Prevent careless or malicious subclasses from pretending that the state of the object has changed, thereby undermining the immutable behavior of the class
  • Ensure mutually exclusive access to any mutable components

    • If a class has domains that point to mutable objects, you must ensure that clients of the class cannot obtain references to these objects. Also, never initialize such a domain with an object reference provided by the client, nor return the object reference from any access method. Protective copy technology is used in constructor, access method and readObject method

Some suggestions for minimizing variability:

  • Unless there is a good reason to make a class variable, it should be immutable;
  • If a class cannot be made immutable, its variability should still be limited as much as possible;
  • Unless there is a convincing reason to make the domain non final, make every domain private final;
  • The constructor should create fully initialized objects and establish all constraint relationships;

How to guarantee the quality

Test Driven Development

Test Driven Development (TDD) requires testing as the center of the development process. Before writing any code, it is required to write tests for code generation behavior, and the written code should aim to make the tests pass. TDD requires that the tests can be run completely automatically and must be run before and after code reconstruction.

The ultimate goal of TDD is clean code that works. Most developers cannot get clean and usable code most of the time. The solution is divide and conquer. First solve the problem of "availability" in the goal, and then solve the problem of "code cleanliness". This is opposite to architecture driven development.

Another advantage of adopting TDD is that we have a detailed set of automated tests generated with the code. In the future, when we need to maintain the code for any reason (requirements, refactoring, performance improvement), our code will always be robust under the driving of this test set.

Development cycle of TDD

Image [6].png

Add a test - > run all tests and check the test results - > write code to pass the test - > run all tests and pass all - > refactor the code to eliminate duplicate design and optimize the design structure

Two basic principles

  • Write code only when the test fails and only code that just makes the test pass
  • Eliminate existing duplicate designs and optimize the design structure before writing the next test

Separation of concerns is another very important principle implied by these two rules. The meaning of its expression means to achieve the goal of "usable" code in the coding stage, and then pursue the goal of "cleanliness" in the reconstruction stage, focusing on only one thing at a time!

Layered test points