About refactoring

Why refactoring

As the project evolves, code keeps piling up. Without someone to take responsibility for the quality of your code, your code will always evolve in a more chaotic direction. When the confusion reaches a certain level, quantitative change leads to qualitative change, and the maintenance cost of the project is already higher than the cost of redeveloping a new set of code, and no one can do it again.

The reasons for this are as follows:

  1. Lack of effective design prior to coding
  2. Cost considerations in the original functional stack programming
  3. Lack of effective code quality monitoring mechanisms

The industry already has a good solution to this problem: clean out the “bad taste” in code through constant refactoring.

What is refactoring

Martin Fowler, author of Refactoring, defines refactoring as:

Refactoring, n: An adjustment to the internal structure of software to improve its comprehensibility and reduce its modification costs without changing its observable behavior. Refactoring (verb) : The use of 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 reconstruction and small reconstruction:

Large-scale refactoring: the refactoring of top-level code design, including: system, module, code structure, and the relationship between classes. The means of refactoring include: layering, modularization, decoupling, abstract reusable components and so on. The tools for this kind of refactoring are the design ideas, principles, and patterns we’ve studied. This kind of refactoring involves many code changes and has a large impact, so it is difficult and time-consuming, and the risk of introducing bugs is relatively high.

Minor refactoring: Refactoring of code details, mainly for classes, functions, variables and other code-level refactoring, such as normalizing naming and commenting, eliminating oversized classes or functions, extracting duplicate code, and so on. Small refactorings are more about using uniform coding specifications. This kind of refactoring requires more concentrated modifications, which are relatively simple, highly operable, time-consuming and less likely to introduce bugs. Refactoring should be done whenever a new feature development, bug fix, or code review “smells bad.” Continuous small refactoring in daily development can reduce the cost of refactoring and testing.

Bad code smell

  • Code duplication
    • The implementation logic is the same, the execution process is the same
  • Way too long
    • The statements in the method do not share the same level of abstraction
    • The logic is hard to understand and requires a lot of comments
    • Procedural programming instead of object oriented
  • Too much class
    • Classes do too many things
    • Contains too many instance variables and methods
    • The naming of the class is not enough to describe what is being done
  • Logic scattered
    • Divergent variation: A class often changes in different directions for different reasons
    • Scatter modification: When a change occurs, it needs to be made in multiple classes
  • Severe attachment
    • Methods of one class make excessive use of members of other classes
  • Data mud/basic type paranoia
    • Two class or method signatures contain the same field or parameter
    • You should use classes but primitive types, such as the Money class, which represents values and currencies, and the Range class, which represents starting and ending values
  • An irrational succession system
    • Inheritance breaks encapsulation and subclasses depend on the implementation details of a particular function in their parent class
    • Subclasses must evolve as their parent class is updated, unless the parent class is specifically designed for extension and is well documented
  • Too much conditional judgment
  • Too long parameter column
  • Too many temporary variables
  • Confusing temporary field
    • An instance variable is set only for a particular case
    • Extract the instance variables and corresponding methods into the new class
  • Pure data classes
    • Contains only fields and methods to access (read and write) those fields
    • This class is called a data container and should be kept to a minimum of variability
  • Improper naming
    • Naming does not accurately describe what is done
    • Naming does not conform to the convention commonly known
  • Too many comments or outdated comments

The problem with bad code

  • It is difficult to reuse
    • Too much system affinity makes it difficult to separate the reusable parts
  • Difficult to change
    • One change leads to many other changes, making the system unstable
  • Difficult to understand
    • The naming is messy, the structure is confusing, and it is difficult to read and understand
  • It is difficult to test
    • Branch, depend on more, difficult to cover comprehensive

What is good code

The evaluation of code quality is highly subjective, and there are many words to describe code quality, such as readability, maintainability, flexibility, elegance and simplicity. These terms measure code quality from different dimensions. Among them, maintainability, readability and extensibility are the most mentioned and the most important three evaluation criteria.

