关于编程:本着什么原则才能写出优秀的代码

58次阅读

共计 8370 个字符,预计需要花费 21 分钟才能阅读完成。

原文链接:本着什么准则,能力写出优良的代码?

作为一名程序员,最不爱干的事件,除了散会之外,可能就是看他人的代码。

有的时候,新接手一个我的项目,关上代码一看,要不是身材好的话,可能间接气到晕厥。

风格各异,没有正文,甚至连最根本的格局缩进都做不到。这些代码存在的意义,可能就是为了证实一句话:又不是不能跑。

在这个时候,大部分程序员的想法是:这烂代码真是不想改,还不如间接重写。

但有的时候,咱们看一些驰名的开源我的项目时,又会感叹,代码写的真好,优雅。为什么好呢?又有点说不出来,总之就是好。

那么,这篇文章就试图剖析一下好代码都有哪些特点,以及本着什么准则,能力写出优良的代码。

初级阶段

先说说比拟根本的准则,只有是程序员,不论是高级还是高级,都会思考到的。

这只是列举了一部分,还有很多,我筛选四项简略举例说明一下。

  1. 格局对立
  2. 命名标准
  3. 正文清晰
  4. 防止反复代码

以下用 Python 代码别离举例说明:

格局对立

格局对立包含很多方面,比方 import 语句,须要依照如下程序编写:

  1. Python 规范库模块
  2. Python 第三方模块
  3. 应用程序自定义模块

而后每局部间用空行分隔。

import os
import sys

import msgpack
import zmq

import foo

再比方,要增加适当的 空格,像上面这段代码;

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

代码都紧凑在一起了,很影响浏览。

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

增加空格之后,立即感觉清晰了很多。

还有就是像 Python 的 缩进,其余语言的大括号地位,是放在行尾,还是另起新行,都须要保障对立的格调。

有了对立的格调,会让代码看起来更加整洁。

命名标准

好的命名是不须要正文的,只有看一眼命名,就能晓得变量或者函数的作用。

比方上面这段代码:

a = 'zhangsan'
b = 0

a 可能还能猜到,但当代码量大的时候,如果满屏都是 abcd,那还不得原地爆炸。

把变量名略微改一下,就会使语义更加清晰:

username = 'zhangsan'
count = 0

还有就是命名要格调对立。如果用驼峰就都用驼峰,用下划线就都用下划线,不要有的用驼峰,有点用下划线,看起来十分决裂。

正文清晰

看他人代码的时候,最大的欲望就是正文清晰,但在本人写代码时,却从来不写。

但正文也不是越多越好,我总结了以下几点:

  1. 正文不限于中文或英文,但最好不要中英文混用
  2. 正文要长篇累牍,一两句话把性能说分明
  3. 能写文档正文应该尽量写文档正文
  4. 比拟重要的代码段,能够用双等号分隔开,突出其重要性

举个例子:

# =====================================
# 十分重要的函数,肯定审慎应用 !!!
# =====================================

def func(arg1, arg2):
    """ 在这里写函数的一句话总结(如: 计算平均值).

    这里是具体形容.

    参数
    ----------
    arg1 : int
        arg1 的具体形容
    arg2 : int
        arg2 的具体形容

    返回值
    -------
    int
        返回值的具体形容

    参看
    --------
    otherfunc : 其它关联函数等...

    示例
    --------
    示例应用 doctest 格局, 在 `>>>` 后的代码能够被文档测试工具作为测试用例主动运行

    >>> a=[1,2,3]
    >>> print [x + 3 for x in a]
    [4, 5, 6]
    """

防止反复代码

随着我的项目规模变大,开发人员增多,代码量必定也会减少,防止不了的会呈现很多反复代码,这些代码实现的性能是雷同的。

尽管不影响我的项目运行,但反复代码的危害是很大的。最间接的影响就是,呈现一个问题,要改很多处代码,一旦漏掉一处,就会引发 BUG。

比方上面这段代码:

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()

