What are the principles that make great code?

As a programmer, the least favorite thing to do, outside of meetings, is probably looking at other people’s code.

Sometimes, if you’re not in good health, you might faint with anger when you open the code on a new project.

Styles vary, there are no comments, and you can’t even indent the most basic formatting. The point of all this code may be to prove one thing: it’s not like you can’t run.

At this point, most programmers are thinking: this code really doesn’t want to change, so they should just rewrite it.

But sometimes, when we look at some well-known open source projects, we will say, the code is so nice and elegant. Why is it good? A little hard to say, but good.

So, this article will try to analyze the characteristics of good code and the principles that make good code.

The primary stage

Let’s start with some basic principles that any programmer, senior or junior, will take into account.

This is just a few of them, but there are many more, and I’ll pick four of them to illustrate them briefly.

  1. Unified format
  2. Naming conventions
  3. Comment is clear
  4. Avoid duplicate code

Here are some examples of Python code:

Unified format

Formatting unification involves many aspects, such as import statements, which need to be written in the following order:

  1. Python standard library modules
  2. Python Third-party Modules
  3. Application custom modules

Then separate each section with a blank line.

import os
import sys

import msgpack
import zmq

import foo
Copy the code

For example, to add appropriate Spaces, like this code;

i=i+1
submitted +=1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
Copy the code

The code is so tightly packed that it’s hard to read.

i = i + 1
submitted += 1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)
Copy the code

After adding whitespace, it immediately feels a lot clearer.

There are also things like indentation in Python, the position of braces in other languages, whether to put them at the end of a line, or whether to start a new line, all need to be consistent.

Having a uniform style will make your code look cleaner.

Naming conventions

Good naming doesn’t require comments, just a glance at the name tells you what a variable or function does.

Take this code for example:

a = 'zhangsan'
b = 0
Copy the code

A might be able to guess, but when you have a lot of code, if you have a screen full of A, B, C, and D, you don’t have to explode in place.

Changing the variable name slightly makes the semantics clearer:

username = 'zhangsan'
count = 0
Copy the code

There is a unified naming style. If you use the hump, use the hump for all of them, underline for all of them, don’t use the hump for some of them, underline for some of them, it looks very split.

Comment is clear

When looking at other people’s code, the greatest desire is to comment clearly, but when writing your own code, never write.

However, more comments are not always better, and I’ve summarized the following points:

  1. Notes are not limited to Chinese or English, but it is advisable not to use both
  2. Notes to be concise, one or two sentences to make the function clear
  3. Document comments should be written as much as possible
  4. Important sections of code can be separated by a double equal sign to highlight their importance

Here’s an example:

# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
# very important function, must be careful to use!!
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

def func(arg1, arg2) :
    Write a one-sentence summary of the function here (e.g., calculate the average value). Here is the description. ---------- arg1: int Description of arg1 arg2: int description of arg2 Returned value ------- Description of the returned value of int See -------- otherfunc: other associated functions... Sample -- -- -- -- -- -- -- -- example USES doctest format, in ` > > > ` code can be document testing tools as a test case automatically run > > > a = [1, 2, 3] > > > print [x + 3 for x in a], [4, 5, 6] "" "
Copy the code

Avoid duplicate code

As a project gets bigger and more developers work on it, the amount of code will inevitably increase, and there will inevitably be a lot of duplicate code that does the same thing.

Although it does not affect project performance, duplicate code can be very harmful. The most immediate effect is that when a problem occurs, you have to change a lot of code, and once you miss one, you cause a BUG.

Take this code for example:

import time


def funA() :
    start = time.time()
    for i in range(1000000) :pass
    end = time.time()

    print("funA cost time = %f s" % (end-start))


def funB() :
    start = time.time()
    for i in range(2000000) :pass
    end = time.time()

    print("funB cost time = %f s" % (end-start))


if __name__ == '__main__':
    funA()
    funB()
Copy the code

Both funA() and funB() have code that outputs the runtime of the function, so it’s a good idea to abstract out these duplicates.

For example write a decorator:

def warps() :
    def warp(func) :
        def _warp(*args, **kwargs) :
            start = time.time()
            func(*args, **kwargs)
            end = time.time()
            print("{} cost time = {}".format(getattr(func, '__name__'), (end-start)))
        return _warp
    return warp
