原文链接:本着什么准则,能力写出优良的代码?
作为一名程序员,最不爱干的事件,除了散会之外,可能就是看他人的代码。
有的时候,新接手一个我的项目,关上代码一看,要不是身材好的话,可能间接气到晕厥。
风格各异,没有正文,甚至连最根本的格局缩进都做不到。这些代码存在的意义,可能就是为了证实一句话:又不是不能跑。
在这个时候,大部分程序员的想法是:这烂代码真是不想改,还不如间接重写。
但有的时候,咱们看一些驰名的开源我的项目时,又会感叹,代码写的真好,优雅。为什么好呢?又有点说不出来,总之就是好。
那么,这篇文章就试图剖析一下好代码都有哪些特点,以及本着什么准则,能力写出优良的代码。
初级阶段
先说说比拟根本的准则,只有是程序员,不论是高级还是高级,都会思考到的。
这只是列举了一部分,还有很多,我筛选四项简略举例说明一下。
- 格局对立
- 命名标准
- 正文清晰
- 防止反复代码
以下用 Python 代码别离举例说明:
格局对立
格局对立包含很多方面,比方 import
语句,须要依照如下程序编写:
- Python 规范库模块
- Python 第三方模块
- 应用程序自定义模块
而后每局部间用空行分隔。
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
可能还能猜到,但当代码量大的时候,如果满屏都是 a
,b
,c
,d
,那还不得原地爆炸。
把变量名略微改一下,就会使语义更加清晰:
username = 'zhangsan'
count = 0
还有就是命名要格调对立。如果用驼峰就都用驼峰,用下划线就都用下划线,不要有的用驼峰,有点用下划线,看起来十分决裂。
正文清晰
看他人代码的时候,最大的欲望就是正文清晰,但在本人写代码时,却从来不写。
但正文也不是越多越好,我总结了以下几点:
- 正文不限于中文或英文,但最好不要中英文混用
- 正文要长篇累牍,一两句话把性能说分明
- 能写文档正文应该尽量写文档正文
- 比拟重要的代码段,能够用双等号分隔开,突出其重要性
举个例子:
# =====================================
# 十分重要的函数,肯定审慎应用 !!!
# =====================================
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 就是指面向对象编程和面向对象设计的五个根本准则,在开发过程中适当利用这五个准则,能够使软件维护和零碎扩大都变得更容易。
五个根本准则别离是:
- 繁多职责准则(SRP)
- 凋谢关闭准则(OCP)
- 里氏替换准则(LSP)
- 接口隔离准则(ISP)
- 依赖倒置准则(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
须要实现两个性能:
- 两数相减
- 两数相加,而后再加 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 的准则是建设繁多接口,不要建设宏大臃肿的接口,尽量细化接口,接口中的办法要尽量少。
也就是说,咱们要为各个类建设专用的接口,而不要试图去建设一个很宏大的接口供所有依赖它的类去调用。
在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵便。
繁多职责与接口隔离的区别:
- 繁多职责准则重视的是职责;而接口隔离准则重视对接口依赖的隔离。
- 繁多职责准则次要是束缚类,其次才是接口和办法,它针对的是程序中的实现和细节;而接口隔离准则次要束缚接口。
举个例子:
首先解释一下这个图的意思:
「犬科」类依赖「接口 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();
}
而后别离为 JavaCourse
和 DesignPatternCourse
编写一个类:
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/…