To write high-quality code, we need to master some more detailed, more practical programming methodology, which includes object-oriented design ideas, design principles, design patterns, coding specifications, refactoring skills, etc.

How to reconstruct

The principle of SOLID

Single responsibility principle

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

The single responsibility principle improves class cohesion by avoiding the design of large, all-in-one classes and coupling unrelated functions together. At the same time, the class responsibility is single, the class depends on and depends on other classes will be less, reduce the code coupling, in order to achieve high cohesion code, loose coupling. However, if you break it down too much, it actually does the opposite, reducing cohesion and the maintainability of your code.

Open – close principle

Adding new functionality should be done by extending the existing code (adding modules, classes, methods, properties, etc.) rather than modifying the existing code (modifying modules, classes, methods, properties, etc.).

The open close principle does not mean that changes are completely eliminated, but that new features are developed with minimal changes to the code.

Many design principles, ideas, and patterns are designed to make code more extensible. In particular, most of the 23 classic design patterns are summed up to solve the problem of code extensibility, and are guided by the open and closed principle. The most common approaches to code extensibility are polymorphism, dependency injection, interface-based programming rather than implementation, and most design patterns (e.g., decorators, policies, templates, chains of responsibility, and state).

Richter’s substitution principle

An object of subtype/derived class can replace an object of base/parent class anywhere in a program, In addition, the logic behavior of the original program is guaranteed to remain unchanged and the correctness is not destroyed.

A subclass can extend the functionality of the parent class, but cannot change the functionality of the parent class

Any method implemented in a parent class (as opposed to an abstract method) actually sets a set of specifications and contracts, and while it does not force all subclasses to follow these contracts, any modification of these nonabstract methods by subclasses can break the inheritance system.

Interface Isolation Principle

The caller should not rely on interfaces it does not need; The dependency of one class on another should be based on the smallest interface. The interface isolation principle provides a criterion for determining whether an interface has a single responsibility: indirectly by how callers use the interface. If the caller uses only part of the interface or part of the function of the interface, the design of the interface is not responsible enough.

Dependency inversion principle

A high-level module should not depend on a low-level module; both should depend on its abstraction; Abstractions should not depend on details, details should depend on abstractions.

Demeter’s rule

One object should have minimal knowledge of other objects

Principle of composite reuse

Try to use composition/aggregation rather than inheritance.

The single responsibility principle tells us to implement a class with a single responsibility; Richter’s substitution tells us not to break the inheritance system; The dependency inversion principle tells us to program for interfaces; The principle of interface isolation tells us to keep interfaces simple and simple when designing them. Demeter’s rule tells us to reduce coupling. The open and closed principle is the general principle, telling us to be open to expansion, closed to modification.

Design patterns

Design patterns: Solutions to common problems faced by software developers during software development. These solutions have been developed by numerous software developers over a long period of trial and error. Each pattern describes a recurring problem around us and the core solution to that problem.

  • Creation type: mainly solves the object creation problem, encapsulates the complex creation process, decouples the object creation code and the usage code
  • Structural type: mainly through different combinations of classes or objects, decouple the coupling of different functions
  • Behavioral: Addresses coupling of interaction behavior between classes or objects