Copy the code

In this way, the same function is achieved through the decorator method. If you need to change it later, just change the decorator, once and for all.

The advanced stage

As the code takes longer to write, it’s bound to be more demanding on itself than just formatting and commenting on basic specifications.

However, in this process, there are also some problems that need to be paid attention to. Here are the details.

Show off

The first thing to say is “show off.” As you get more familiar with the code, you always want to write advanced uses. But the result of reality is that code is often over-designed.

I have to say this from my own experience. There was a time when I was obsessed with advanced usage.

I once wrote a long and complicated SQL that even included a recursive call. There is even more “showy” Python code, often containing N magic methods in a single line.

Then after finishing writing, he gave a satisfied smile and sighed that his skills were really great.

The result is all kinds of scold, more importantly, a week later, I can’t understand.

In fact, the code is not the more advanced methods used the better, but to find the best fit.

The simpler the code, the clearer the logic, the less likely it is to make mistakes. And in a team where you’re not the only one maintaining your code, it’s important to keep the cost of reading and understanding the code down.

fragile

The second concern is the fragility of the code, whether small changes can cause major failures.

Is the code full of hard coding? If so, it is not an elegant implementation. It is likely that every performance optimization or configuration change will require modification of the source code. They even have to repackage and deploy online, which is very troublesome.

You take that hard code out and design it to be configurable, and when you need to change it, you just change the configuration.

Again, is there a check on the parameters? Or fault tolerant? If an API is called by a third party, will the program crash if the third party does not pass the required parameter?

Here’s an example:

page = data['page']
size = data['size']
Copy the code

This is not as good as the following:

page = data.get('page'.1)
size = data.get('size'.10)
Copy the code

Continue, are the libraries that depend on the project updated in a timely manner?

Aggressive, timely upgrades can avoid cross-release upgrades, which often cause problems.

In addition, upgrading is a good solution to some security vulnerabilities.

Finally, are unit tests good? Is there high coverage?

To be honest, programmers love writing code, but often don’t like writing unit tests, which is a bad habit.

With well-developed, well-covered unit tests, the overall robustness of the project can be improved and the possibility of bugs resulting from code changes can be minimized.

refactoring

As code gets bigger and bigger, refactoring is a lesson every developer has to learn. Martin Fowler defines refactoring as changing the internal structure of software to make it easier to understand and modify without changing its external behavior.

The benefits of refactoring are clear, resulting in improved code quality and performance, and improved development efficiency in the future.

But refactoring can be risky, and can cause problems if the logic of the code is not clear and regression testing is not done well.

This requires a special focus on code quality during development. In addition to some of the specifications mentioned above, there are a number of issues that need to be noted, such as whether object-oriented programming principles are being abused, whether interfaces are being over-coupled, etc.

So, is there a guiding principle that can be used to circumvent these problems during development?

Of course there is. Keep reading.

Advanced stage

I just finished reading a book recently, Uncle Bob’s ** The Way to Clean Architecture **, and I feel good about it.

The book is basically describing some theoretical knowledge of software design. It is generally divided into three parts: programming paradigms (structured programming, object-oriented programming, and functional programming), design principles (mainly SOLID), and software architecture (which talks a lot about high architecture).

In general, this book gives you a comprehensive understanding of software design at both the micro (code level) and the macro (architecture level) levels.

SOLID refers to the five basic principles of object-oriented programming and object-oriented design. Properly applying these five principles in the development process can make software maintenance and system expansion easier.

The five basic principles are:

  1. Single Responsibility Principle (SRP)
  2. Open and Closed Principle (OCP)
  3. Richter’s Substitution Principle (LSP)
  4. Interface Isolation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

Single Responsibility Principle (SRP)

A class should have one, and only one, reason to change. — Robert C Martin

The optimal structure of a software system is highly dependent on the internal structure of the organization of the system, so each software module has one and only one reason to be changed.

This principle is easy to misunderstand, and many programmers think that each module can only do one thing, which is not the case.

Here’s an example:

Suppose there is A class T, which contains two functions, A() and B(), when there is A need to modify A(), but it may affect the function of B().

This is not A good design, because A() and B() are coupled together.

Open and Closed Principle (OCP)

Software entities should be open for extension, but closed for modification. — Bertrand Meyer, Object-Oriented Software Construction