funA()funB() 中都有输入函数运行工夫的代码,那么就适宜将这些反复代码形象进去。

比方写一个装璜器:

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

这样,通过装璜器办法,实现了同样的性能。当前如果须要批改的话,间接改装璜器就好了,一劳永逸。

进阶阶段

当代码写工夫长了之后,必定会对本人有更高的要求,而不只是 格局 正文 这些根本标准。

但在这个过程中,也是有一些问题须要留神的,上面就来具体说说。

炫技

第一个要说的就是「炫技」,当对代码越来越相熟之后,总想写一些高级用法。但事实造成的后果就是,往往会使代码适度设计。

这不得不说说我的亲身经历了,已经有一段时间,我特地迷恋各种高级用法。

有一次写过一段很长的 SQL,而且很简单,外面甚至还蕴含了一个递归调用。有「炫技」嫌疑的 Python 代码就更多了,往往就是一行代码蕴含了 N 多魔术办法。

而后在写完之后漏出称心的笑容,感叹本人技术真牛。

后果就是各种被骂,更重要的是,一个星期之后,本人都看不懂了。

其实,代码并不是高级办法用的越多就越牛,而是要找到最适宜的。

越简略的代码,越清晰的逻辑,就越不容易出错。而且在一个团队中,你的代码并不是你一个人保护,升高他人浏览,了解代码的老本也是很重要的。

软弱

第二点须要关注的是代码的脆弱性,是否轻微的扭转就可能引起重大的故障。

代码里是不是充斥了硬编码?如果是的话,则不是优雅的实现。很可能导致每次性能优化,或者配置变更就须要批改源代码。甚至还要从新打包,部署上线,十分麻烦。

而把这些硬编码提取进去,设计成可配置的,当须要变更时,间接改一下配置就能够了。

再来,对参数是不是有校验?或者容错解决?如果有一个 API 被第三方调用,如果第三方没按要求传参,会不会导致程序解体?

举个例子:

page = data['page']
size = data['size']

这样的写法就没有上面的写法好:

page = data.get('page', 1)
size = data.get('size', 10)

持续,我的项目中依赖的库是不是及时降级更新了?

踊跃,及时的降级能够防止跨大版本升级,因为跨大版本升级往往会带来很多问题。

还有就是在遇到一些安全漏洞时,降级是一个很好的解决办法。

最初一点,单元测试欠缺吗?覆盖率高吗?

说实话,程序员喜爱写代码,但往往不喜爱写单元测试,这是很不好的习惯。

有了欠缺,覆盖率高的单元测试,能力进步我的项目整体的健壮性,能力把因为批改代码带来的 BUG 的可能性降到最低。

重构

随着代码规模越来越大,重构是每一个开发人员都要面对的功课,Martin Fowler 将其定义为:在不扭转软件内部行为的前提下,对其内部结构进行扭转,使之更容易了解并便于批改。

重构的收益是显著的,能够进步代码品质和性能,并进步将来的开发效率。

但重构的危险也很大,如果没有理清代码逻辑,不能做好回归测试,那么重构势必会引发很多问题。

这就要求在开发过程中要特地重视代码品质。除了上文提到的一些标准之外,还要留神是不是滥用了面向对象编程准则,接口之间设计是不是适度耦合等一系列问题。

那么,在开发过程中,有没有一个指导性准则,能够用来躲避这些问题呢?

当然是有的,接着往下看。

高级阶段

最近刚读完一本书,Bob 大叔的《架构整洁之道》,感觉还是不错的,播种很多。

全书基本上是在形容软件设计的一些理论知识。大体分成三个局部:编程范式(结构化编程、面向对象编程和函数式编程),设计准则(次要是 SOLID),以及软件架构(其中讲了很多高屋建翎的内容)。

总体来说,这本书中的内容能够让你从宏观(代码层面)和宏观(架构层面)两个层面对整个软件设计有一个全面的理解。