type model instructions Applicable scenario
Create a type The singleton A class allows you to create only one instance or object and provide it with a global access point Stateless/globally unique/controlled resource access
The factory Create one or more related objects, regardless of the specific implementation class Separate object creation and use
The builders Used to create a complex object of a type that is “customized” by setting different optional parameters Object has many and many optional construction parameters
The prototype Create a new object by copying an existing object Object creation costs are high and there is little difference between objects of the same class
structured The agent Add functionality to an original class by introducing a proxy class without changing the original class or using inheritance Add proxy access, such as monitoring, caching, limiting, transactions, and RPC
decorator Dynamically extend the functionality of an original class by composition without changing the original class or using inheritance Dynamically extend the functionality of a class
The adapter Adapt the original class to the new interface by composition without changing it Reuse existing classes, but do not match the desired interface
The bridge When a class has multiple dimensions that vary independently, it can be extended independently through composition There are multiple dimensions of inheritance system
The facade Define a higher-level interface for a set of interfaces in a subsystem to make it easier to use Resolve the contradiction between interface reuse (fine granularity) and interface ease of use (coarse granularity)
combination Group objects into a tree structure to represent a partial-whole hierarchy, unifying the processing logic of individual pairs and groups of objects Satisfies the tree structure of part and whole
The flyweight Use sharing techniques to effectively support a large number of fine-grained objects When the system has a large number of objects, many fields of these objects have fixed values
Behavior type The observer Multiple observers listen to the same topic object and notify all observers when the topic object’s state changes, enabling them to update themselves automatically Decouple event creator and receiver
The template Define the skeleton of an algorithm in an operation, deferring some step implementation to subclasses Solve reuse and extension problems
strategy Define a set of algorithm classes and encapsulate each algorithm individually so that they can be replaced with each other Eliminate all kinds of if-else branch judgments

Definition, creation, and use of decoupling policies
state Allows an object to change its behavior when its internal state changes Separate the state and behavior of an object
cor A set of objects is concatenated into a chain, along which the request is passed until an object can handle it Decouple the sender and receiver of a request
The iterator Provides a way to sequentially access individual elements of a collection object without exposing the internal representation of the object Decouple the internal representation of a collection object from traversal access
The visitor Encapsulates operations that operate on elements of a data structure and defines new operations that operate on those elements without changing the data structure. Separate an object’s data structure from its behavior
The memo To capture the internal state of an object and save the state outside of the object so that the object can be restored to its previous state later, without violating the encapsulation principle Used for object backup and recovery
The command Encapsulate different requests into corresponding command objects, which control the execution of commands and are transparent to users Used to control the execution of commands, such as asynchrony, delay, queuing, undo, store, and undo
The interpreter Define a syntax representation for a language, and define an interpreter to handle the syntax Used in specific scenarios such as compilers, rule engines, regular expressions, etc
The mediation Define a single mediation object that encapsulates the interaction between a group of objects, avoiding direct interaction between objects It loosens the coupling of objects by eliminating the need to explicitly refer to each other

Layer code

Module structure Description

  • Server_main: Configuration layer, responsible for module management of the whole project, Maven configuration management, resource management, etc.

  • Server_application: Application access layer, which accepts external traffic, such as RPC interface implementation, message processing, and scheduled tasks. Do not include business logic here;

  • Server_biz: Core business layer, use case services, domain entities, domain events, etc

  • Server_irepository: Resource interface layer, responsible for resource interface exposure

  • Server_repository: Resource layer, responsible for proxy access to resources, unified access to external resources, and isolation of changes. Note: the emphasis here is weak business, strong data;

  • Server_common: Common layer, VO, tools, etc

Code development follows the specifications of each layer and pays attention to the dependencies between the layers.

Naming conventions

A good name should satisfy the following two constraints:

  • Describe exactly what was done
  • The format conforms to general conventions

If you find a class or method hard to name, it may be that it hosts too many functions and needs to be broken up further.

A convention commonly known as a convention

scenario Strong constraint The sample
The project name All lowercase, multiple words separated by underscore ‘-‘ spring-cloud
The package name All lowercase com.alibaba.fastjson
Class name/interface name Capitalize the first letter ParserConfig,DefaultFieldDeserializer
The variable name Lowercase letter: When multiple words are composed, all words must start with a uppercase letter except the first word password, userName
Constant names All caps, multiple words, separated by ‘_’ CACHE_EXPIRED_TIME
methods With variable read(), readObject(), getById()

Class name

