I. Introduction
Although software development has always been committed to the pursuit of efficient, readable, and easy-to-maintain features, these features are like an impossible triangle, intertwined with each other, and one is ebbing and the other is ebbing. Just like low-level languages (such as assembly and C language) can maintain efficient running performance, but have shortcomings and disadvantages in readability and maintainability; while high-level languages (such as Java and Python) have shortcomings and disadvantages in readability and maintainability. It performs well in terms of performance, but falls short in terms of execution efficiency.
Building the advantages of the language ecology and making up for its shortcomings has always been an evolution direction of programming languages.
Different programming languages have different features and protocols. Let’s take the JAVA language as an example to break down the knowledge points and practical skills that are easily overlooked during the development process but must be mastered.
2 Basics
In 1999, NASA's Mars mission failed: In this mission, the flight system software on the Mars Climate Explorer used the metric unit Newton to calculate thruster power, while the direction correction amount and thrust input by ground personnel The detector parameters used the British unit of pound force, which caused the detector to enter the atmosphere at an incorrect height and eventually disintegrated.
This was an accident caused by the conflict between international standards (cattle) and localization (pounds). This leads to the topic that programs need to pay attention to maintainability. Since software production often requires the collaboration of multiple people, maintainability is an important part of collaborative consensus. Regarding this aspect, the two aspects that people most easily think of are naming and annotation. Let’s discuss them below.
2.1 About naming
According to reading habits, the variable naming method of the program needs to overcome the space problem between words, so as to connect different words together, and ultimately achieve the effect of creating a new "word" that is easy to read. Common naming methods include the following:
- Snake case : Also called underline naming, use underscores and lowercase words, for example: my_system;
- Camel case (camel case): Case-sensitive according to the first letter of the word, and can be subdivided into large camel case and small camel case, such as: MySystem, mySystem;
- Hungarian nomenclature (HN case): attribute + type + description, such as: nLength, g_cch , hwnd;
- Pascal case (Pascal case): all first letters are capitalized, which is equivalent to the big camel case nomenclature, such as: MySystem;
- Spinal case : use a dash, for example: my-system;
- Studly caps: mixed case, no concise rules, such as: mySYSTEM, MYSystem;
Ranked according to audience size and popularity, camel case nomenclature and snake nomenclature are more popular. After all, they have advantages in readability and ease of writing.
2.1.1 Naming dictionary
Knowing the meaning after seeing the name: A good naming is a kind of comment.
It is recommended that R&D students memorize the names of common business scenarios in the industry. Of course, someone has already summarized them for us, so I won’t go into too much explanation here. Here is an excerpt for your reference:
Management class naming : Bootstrap, Starter, Processor, Manager, Holder, Factory, Provider, Registrar, Engine, Service, Task
Propagation class naming : Context, Propagator
Callback class naming : Handler, Callback, Trigger, Listener, Aware
Monitoring class naming : Metric, Estimator, Accumulator, Tracker
Memory management class naming : Allocator, Chunk, Arena, Pool
Filter detection class naming : Pipeline, Chain, Filter, Interceptor, Evaluator, Detector
Structural class naming : Cache, Buffer, Composite, Wrapper, Option, Param, Attribute, Tuple, Aggregator, Iterator, Batch, Limiter
Common design pattern naming : Strategy, Adapter, Action, Command, Event, Delegate, Builder, Template, Proxy
Parsing class naming : Converter, Resolver, Parser, Customizer, Formatter
Network class naming : Packet, Encoder, Decoder, Codec, Request, Response
CRUD naming : Controller, Service, Repository
Auxiliary class naming : Util, Helper
Other class naming : Mode, Type, Invoker, Invocation, Initializer, Future, Promise, Selector, Reporter, Constants, Accessor, Generator
2.1.2 Naming practices
What are the common naming rules for projects ? Different languages may have different habits. Take the camel case naming convention of the Java language as an example:
The specification is relatively abstract. Let’s first take a look at what are the bad names?
In addition to standard specifications, there are also some actual cases that trouble us during the actual development process.
1. When defining a member variable, should you use a wrapper type or a basic data type?
The default values of wrapper classes and basic data types are different. The former is null, and the default values of the latter are different depending on the type. From a data rigorous perspective, the null value of the wrapper class can represent additional information, making it safer. For example, it can avoid the NPE risk caused by basic types of automatic unboxing and the risk of abnormal business logic processing. Therefore, member variables must use wrapper data types, and basic data types are used in the context of local variables.
2. Why is it not recommended that Boolean type member variables start with is?
There are actually clear regulations on the definition of getter/setter methods in Java Beans. According to the JavaBeans(TM) Specification, if it is an ordinary parameter named propertyName, its setter/getter needs to be defined in the following way:
public <PropertyType> get<PropertyName>();
public void set<PropertyName>(<PropertyType> p)
However, the Boolean type variable propertyName follows a different set of naming principles:
public boolean is<PropertyName>();
public void set<PropertyName>(boolean p)
Due to differences in the way various RPC frameworks and object serialization tools handle Boolean type variables, it is easy to cause code portability problems. There are compatibility issues between the most common json serialization libraries Jackson and Gson. The former traverses all getter methods in the class through reflection and obtains the properties of the object through method name interception, while the latter directly uses reflection Iterate over the properties in this class. In order to avoid the impact of this difference on business, it is recommended that all member variables do not start with is to prevent unpredictable serialization results.
3. See what side effects can be caused by word case?
The JAVA language itself is case-sensitive, but when operating files with file paths and file names, the file names and paths here are not case-sensitive. This is because the file system is not case-sensitive. A typical scenario is that when we use a code management platform such as git and change the uppercase file name in the package path to lowercase, git cannot be updated. In order to avoid unnecessary trouble, it is recommended to use lowercase words in the package path. Multiple words are defined through a path hierarchy.
4. Will classes in different jar packages conflict with each other?
Here we focus on the second situation. This situation is likely to occur when the system is split and reconstructed. The original project is copied and then deleted, resulting in some tools or enumeration classes having the same paths and names as the original ones. , when third-party callers rely on these two systems at the same time, it is easy to lay a trap for future iterations. To avoid such problems, you must create a unique package path for the system.
Supplement: If you rely on third-party libraries and there is a class conflict, you can solve the jar package conflict by introducing the third-party library jarjar.jar and modifying the package name of one of the conflicting jar files.
5. How to make a trade-off between the readability of variable naming and the resources (memory, bandwidth) occupied?
You can use object serialization tools as a breakthrough, taking the common Json (Jackson) serialization method as an example:
public class SkuKey implements Serializable {
@JsonProperty(value = "sn")
@ApiModelProperty(name = "stationNo", value = " 门店编号", required = true)
private Long stationNo;
@JsonProperty(value = "si")
@ApiModelProperty(name = "skuId", value = " 商品编号", required = true)
private Long skuId;
// 省略get/set方法
}
The function of the @JsonProperty annotation is to rename the common properties in JavaBean to the specified new name during serialization. This implementation has no impact on business implementation. The original naming operation still prevails. It only takes effect when external RPC requires serialization and deserialization. In this way, the conflict between readability and resource occupation is better solved.
6. To provide input and output parameters for external services, should we use class objects or Map containers?
From a flexibility perspective, Map containers are stable and more flexible. From the perspective of stability and readability, the Map container is a black box. You don’t know what is inside. You must have auxiliary detailed documentation to collaborate. Since the actions of maintaining documents are often separated from the engineering code, the mechanism will As a result, it is difficult to ensure the accuracy and timeliness of information. Therefore, it is still recommended to use class structure objects to maintain the incoming and outgoing parameter structures.
2.2 About comments
Comments are an important means of communication between programmers and readers. They are explanations and explanations of the code. Good comments can improve the readability of the software and reduce the cost of maintaining the software.
2.2.1 Good comments
Hierarchical : Annotate and explain each with its own focus according to different granularities such as system, package, class, method, code block, code line, etc.
There are specifications : good code is better than a lot of comments, which is the same reason as we often say "convention is greater than configuration". It is a good standard way to use third-party libraries such as swagger to implement annotations, that is, interface documents ;
2.2.2 Bad comments
In order to make comments accurately and clearly express functional logic, the maintenance of comments has considerable maintenance costs, so the more comments, the more detailed the better. Here are some bad annotation scenarios to assist understanding:
2.3 About layering
The main purpose of system layered design is to reduce the complexity of the system by separating concerns, while improving reusability and reducing maintenance costs. Therefore, if you understand the concept of layering, the maintainability of the system will have a skeleton to a large extent.
2.3.1 System layering
Before ISO (International Standardization Organization) formulated the seven-layer network communication model (Open System Interconnection Reference Model, OSI/RM) in 1981, there were many architectures in computer networks, among which IBM's SNA (System Network Architecture) Structure) and DEC's DNA (Digital Network Architecture) digital network architecture are the most famous.
Previously, the different standards proposed by each manufacturer were based on their own equipment. Users could only use products from the same company when choosing products, because different companies have different standards and may have different working methods. The result is that there may be incompatibility between network products from different manufacturers. If the products of the same company can meet the needs of users, then it depends on which company is stronger, stronger, and more user-friendly. Naturally, users will not say anything. The problem is that one company is not right. All products excel. This will lead to painful suffering for both manufacturers and users. By analogy with the current mobile phone charging interface protocols (Micro USB interface, Type-c interface, Lightning interface), if you always have various charging cables on hand, you can deeply understand the meaning of the standard.
2.3.2 Software scalability
Software scalability refers to the ability of a software system to maintain its original performance and expand to support more tasks when facing load pressure.
Scalability can have two aspects, vertical scalability and horizontal scalability. Vertical scalability improves the throughput of the system by adding resources in the same business unit, such as increasing the number of server CPUs, increasing server memory, etc. Horizontal scalability is achieved by adding multiple business unit resources so that all business units logically act like one unit. For example, the ejb distributed component model, microservice component model, etc. all belong to this method.
Software systems need to consider effective scalability design when designing to ensure sufficient performance support when facing load pressure.
From a scalability perspective, system layering falls more into the category of horizontal scalability. In J2EE system development, we generally adopt a layered architecture, which is generally divided into presentation layer, business layer and persistence layer. After layering is adopted, because communication between layers will cause additional overhead, what will be brought to our software system is that the processing overhead of each business will increase.
Since layering will bring additional overhead, why do we need to layer it?
This is because there is an upper limit to improving software performance and throughput by simply relying on vertical scaling of heap hardware resources, and as the system scale expands, the cost of vertical scaling will also become very expensive. When layering is adopted, although communication overhead is incurred between layers, it is beneficial to the horizontal scalability of each layer, and each layer can be independently scaled without affecting other layers. That is to say, when the system needs to cope with a larger amount of access, we can increase the system throughput by adding multiple business unit resources.
2.4 Summary
This chapter mainly talks about the need to achieve a unified consensus on naming and annotation during the development process from the aspects of readability and maintainability. In addition to consensus, the isolation of concerns also needs to be done at the design level. This includes the splitting of system responsibilities, the division of module functions, the convergence of class capabilities, and the relationship between entity structures all need to be well planned.
Three Practice Chapters
Let’s learn from those classic practical cases from several important quality indicators such as program scalability, maintainability, security and performance.
3.1 Class definition
3.1.1 Constant definition
A constant is a fixed value that does not change during program execution. You can use enumerations (Enum) or classes (Class) to define constants.
If you need to define a group of related constants, then using an enumeration is more appropriate. Enumerations have greater advantages in terms of safety and operability (supporting traversal and function definition).
public enum Color {
RED, GREEN, BLUE;
}
If you only need to define one or a few read-only constants, it is more concise and convenient to use class constants.
public class MyClass {
public static final int MAX_VALUE = 100;
}
3.1.2 Tools
Tool classes usually contain common public methods in a certain non-business field. They do not require supporting member variables and are only used as tool methods. Therefore, it is most appropriate to make it a static method, which does not require instantiation and can only be used to obtain the definition of the method and call it.
The reason why the tool class is not instantiated is to save memory space, because the tool class provides static methods that can be called through the class, and there is no need to instantiate the tool class object.
public abstract class ObjectHelper {
public static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
}
In order to achieve constraints that do not require instantiated objects, it is best to add the abstract keyword to qualify the declaration when defining the class . This is why most open source tool classes such as spring are modified with the abstract keyword.
3.1.3 JavaBean
There are two common implementation methods for JavaBean definition: manual writing and automatic generation.
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Use the lombok plug-in to enhance Java code writing through annotations, and dynamically generate get and set methods during compilation.
import lombok.Data;
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class Person {
private String name;
private int age;
}
The plug-in package also provides practical chain programming capabilities such as @Builder and @Accessors, which can improve coding efficiency to a certain extent.
3.1.4 Immutable classes
In some scenarios, in order to ensure the stability and consistency of its functions and behaviors, classes are designed so that they cannot be inherited and overridden.
The way to define it is to add the final keyword to the class, example:
public final class String implements Serializable, Comparable<String>, CharSequence {
}
The following are some classes that cannot be inherited and overridden, which are used in some underlying middleware:
java.lang.String
java.lang.Math
java.lang.Boolean
java.lang.Character
java.util.Date
java.sql.Date
java.lang.System
java.lang.ClassLoader
3.1.5 Anonymous inner classes
Anonymous inner classes are usually used to simplify code. Its definition and use usually occur in the same place. Its usage scenarios are as follows:
public class Example {
public static void main(String[] args) {
// 创建一个匿名内部类
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello, World!");
}
};
// 调用匿名内部类的方法
runnable.run();
}
}
3.1.6 Declaring a class
A declaration class is a basic type or interface in the Java language, used to define the behavior or characteristics of the class. Some are even just a declaration without specific method definitions.
3.1.7 Record class
The Record class was previewed in Java14 and was not officially released until Java17. According to the description of JEP395, the Record class is a carrier of immutable data, similar to various model, dto, vo and other POJO classes that are widely used today, but the record itself is no longer assignable after construction. All record classes inherit from java.lang.Record. The Record class provides a full field constructor, attribute access, and equals, hashcode, and toString methods by default. Its function is very similar to the lombok plug-in .
Defined way
/**
* 关键定义的类是不可变类
* 将所有成员变量通过参数的形式定义
* 默认会生成全部参数的构造方法
* @param name
* @param age
*/
public record Person(String name, int age) {
public Person{
if(name == null){
throw new IllegalArgumentException("提供紧凑的方式进行参数校验");
}
}
/**
* 定义的类中可以定义静态方法
* @param name
* @return
*/
public static Person of(String name) {
return new Person(name, 18);
}
}
Usage
Person person = new Person("John", 30);
// Person person = Person.of("John");
String name = person.name();
int age = person.age();
scenes to be used
Construct a temporary storage object through Record and sort the Person array objects according to age.
public List<Person> sortPeopleByAge(List<Person> people) {
record Data(Person person, int age){};
return people.stream()
.map(person -> new Data(person, computAge(person)))
.sorted((d1, d2) -> Integer.compare(d2.age(), d1.age()))
.map(Data::person)
.collect(toList());
}
public int computAge(Person person) {
return person.age() - 1;
}
3.1.8 Sealing type
The new feature Sealed Classes introduced in Java 17 is mainly used to limit the inheritance of classes. We know that there are two main types of restrictions on class inheritance functions:
But obviously, these two restrictions are very rough, and sealed classes are more fine-grained control of class inheritance.
sealed class SealedClass permits SubClass1, SubClass2 {
}
class SubClass1 extends SealedClass {
}
class SubClass2 extends SealedClass {
}
In the above example, SealedClass is a sealed class which contains two subclasses SubClass1 and SubClass2. In the definition of SubClass1 and SubClass2, you must use the extends keyword to inherit from SealedClass, and use the permits keyword to specify which subclasses they allow to inherit. By using a sealed class, you can ensure that only subclasses that meet certain conditions can inherit or implement the protocol or specification.
3.2 Method definition
3.2.1 Construction method
A constructor is a special method used to create and initialize objects. The name of the constructor must be the same as the class name and have no return type. When creating an object, you can call the constructor by using the new keyword.
public class MyClass {
private int myInt;
private String myString;
// 构造方法
public MyClass(int myInt, String myString) {
this.myInt = myInt;
this.myString = myString;
}
}
An important feature of implementing the singleton pattern is that users are not allowed to create (new) objects at will. How to achieve security control? Declare the constructor as private is an essential step.
3.2.2 Method rewriting
Method overriding refers to redefining a method with the same name in the parent class in the subclass. Method overriding allows a subclass to override the method implementation in the parent class to implement its own behavior based on the needs of the subclass.
class Animal {
public void makeSound() {
System.out.println("Animal is making a sound");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow");
}
}
public class Main {
public static void main(String[] args) {
Animal myCat = new Cat();
myCat.makeSound(); // 输出 "Meow"
}
}
One of the three major characteristics of object-oriented polymorphism, method rewriting is its core.
3.2.3 Method overloading
Multiple methods are defined in a class with the same name but different parameter lists. Method overloading allows us to use the same method name to perform different operations and execute different code logic based on the parameters passed to the method.
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
int result1 = calculator.add(2, 3);
double result2 = calculator.add(2.5, 3.5);
System.out.println(result1); // 输出 5
System.out.println(result2); // 输出 6.0
}
}
3.2.4 Anonymous methods
Java 8 introduced Lambda expressions, which can be used to implement functions similar to anonymous methods. A lambda expression is an anonymous function that can be passed as a parameter to a method or used directly as a standalone expression.
public static void main(String args[]) {
List<String> names = Arrays.asList("hello", "world");
// 使用 Lambda 表达式作为参数传递给 forEach 方法
names.forEach((String name) -> System.out.println("Name: " + name));
// 使用 Lambda 表达式作为独立表达式使用
Predicate<String> nameLengthGreaterThan5 = (String name) -> name.length() > 5;
boolean isLongName = nameLengthGreaterThan5.test("John");
System.out.println("Is long name? " + isLongName);
}
3.3 Object definition
3.3.1 Singleton object
A singleton object is an object that can be used repeatedly, but has only one instance. It has the following functions:
For example, use an enumeration to implement the singleton pattern:
public enum Singleton {
INSTANCE;
public void someMethod() {
// ...其他代码...
}
}
3.3.2 Immutable objects
Immutable objects in Java are objects whose state cannot be modified once created. Immutable objects are a very useful object because they ensure that the object's state is consistent at all times, thus avoiding problems caused by modifying the object's state. There are several ways to implement immutable objects:
Some container class operations also have corresponding wrapper classes to implement the immutability of container objects, such as defining immutable array objects:
Collections.unmodifiableList(new ArrayList<>());
When an object in the domain is passed out as an input parameter, it is defined as an immutable object. This is very important in maintaining data consistency. Otherwise, the unpredictability of object attribute changes will be very troublesome when locating problems.
3.3.3 Tuple object
Tuple is a common concept in functional programming languages. A tuple is an immutable object that can store multiple objects of different types in a type-safe form. It is a very useful data structure that allows developers to process multiple data elements more conveniently and efficiently. However, the native Java standard library does not provide tuple support, and we need to implement it ourselves or with the help of a third-party class library.
Two-tuple implementation
public class Pair<A,B> {
public final A first;
public final B second;
public Pair(A a, B b) {
this.first = a;
this.second = b;
}
public A getFirst() {
return first;
}
public B getSecond() {
return second;
}
}
Triplet implementation
public class Triplet<A,B,C> extends Pair<A,B>{
public final C third;
public Triplet(A a, B b, C c) {
super(a, b);
this.third = c;
}
public C getThird() {
return third;
}
public static void main(String[] args) {
// 表示姓名,性别,年龄
Triplet<String,String,Integer> triplet = new Triplet("John","男",18);
// 获得姓名
String name = triplet.getFirst();
}
}
Multiple group implementation
public class Tuple<E> {
private final E[] elements;
public Tuple(E... elements) {
this.elements = elements;
}
public E get(int index) {
return elements[index];
}
public int size() {
return elements.length;
}
public static void main(String[] args) {
// 表示姓名,性别,年龄
Tuple<String> tuple = new Tuple<>("John", "男", "18");
// 获得姓名
String name = tuple.get(0);
}
}
Tuple mainly has the following functions:
1. Store multiple data elements: Tuple can store multiple different types of data elements, which can be basic types, object types, arrays, etc.;
2. Simplify the code: Tuple can make the code more concise and reduce the writing of repeated code. Through Tuple, we can pack multiple variables into one object, thereby reducing the amount of code;
3. Improve code readability: Tuple can improve code readability. Through Tuple, we can pack multiple variables into one object, making the code more readable;
4. Support functions returning multiple values: Tuple can support functions returning multiple values. In Java, a function can only return one value, but through Tuple, we can package multiple values into an object and return it;
In addition to customization, third-party class libraries that implement the tuple concept include: Google Guava, Apache Commons Lang, JCTools, Vavr, etc.
The Google Guava library's Tuple provides more functionality and is widely used. For example, in order to make the meaning of tuples more clear, Guava provides the concept of named tuples. By giving tuples names, you can express the meaning of each element more clearly. Example:
NamedTuple namedTuple = Tuples.named("person", "name", "age");
3.3.4 Temporary objects
Temporary objects refer to objects that are temporarily needed during program execution but have a short life cycle. These objects typically exist only briefly during use and do not require long-term storage or reuse.
Optimization suggestions for temporary objects are as follows:
3.3.5 Valhalla
As a high-level language, there has always been a big gap in performance between Java and the lower-level C language and assembly language. To bridge this gap, the Valhalla project was launched in 2014 with the goal of bringing more flexible flat data types to JVM-based languages.
We all know that Java supports both native types and reference types. Native data types are passed by value. Assignment and function parameter passing will copy the value. After copying, there is no correlation between the two copies. Reference types are passed pointers in any case, and modifying the content pointed to by the pointer will affect All references. Valhalla introduced value types, a concept between primitive types and reference types.
Since most of the Java data structures in an application are objects, we can consider Java as a pointer-intensive language. This pointer-based object implementation is used to enable object identification, which itself is used for language features such as polymorphism, mutability, and locking. By default, these properties apply to every object regardless of whether they are actually needed. This is where value types come into play.
The concept of value types is to represent pure data aggregation, which removes the functionality of regular objects. So we have pure data, no identity. Of course, this means that we also lose the functionality that can be achieved using object identification. Since we no longer have an object identity, we can abandon pointers and change the general memory layout of value types. Let's compare object reference and value type memory layout.
The object header information is removed, and the value type saves 16 bytes of space in the object header in a 64-bit operating system. At the same time, it also means giving up the object's unique identity (Identity) and initialization security. The previous keywords or methods such as wait(), notify(), synchronized(obj), System.identityHashCode(obj), etc. will be invalid and cannot be used.
Valhalla will significantly improve performance and reduce leaky abstractions:
As of September 2023, the Valhalla project is still in progress and no official version has been released yet. This innovative project is worth looking forward to.
Four summary
This article summarizes the basic common sense often used in the software development process, and is divided into two chapters: basics and practice. The basics focuses on the naming conventions of classes, methods, variables, and the criteria for judging the quality of code comments. In the practical chapter, common technical concepts and implementation practices are analyzed from the three levels of classes, methods and objects. I hope that this common sense can bring some thinking and help to readers.
Lei Jun: The official version of Xiaomi's new operating system ThePaper OS has been packaged. The pop-up window on the lottery page of Gome App insults its founder. Ubuntu 23.10 is officially released. You might as well take advantage of Friday to upgrade! Ubuntu 23.10 release episode: The ISO image was urgently "recalled" due to containing hate speech. A 23-year-old PhD student fixed the 22-year-old "ghost bug" in Firefox. RustDesk remote desktop 1.2.3 was released, enhanced Wayland to support TiDB 7.4 Release: Official Compatible with MySQL 8.0. After unplugging the Logitech USB receiver, the Linux kernel crashed. The master used Scratch to rub the RISC-V simulator and successfully ran the Linux kernel. JetBrains launched Writerside, a tool for creating technical documents.Author: JD Retail Emily Liu
Source: JD Cloud Developer Community Please indicate the source when reprinting