其中 SOLID 就是指面向对象编程和面向对象设计的五个根本准则,在开发过程中适当利用这五个准则,能够使软件维护和零碎扩大都变得更容易。

五个根本准则别离是:

  1. 繁多职责准则(SRP)
  2. 凋谢关闭准则(OCP)
  3. 里氏替换准则(LSP)
  4. 接口隔离准则(ISP)
  5. 依赖倒置准则(DIP)

繁多职责准则(SRP)

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

一个软件系统的最佳构造高度依赖于这个零碎的组织的内部结构,因而每个软件模块都有且只有一个须要被扭转的理由。

这个准则非常容易被误会,很多程序员会认为是每个模块只能做一件事,其实不是这样。

举个例子:

如果有一个类 T,蕴含两个函数,别离是 A()B(),当有需要须要批改 A() 的时候,但却可能会影响 B() 的性能。

这就不是一个好的设计,阐明 A()B() 耦合在一起了。

凋谢关闭准则(OCP)

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

如果软件系统想要更容易被扭转,那么其设计就必须容许新增代码来批改零碎行为,而非只能靠批改原来的代码。

艰深点解释就是设计的类对扩大是凋谢的,对批改是关闭的,即可扩大,不可批改。

看上面的代码示例,能够简略清晰地解释这个准则。

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;
        }
    }
}

下面这段代码就没有恪守 OCP 准则。

如果咱们想要减少一个三角形,那么就必须在 switch 上面新增一个 case。这样就批改了源代码,违反了 OCP 的关闭准则。

毛病也很显著,每次新增一种形态都须要批改源代码,如果代码逻辑简单的话,产生问题的概率是相当高的。

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

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

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

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

通过这样批改,代码就优雅了很多。这个时候如果须要新增一种类型,只须要减少一个继承 Shape 的新类就能够了。齐全不须要批改源代码,能够释怀扩大。

里氏替换准则(LSP)

Require no more, promise no less.– Jim Weirich

这项准则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须恪守同一个约定,以便让这些组件能够互相替换。

里氏替换准则能够从两方面来了解:

第一个是 继承。如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类办法就应该放弃不变,不能被子类从新定义。

子类只能通过新增加办法来扩大性能,父类和子类都能够实例化,而子类继承的办法和父类是一样的,父类调用办法的中央,子类也能够调用同一个继承得来的,逻辑和父类统一的办法,这时用子类对象将父类对象替换掉时,当然逻辑统一,相安无事。

第二个是 多态,而多态的前提就是子类笼罩并从新定义父类的办法。

为了合乎 LSP,应该将父类定义为抽象类,并定义形象办法,让子类从新定义这些办法。当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里,也就不存在子类替换父类实例(基本不存在父类实例了)时逻辑不统一的可能。

举个例子:

看上面这段代码:

class A{public int func1(int a, int b){return a - 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));
    }
}

输入;

100-50=50
100-80=20

当初,咱们新增一个性能:实现两数相加,而后再与 100 求和,由类 B 来负责。即类 B 须要实现两个性能:

  1. 两数相减
  2. 两数相加,而后再加 100

当初代码变成了这样:

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));
    }
}

输入;

100-50=150
100-80=180
100+20+100=220

能够看到,本来失常的减法运算产生了谬误。起因就是类 B 在给办法起名时重写了父类的办法,造成所有运行相减性能的代码全副调用了类 B 重写后的办法,造成本来运行失常的性能呈现了谬误。

这样做就违反了 LSP,使程序不够强壮。更通用的做法是:原来的父类和子类都继承一个更艰深的基类,原有的继承关系去掉,采纳依赖、聚合,组合等关系代替。

接口隔离准则(ISP)

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

软件设计师应该在设计中防止不必要的依赖。

ISP 的准则是建设繁多接口,不要建设宏大臃肿的接口,尽量细化接口,接口中的办法要尽量少。

也就是说,咱们要为各个类建设专用的接口,而不要试图去建设一个很宏大的接口供所有依赖它的类去调用。

在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵便。