Class names use the big hump nomenclature, and class names usually use nouns or noun phrases. In addition to nouns and noun phrases, an interface name can also use adjectives or adjective phrases, such as Cloneable, Callable, etc., to indicate that the class implementing the interface has a certain function or capability.

scenario The constraint The sample
An abstract class Abstract or Base BaseUserService
Enumeration class Enum as suffix GenderEnum
Utility class Utils as a suffix StringUtils
Exception class The Exception end RuntimeException
Interface implementation class Interface name + Impl UserServiceImpl
Design pattern-related classes Builder, Factory, etc When you use a design pattern, you need to use the suffix of the corresponding design pattern, such as ThreadFactory
A class that handles a particular function The Handler, the Predicate, the Validator It’s a processor, it’s a validator, it’s an assertion, and these class factories have method names like Handle, predicate, validate
Classes at a specific level Controller, Service, ServiceImpl, Dao suffix UserController, UserServiceImpl,UserDao
A value object of a particular level Ao, Param, Vo, Config, Message Param call input parameter; Ao returns the result for thrift; Vo universal value object;

Config Config class; Message is an MQ Message
The test class The Test end UserServiceTest, which is used to test the UserService class

Method named

Method names are small humps, with the first letter in lower case and each subsequent word capitalized. Unlike class names, method names are usually verbs or phrasal verbs, which together with parameters or parameter names form verb-object phrases, i.e., verbs + nouns. A good function name usually tells you directly what the function does.

scenario The constraint The sample
Returns true or false values is/can/has/needs/should isValid/canRemove
Used to check ensure/validate ensureCapacity/validateInputs
According to the need to perform IfNeeded/try/OrDefault/OrElse drawIfNeeded/tryCreate/getOrDefault
Data related to get/search/save/update/batchSave/

batchUpdate/saveOrUpdateselect

/insert/update/delete
getUserById/searchUsersByCreateTime
The life cycle initialize/pause/stop/destroy initialize/pause/onPause/stop/onStop
Pairs of common verbs Split /join, inject/extract, bind/seperate,

Increase /decrease, lanch/run, observe/listen, build/publish,

Encode /decode, submit/commit, push/pull, Enter /exit,

Expand/collapse, encode/decode

Refactoring technique

Refining method

Multiple methods have duplicate code, code within a method is too long, or statements within a method are not at the same level of abstraction. Method is the minimum granularity of code reuse, too long method is not conducive to reuse, low readability, refining method is often the first step of the reconstruction work.

Intent-oriented programming: Separating the process of doing something from how it is done.

  • Break down a problem into a series of functional steps and assume that those functional steps have already been implemented
  • We can solve this problem by grouping the functions together
  • After organizing the entire functionality, we implement each method function separately