If a software system is to be more easily changed, it must be designed to allow new code to modify the behavior of the system, not just the original code.

Popular point of explanation is that the design of the class is open to extension, is closed to modification, can be extended, can not be modified.

The following code example illustrates this principle simply and clearly.

void DrawAllShape(ShapePointer list[].int n)
{
    int i;
    for (i = 0; i < n; i++)
    {
        struct Shape* s = list[i];
        switch (s->itsType)
        {
            case square:
                DrawSquare((struct Square*)s);
                break;
            case circle:
                DrawSquare((struct Circle*)s);
                break;
            default:
                break; }}}Copy the code

The above code does not follow the OCP principle.

If we want to add a triangle, we must add a case under the switch. This modifies the source code and violates OCP’s closed principle.

The disadvantages are also obvious. Every time a new shape is added, the source code needs to be modified, and the probability of problems is quite high if the code logic is complex.

class Shape
{
    public:
        virtual void Draw(a) const = 0;
}

class Square: public Shape
{
    public:
        virtual void Draw(a) const;
}

class Circle: public Shape
{
    public:
        virtual void Draw(a) const;
}

void DrawAllShapes(vector<Shape*>& list)
{
    vector<Shape*>::iterator I;
    for (i = list.begin(): i ! = list.end(a); i++) { (*i)->Draw();
    }
}
Copy the code

With this modification, the code is much more elegant. If you need to add a new type, you just need to add a new class that inherits Shape. There is no need to modify the source code, can be extended.

Richter’s Substitution Principle (LSP)

Require no more, promise no less. — Jim Weirich

This principle means that if you want to build a software system with replaceable components, those components must follow the same convention so that they can be replaced with each other.

Richter’s substitution principle can be understood from two aspects:

The first is inheritance. If inheritance is for code reuse, that is, for shared methods, then shared parent methods should remain unchanged and not be redefined by subclasses.

Subclass only through new add methods to extend the function of parent and child classes can be instantiated, and a subclass inherits methods and the parent class is the same, the parent class method is called, the subclass can also call the same inherited, logic, and the parent class consistent method, then use a subclass object replace superclass object, of course, logically consistent, smoothly.

The second is polymorphism, which assumes that a subclass overrides and redefines the methods of its parent class.

To comply with LSPS, you define the parent class as an abstract class and define abstract methods that subclasses can redefine. When the superclass is abstract, the superclass simply cannot be instantiated, so there is no instantiable superclass object in the program, and there is no logical inconsistency when subclasses replace instances of the superclass (there are no instances of the superclass).

Here’s an example:

Take a look at this code:

class A{
	public int func1(int a, int b){
		returna - b; }}public class Client{
	public static void main(String[] args){
		A a = new A();
		System.out.println("100-50 =" + a.func1(100.50));
		System.out.println("100-80 =" + a.func1(100.80)); }}Copy the code

Output;

100-50 = 50 = 20, 100-80Copy the code

Now, let’s add a new function: add the two numbers and then add them to 100, handled by class B. Class B needs to do two things:

  1. Two Numbers subtraction
  2. Add the two numbers and add 100

Now the code looks like this:

class B extends A{
	public int func1(int a, int b){
		return a + b;
	}
	
	public int func2(int a, int b){
		return func1(a,b) + 100; }}public class Client{
	public static void main(String[] args){
		B b = new B();
		System.out.println("100-50 =" + b.func1(100.50));
		System.out.println("100-80 =" + b.func1(100.80));
		System.out.println("100 + 20 + 100 =" + b.func2(100.20)); }}Copy the code

Output;

100-50 = 150, 100-80 = 180, 100 + 20 + 100 = 220Copy the code

As you can see, the normal subtraction has gone wrong. The reason is that class B overwrites the method of the parent class when naming the method, causing all the codes running the subtraction function to call the method overwritten by class B, resulting in the error of the function that used to run normally.

Doing so violates the LSP and makes the program less robust. A more general approach is: the original parent class and child class inherit a more popular base class, the original inheritance relationship removed, replaced by dependency, aggregation, composition, etc.

Interface Isolation Principle (ISP)

Clients should not be forced to depend on methods they do not use. — Robert C. Martin

Software designers should avoid unnecessary dependencies in their designs.