繁多职责与接口隔离的区别:

  1. 繁多职责准则重视的是职责;而接口隔离准则重视对接口依赖的隔离。
  2. 繁多职责准则次要是束缚类,其次才是接口和办法,它针对的是程序中的实现和细节;而接口隔离准则次要束缚接口。

举个例子:

首先解释一下这个图的意思:

「犬科」类依赖「接口 I」中的办法:「捕食」,「行走」,「奔跑」;「鸟类」类依赖「接口 I」中的办法「捕食」,「滑翔」,「翱翔」。

「宠物狗」类与「鸽子」类别离是对「犬科」类与「鸟类」类依赖的实现。

对于具体的类:「宠物狗」与「鸽子」来说,尽管他们都存在用不到的办法,但因为实现了「接口 I」,所以也 必须要实现这些用不到的办法,这显然是不好的设计。

如果将这个设计批改为合乎接口隔离准则的话,就必须对「接口 I」进拆分。

在这里,咱们将原有的「接口 I」拆分为三个接口,拆分之后,每个类只需实现本人须要的接口即可。

依赖倒置准则(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

高层策略性的代码不应该依赖实现底层细节的代码。

这话听起来就让人听不明确,我来翻译一下。大略就是说在写代码的时候,应该多应用稳固的形象接口,少依赖多变的具体实现。

举个例子:

看上面这段代码:

public class Test {public void studyJavaCourse() {System.out.println("张三正在学习 Java 课程");
    }

    public void studyDesignPatternCourse() {System.out.println("张三正在学习设计模式课程");
    }
}

下层间接调用:

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

这样写乍一看并没有什么问题,性能也实现的好好的,但仔细分析,却并不简略。

第一个问题:

如果张三又新学习了一门课程,那么就须要在 Test() 类中减少新的办法。随着需要增多,Test() 类会变得十分宏大,不好保护。

而且,最现实的状况是,新增代码并不会影响原有的代码,这样能力保证系统的稳定性,升高危险。

第二个问题:

Test() 类中办法实现的性能实质上都是一样的,然而却定义了三个不同名字的办法。那么有没有可能把这三个办法形象进去,如果能够的话,代码的可读性和可维护性都会减少。

第三个问题:

业务层代码间接调用了底层类的实现细节,造成了重大的耦合,要改全改,牵一发而动全身。

基于 DIP 来解决这个问题,势必就要把底层形象进去,防止下层间接调用底层。

形象接口:

public interface ICourse {void study();
}

而后别离为 JavaCourseDesignPatternCourse 编写一个类:

public class JavaCourse implements ICourse {

    @Override
    public void study() {System.out.println("张三正在学习 Java 课程");
    }
}

public class DesignPatternCourse implements ICourse {

    @Override
    public void study() {System.out.println("张三正在学习设计模式课程");
    }
}

最初批改 Test() 类:

public class Test {public void study(ICourse course) {course.study();
    }
}

当初,调用形式就变成了这样:

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

通过这样开发,下面提到的三个问题失去了完满解决。

其实,写代码并不难,通过什么设计模式来设计架构才是最难的,也是最重要的。

所以,下次有需要的时候,不要焦急写代码,先想分明了再入手也不迟。

这篇文章写的特地辛苦,次要是后半局部了解起来有些艰难。而且有一些准则也的确没有应用教训,单靠文字了解还是差点意思,领会不到精华。

其实,文章中的很多要求我都做不到,总结进去也相当于是对本人的一个激励。当前对代码要更加敬畏,而不是为了实现性能草草了事。写出强壮,优雅的代码应该是每个程序员的指标,与大家共勉。

如果感觉这篇文章还不错的,欢送 点赞 转发,感激~


举荐浏览:

  • Go 学习路线(2022)

参考资料:

  • 《架构整洁之道》
  • https://www.cyningsun.com/08-…
  • https://blog.csdn.net/yabay22…
  • https://zhuanlan.zhihu.com/p/…

正文完
 0