/** * 1. The transaction starts with a standard ASCII string. * 2. This information string must be converted to an array of strings containing 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 different way (with different algorithms) than small transactions to improve efficiency. * 5. If the commit succeeds, the API returns "true"; On failure, false is returned. * /
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);        
    }        
    returnresult; }}Copy the code

Replace functions with function objects

Put functions into a single object, so that local variables become fields within the object. You can then break this large function into smaller functions on the same object.

Introducing parameter objects

If a method has too many parameters, encapsulate the parameters as parameter objects

Removes an assignment to a parameter

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; 
}
Copy the code

Separate queries from modifications

Any method that returns a value should have no side effects

  • Do not call write operations in convert to avoid side effects
  • The common exception is to cache the query results locally

Remove unnecessary temporary variables

Temporary variables are used only once or value logic costs are low

Introduce explanatory variables

Put the result of a complex expression (or part of it) into a temporary variable whose name explains the expression’s purpose

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 
}
Copy the code

Use guard statements instead of nested conditional judgments

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

// No guard statement is used
public void getHello(int type) {
    if (type == 1) {
        return;
    } else {
        if (type == 2) {
            return;
        } else {
            if (type == 3) {
                return;
            } else{ setHello(); }}}}// use a guard statement
public void getHello(int type) {
    if (type == 1) {
        return;
    }
    if (type == 2) {
        return;
    }
    if (type == 3) {
        return;
    }
    setHello();
}
Copy the code

Use polymorphic substitution conditions to judge the fault

When such a conditional expression exists, it selects different behaviors depending on the object type. You can put each branch of such an expression into a copy function within 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;
}
Copy the code

When there is a lot of type checking and judging, if else (or switch) statements tend to be bulky, which undoubtedly reduces the readability of the code. In addition, if else (or switch) is itself a “point of change”. When a new type needs to be extended, we have to append the if else (or switch) statement block and the corresponding logic, which makes the program less extensible and violates the object-oriented open and close principle.

Based on this scenario, we can consider using “polymorphisms” instead of verbose conditional judgments, encapsulating “change points” in if else (or switch) into subclasses. This eliminates the need for if else (or switch) statements, and instead uses instances of subclass polymorphism, which improves code readability and extensibility. Many design patterns use this pattern, such as policy patterns and state patterns.

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);
}
Copy the code

Use exception substitution to return the error code

Handle abnormal business status by throwing 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
  • Use standard exceptions whenever possible
  • Avoid throwing exceptions in finally blocks
    • If both exceptions are thrown, the call stack for the first exception is lost
    • The finally block should only do things like close resources
// Use error codes
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;
}
Copy the code

Introduction of assertions

A piece of code needs to make certain assumptions about the state of the program in order to assert explicit representation of that assumption.

  • Do not abuse assertions, do not use it to check for “should be true” conditions, only for “must be true” conditions
  • Does the code still work if the constraints indicated by the assertion are not met? Remove assertions if you can

Introduce Null objects or special objects

When using an object returned by a method that may be null, nulls the object before operating on it, or a null pointer is reported. When this judgment occurs frequently in code everywhere, it can affect the beauty and readability of the code, and even increase the chance of bugs.

The problem of empty references is unavoidable in Java, but it can be ameliorated by code programming techniques that introduce empty objects.

// Empty object example
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"); }}Copy the code

Derived classes

According to the single responsibility principle, a class should have clear boundaries of responsibility. But in practice, classes are constantly expanding. When adding a new responsibility to a class, you may not feel it is worth separating out a separate class. As a result, as the responsibilities increase, the class contains a lot of data and functions, and the logic is complex and difficult to understand.

At this point you need to consider which parts to separate into a single class, depending on the principle of high cohesion and low coupling. If some data and methods always appear together, or if some data often changes at the same time, this is a good indication that they should be in the same class. Another signal is how a class is subclassed: if you find that subclassing affects only part of the class’s features, or that the class’s features need to be subclassed differently, that means you need to break up the original class.

/ / the original 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; }}Copy the code

Composition takes precedence over inheritance

Inheritance is a powerful tool for code reuse, but it’s not always the best tool to do the job, and when used incorrectly, software can become vulnerable. Unlike method calls, inheritance breaks encapsulation. A subclass depends on the implementation details of a particular function in its parent class, and if the parent’s implementation changes from release to release, a subclass may be broken, even if its code does not change at all.

For example, suppose you have a program that uses a HashSet. 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 variation 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() {
        returnaddCount; }}Copy the code

By adding a private domain to a new class that references an instance of an existing class, this design is called composition, because the existing class becomes a component of the new class. The resulting class will be robust and independent of the implementation details of the existing class. Even if an existing class adds a new method, the new class will not be affected. Many design patterns use this pattern, such as the proxy pattern and decorator pattern

// 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() {
        returnaddCount; }}Copy the code

How to choose between inheritance and combination

  • Inheritance is appropriate only if a subclass is truly a child type of a parent class. For two classes A and B, class B should inherit from CLASS A only if there is A real “IS-A” relationship between them.
  • Using inheritance inside a package is very safe, with the implementation of subclasses and superclasses under the control of the same programmer;
  • Using inheritance is also very safe for classes that are designed specifically for inheritance and are well documented;
  • In other cases, the combinatorial approach should take precedence

Interfaces are superior to abstract classes

Java provides two mechanisms for defining types that allow multiple implementations: interfaces and abstract classes. Since Java8 added default methods to interfaces, both mechanisms allow implementation of instance methods. The main difference is that in order to implement a type defined by an abstract class, the class must be called a subclass of the abstract class. Because Java allows only 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 a non-hierarchical type framework.

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

  • Interface variable modifiers must be public static final
  • Interface method modifiers must be public
  • There is no constructor for the interface, and there is no this
  • It is possible to add default methods to an existing interface, but there is no guarantee that these methods will work well in a pre-existing implementation.
    • Because these default methods are injected into existing implementations, their implementers are unaware of and do not have permission to do so

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

  • For interface evolution
    • Before Java 8, we knew that all methods of an interface had to be subclassed (not an abstract class, of course), but after Java 8 the default methods of an interface could be optionally not implemented, and the above operations could be compiled at compile time. This avoids project compilation errors when upgrading from Java 7 to Java 8. Java8 adds a number of new default methods to the core collection interface, primarily to facilitate lambda.
  • You can reduce the creation of third-party utility classes
    • The List interface provides replaceAll(UnaryOperator), sort(Comparator), spliterator(), and other default methods. These methods are created inside the interface. You avoid creating utility classes for these methods.
  • You can avoid creating base classes
    • Prior to Java 8, we might have needed to create a base class for code reuse, but with default methods, we didn’t need to do that.

Interfaces cannot completely replace abstract classes because of their limitations and design purposes. But by providing an abstract skeleton implementation class for an interface, you can combine the benefits of an interface with an abstract class. Interfaces are responsible for defining types and perhaps providing some default methods, while skeleton implementation classes are responsible for implementing non-primitive interface methods in addition to primitive interface methods. The extension skeleton implementation takes up most of the work outside of implementing the interface. This is the Template Method design pattern.

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

Abstract class AbstractProtocol: Encapsulates the Exporter after the exposure service and the Invoker instance after the reference service, and implements the logic of service destruction

Specific implementation class XxxProtocol: implements export exposure service and refer reference service specific logic

Give priority to generics

Classes or interfaces that declare one or more type parameters are generic classes or interfaces. Generic classes and interfaces are collectively referred to as generic Types. Generics were introduced from Java 5 to provide compile-time type-safety checking. The nature of generics is parameterized typing, where a parameter represents the data type being operated on, and the type range of this parameter can be limited. The advantage of generics is compile-time type detection, avoiding type conversions.

// Compare three values and return the maximum
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 greater
  }   if ( z.compareTo( max ) > 0 ) {     
    max = z; // Now z is bigger
  }   return max; // Return the largest object
}

public static void main( String args[] ) {   
  System.out.printf( "The largest number in %d, %d, and %d is %d\n\n".3.4.5, maximum( 3.4.5 ));   
  System.out.printf( "%.1f, %.1f and %.1f the largest number is %.1f\n\n".6.6.8.8.7.7,  maximum( 6.6.8.8.7.7 ));   
  System.out.printf( "The largest number in %s, %s and %s is %s\n"."pear"."apple"."orange", maximum( "pear"."apple"."orange")); }Copy the code

Do not use native types

In order to maintain Java code compatibility, support and native type conversion, and use the erasing mechanism to implement generics. But if you use a primitive type, you lose the benefits of generics and are warned by the compiler.

Eliminate every non-inspection warning as much as possible

Each warning indicates that a ClassCastException may be thrown at runtime. Do your best to dispel these warnings. Warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning warning

Use restricted wildcards to increase the flexibility of the API

Parameterized types do not support covariant, that is, List is neither a subtype nor a superclass of List for any two different types, Type1 and Type2. To address this problem and increase flexibility, Java provides a special parameterized type called a restricted wildcard type, List<? Extends E > and List <? Super E >. The principle is producer-extends and consumer-super (PECS). If you are both producer and consumer, there is no need to use wildcards.

There is also a special unrestricted wildcard, List<? >, indicating some type but not certain. Commonly used as a reference to a generic type, to which no object other than Null may be added.

//List<? extends E>
// Number is 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 = newArrayList<Integer>(); ,// Number is the parent of Integer
List<? super Integer> array = new ArrayList<Number> ();// Object is the parent of Integer
List<? super Integer> array = new ArrayList<Object> (); publicstatic <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()); }}}Copy the code

Static member classes are superior to non-static member classes

A nested class is a class defined inside another class. A nested class exists only to serve its external classes, and should be a top-level class if it is used in other environments. There are four types of nested classes: static member class, nonstatic Member Class, anonymous Class, and Local class. All but the first are called inner classes.

Anonymous Classes

No name, declaration and instantiation, can only be used once. When present in a non-static environment, a reference to an external class instance is held. It is commonly used to create function and procedure objects, although lambdas are now preferred.

Local Class

Local classes can be declared anywhere local variables can be declared, while following the same scoping rules. Unlike anonymous classes, names can be used repeatedly. In practice, however, local classes are rarely used.

Static Member Class

The simplest type of nested class, declared inside another class, is a static member of that class that follows the same accessibility rules. The common usage is as a public helper class that makes sense only with its external classes.

Nonstatic Member Class

Although syntactically the only difference from static member classes is that the class declaration does not contain static, there is a big difference. Each instance of a non-static member class is implicitly associated with an instance of an external class, with access to its member properties and methods. In addition, an instance of an external class must be created before an instance of a non-static member class can be created.

All in all, these four nested classes have their uses. Given that the nested class is inside a method, make it anonymous if you only need to create instances in one place and already have a predefined type that characterizes the class. Member classes should be used if a nested class needs to remain visible outside of a single method, or if it is too long to fit inside a method. If each instance of a member class needs a reference to its peripheral instance, make it non-static; otherwise, make it static.

Templates/utility classes are preferred

By abstracting and encapsulating the code logic of common scenarios, the corresponding template tool class can greatly reduce the repeated code, focus on the business logic, and improve the code quality.

Separate object creation and use

Object – oriented programming has more instantiation than procedure – oriented programming, and object creation must specify the concrete type. The common approach is “create where you use it”, using the same piece of code that creates the instance. This may seem 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}}Copy the code

The creator of an object is coupled to its concrete type, while the consumer of an object is coupled to its interface. That is, the creator cares about what the object is, and the user cares about what it can do. These two should be considered separate considerations, and they tend to change for different reasons.

When the object type involves polymorphism and object creation is complex (with many dependencies), the object creation process can be considered to be separated, so that users do not need to pay attention to the details of object creation. This is the starting point for the creative pattern in design patterns, and real projects can use factory patterns, builders, and dependency injection.

Accessibility is minimized

An important factor in distinguishing a well-designed component is whether it hides its internal data and implementation details from external components. Java provides access control mechanisms 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) that appear in the entity declaration.

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

There are four levels of access for members (instance/domain, method, nested class, and nested interface), increasing accessibility as follows:

  • Private — the member is accessible only from inside the top-level class that declares it;
  • Package-level private (default) — the member is accessible to any class inside the package that declares it;
  • Protected — The member can be accessed by subclasses of the class that declares it, and by any class within the package that declares it;
  • Public (public modifier) — the member can be accessed anywhere;

Proper use of these modifiers is critical to achieving information hiding by making every class and member as inaccessible as possible (private or package-private). The advantage is that it can be modified, replaced, or removed in future releases without worrying about affecting existing client programs.

  • If a class or interface can be package-private, it should be;
  • If a package-level private top-level class or interface is used only inside a class, consider making it a private nested class for that class.
  • Public classes should not expose instance fields directly and should provide methods to preserve the flexibility to change the internal representation of the class in the future;
  • Once the class’s public API is identified, all other members should be made private;
  • If there is a lot of access between classes in the same package, consider redesigning to reduce this coupling;

Minimization of variability

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 remains constant throughout the life of the object. The advantage of immutable classes is that they are easy to use, thread-safe, freely shareable and error-prone. The Java platform class library contains many immutable classes, such as String, primitive wrapper classes, BigDecimal, and so on.

To make a class immutable, follow these five rules:

  • Declare that all fields are private
  • Declare all fields to be final
    • If a reference to a newly created instance is passed from one thread to another in the absence of synchronization, correct behavior must be ensured
  • No methods are provided that modify the state of the object
  • Ensure that classes are not extended (prevent subclassing, declare classes final)
    • Prevents careless or malicious subclasses from corrupting the immutable behavior of an object by pretending that its state has changed
  • Ensure mutually exclusive access to any mutable component
    • If a class has fields that point to mutable objects, you must ensure that clients of that class cannot obtain references to those objects. Also, never initialize such a domain with a client-supplied object reference or return it from any access method. Use protective copy techniques in constructors, access methods, and readObject methods

Some suggestions for minimizing variability:

  • Unless there is a good reason for a class to be mutable, it should be immutable;
  • If a class cannot be made immutable, its variability should still be limited as much as possible;
  • Make every domain private final unless there is a compelling reason to make it non-final;
  • The constructor should create a fully initialized object with all constraints established;

How to guarantee quality

Test-driven development

Test-driven development (TDD) requires that tests be at the center of the development process, that tests be written for code production before any code is written, and that code be written with the goal of passing the tests. TDD requires that tests be fully automated and must be run before and after refactoring the code.

The ultimate goal of TDD is clean code that works. Most developers don’t get clean, usable code most of the time. The solution is divide and conquer. Address the “availability” problem in the goal first, then the “clean code” problem. This is the opposite of architecture-driven development.

Another benefit of adopting TDD is that we have a detailed set of automated tests that accompany the code. Driven by this set of tests, our code will remain robust in the future whenever it needs to be maintained for any reason (requirements, refactoring, performance improvements).

TDD development cycle

Add a test -> Run all the tests and check the test results -> Write code to pass the tests -> Run all the tests and pass all the tests -> Refactor the code to eliminate duplicate design and optimize the design structure

Two basic principles

  • Write code only if a test fails and only if the test passes
  • Eliminate existing duplicates and optimize the design structure before writing the next test

Separation of concerns is another very important principle implicit in these two rules. This means achieving the goal of “usable” code in the coding phase and “clean” code in the refactoring phase, focusing on one thing at a time!

Stratified test point

Test type The target Testing and determination of results
The Dao test Verify the correctness of mybatis-config, mapper, and Handler Memory based database

You can use assert to verify
Adapter test Verify that the external dependency interaction is correct

Verify converter is correct
Depend on external environment

Accuracy depends on manual interpretation
The Repository test Verify internal calculation and transformation logic Mock external dependencies

You can use assert to verify
Biz layer test Validate internal business logic Isolate as many external dependencies as possible

Multiple tests are required, each verifying a scenario or branch

Use Assert to verify without relying on human judgment
Application layer test Verify that entry parameters are handled correctly

Verify that links in the system are not blocked
You can isolate external dependencies

Scenario coverage is controlled by parameters

You can use single-step debugging to see where the code is going

Verbose logic is not validated

The resources

Refactoring – Improving the design of existing code

Design patterns

Effective Java

Agile software development and design best practices

Implementation pattern

Test-driven development