The principle of AN ISP is to create a single interface rather than a large and bloated interface. Instead, try to elaborate the interface and use as few methods as possible on the interface.

That is, instead of trying to create a huge interface for all the classes that depend on it, we need to create interfaces that are specific to each class.

In programming, relying on several specialized interfaces is more flexible than relying on one comprehensive interface.

The difference between single responsibility and interface isolation:

  1. The principle of single responsibility focuses on responsibility; The interface isolation principle focuses on the isolation of interface dependencies.
  2. The single responsibility principle is mainly to constrain the class, followed by the interface and method, it is aimed at the implementation and details in the program; The interface isolation principle mainly restricts interfaces.

Here’s an example:

Let’s first explain what this graph means:

The “canine” class relies on “interface I” methods: “feed”, “walk”, “run”; The “bird” class relies on the “feed”, “glide”, and “fly” methods in “interface I”.

“Pet dog” category and “pigeon” category are respectively the realization of “dog” category and “bird” category.

For specific classes: “Dog” and “pigeon”, although they both have methods that are not needed, because interface I is implemented, they must also implement these methods, which is obviously bad design.

If this design is modified to comply with the interface isolation principle, “interface I” must be split.

In this case, we split the original “interface I” into three interfaces, and each class only needs to implement its own interface.

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not Depend on details. Details should depend on Abstractions. — Robert C. Martin

High-level strategic code should not rely on code that implements low-level details.

That doesn’t sound very clear. Let me translate. This basically means that when writing code, you should use more stable abstract interfaces and less variable concrete implementations.

Here’s an example:

Take a look at this code:

public class Test {

    public void studyJavaCourse(a) {
        System.out.println("Joe is taking a Java course.");
    }

    public void studyDesignPatternCourse(a) {
        System.out.println("Joe is taking a design patterns course."); }}Copy the code

Upper-level direct call:

public static void main(String[] args) {
    Test test = new Test();
    test.studyJavaCourse();
    test.studyDesignPatternCourse();
}
Copy the code

This writing at first glance does not have any problems, the function is well implemented, but careful analysis, but not simple.

First question:

If John is learning a new class, he needs to add new methods to the Test() class. As the requirements increase, the Test() class becomes very large and difficult to maintain.

And, ideally, the new code does not affect the original code, which ensures system stability and reduces risk.

Second question:

The methods in the Test() class implement essentially the same functionality, but define three methods with different names. Is it possible to abstract these three methods, and if so, increase the readability and maintainability of the code?

Third question:

The business layer code directly calls the implementation details of the underlying class, resulting in serious coupling.

To solve this problem based on DIP, it is necessary to abstract the bottom layer and avoid the upper layer directly calling the bottom layer.

Abstract interface:

public interface ICourse {
    void study(a);
}
Copy the code

Then write a class for JavaCourse and DesignPatternCourse, respectively:

public class JavaCourse implements ICourse {

    @Override
    public void study(a) {
        System.out.println("Joe is taking a Java course."); }}public class DesignPatternCourse implements ICourse {

    @Override
    public void study(a) {
        System.out.println("Joe is taking a design patterns course."); }}Copy the code

Finally modify the Test() class:

public class Test {

    public void study(ICourse course) { course.study(); }}Copy the code

Now, the call looks like this:

public static void main(String[] args) {
    Test test = new Test();
    test.study(new JavaCourse());
    test.study(new DesignPatternCourse());
}
Copy the code

Through this development, the three problems mentioned above are solved perfectly.

Actually, writing code is not difficult, but designing architecture through design patterns is the most difficult and important.

So, the next time you need something, don’t write code. Think it through first.

This article is especially hard to write, mainly because the latter part is difficult to understand. And there are some principles really do not use experience, only rely on text understanding or almost meaning, experience the essence.

In fact, many requirements in the article I can not do, summed up quite an incentive to their own. Be more respectful of your code in the future, rather than trying to implement features. Writing robust, elegant code should be the goal of every programmer.

If you think this article is good, please like and forward, thanks ~


Recommended reading:

  • Go Learning Route (2022)

References:

  • The Way to Clean Architecture
  • www.cyningsun.com/08-03-2019/…
  • Blog.csdn.net/yabay2208/a…
  • zhuanlan.zhihu.com/p/92488185