编程原则

[toc]

有目的的努力方向,才是正确的方向。而编程原则就是抽象于各种编程语言而来的思想,为的就是写下那隽永留千年的代码,因为最经典、最基础的一些思想总是源远流长的!

代码发布后故障不断。

多时之后再看自己的代码,已经不能再忍受,重构。

修改一行代码,整个程序都崩溃了。

我的新功能应该在什么地方加入呢?

系统最本质的内容,我们往往理解的不够,所以出现了很多矛盾和纠结。为啥不深入研究下软件中最基本的内容并且沉淀呢,面对这个最复杂的东西探析清楚,知其所以然。这就是本篇的内容。

0 准确定义

0.1 软件

编程的产物也就是“软件”。我们常说的系统、工具、应用、平台等均是编程的产物,均归类为软件。

0.2 代码

编程中编写的东西统称为“代码”,包括源代码、程序、源程序等均为代码。

0.3 编程

编写代码这件事或行为,我们就叫“编程”;也有叫编码、Coding、开发、实现等,均用编程指代。

0.4 程序员

负责编程的人统一称作“程序员”,根据不同的场景也区分为开发者、开发人员、软件工程师、程序猿。

0.5 函数

可供调用的代码块也就是“函数”,还能细化为功能、过程、方法、成员函数等;往往词语指代东西并不一致,功能是有返回值,而过程是无返回值。

0.6 模块

函数和变量的组合单元可以称为“模块”。包括更多的概念都可以提供如此的组合概念,如类、文件、库、组件、部件等,代表的是关联性较强的代码集合。

0.7 软件架构

软件的整体结构就是“软件架构”,高于模块一层来看整个软件的整体内容,聚焦模块间的交互。

1 永恒的真理

1.1 No Silver Bullet

银弹是用来镇住狼人的唯一方法,那就是将银弹射入狼人的体内。软件编程中没有万能解决方案,没有特效药。如果有,软件就是一件简单的事情。

软件是复杂的,千万行代码的软件比比皆是,而软件要素之间的依赖关系是动态的剪不断、理还乱。软件比其他任何构造物都复杂。

软件是实时的,需要响应当下的状态,与硬件、网络、人的行为、其它软件等均相关联,这种实时是一种生命的不断向前,如果没有,那就没有软件的价值。

软件是可变的,软件的生命在于它与现实世界的交互,双方处于螺旋而上升的变化状态中;现实世界因为软件的进步而更美好,更美好的现实世界驱动软件的进步;

软件是不可见的,是现实生活中概念的虚实映射,进程是没法看见的,对概念的表达是肉眼看不见的东西,对世界是抽象的表达,舍象的呈现,无法全部表现。

本质,是定义上无法舍去的东西,不可欠缺的性质。非本质是指次要的、附属的性质,不能最终影响事物本质的。

而软件在本质上具有难度,归因就是软件具有复杂性。通过各种方法和思路,来降低复杂性给软件带来的影响。围绕软件的构建、环境、编程语言、库、框架等都是软件的非本质部分,而非本质的部分对于推进软件本质部分内容具有巨大的贡献,而往往非本质部分能够得到更简单的解决,间接降低软件整体的复杂度 。

非本质部分的自动化可以大幅改善工作效率和工作质量,而目前对于软件本质来说,需要通过自动化非本质部分而腾出更多的时间给到软件本质,软件真正的价值上。这个和当前的云原生的理念是不谋而合!

1.2 代码即设计文档

软件最终交付的有价值的内容除了代码,别无其它。而编程就是一个设计过程,只不过设计的是代码,而不是文档。产品的性能无法在制造和生产环境得到提升,能够得到提升的过程就是设计过程。不管是哪个环节,最终设计必须使用编程语言来表现,这个过程中编程语言才是最核心的设计要素。

基本设计、详细设计等上游不应该偏离编程语言,因为编程、调测、测试等下游均是基于编程语言。所以如何端到端的打通整个过程,才是设计的初衷。设计需要创造力,编程就是一种创造行为,与其把不依赖编程语言的设计图翻译为代码,倒不如一开始让设计者编写代码来的省心。代码就是属于设计范畴,应该尽早的开始编写代码。

代码是设计文档,并不代表代码是唯一文档。代码表达了“怎么做”和“是什么”,代码不能表达为什么这么做。而设计理由需要有地方承载。对于代码中的注释可以承担部分,注释需要弥补代码表达不出来的东西。另一方面,对代码进行说明的注释就是重复,应该避免这点。

罗塞塔石碑1的文档,这是一份写个未来维护负责人的简单手册,描述了理解软件开发环境和软件架构的信息,描述构建和测试过程的执行方法,而其中的测试可以避免维护负责人理解的陷阱,在架构方面描述纵观全部代码的图表。

1.3 一定会被修改的代码

代码如果不被修改,那软件的生命周期已经趋于死亡。“代码会被修改”是既定事实标准,我们做选择和判断时候优先考虑的事情。软件产生故障,需要修复;软件的新需求,需要新增;不管是修复还是新增,都是对软件的修改,软件必须迎合变化,没有一开始就能满足所有诉求的软件。

需要编写经得起修改的代码,这个过程中代码的“可读”尤为重要。不管写代码需要耗费多少时间,只要读代码的时间能够缩短,那么我们就能够把消耗在写代码上的时间赚回来。要相信这点!

2 正确的指导方针

2.1 KISS (Keep it short & simple)

保持简洁,容易理解,易于修改和使用。必不可少的最小集是什么,还能否做减法?功能简洁,接口简洁。奥卡姆剃刀,当一个事物存在多种解释时,最简单的哪个解释时正确的,减轻思考的负担、增强可读可理解,最后可敏捷修改。

2.2 DRY (Don’t repeat yourself)

禁止重复的拷贝。发现重复应该立即予以消除,避免重复这点没有商量余地。重复的代码可读性下降、难以修改且分散多处测试困难。一定程度上的代码抽象化操作可以消除重复。逻辑执行抽象,就是给整个处理过程命名,将其函数化、模块化。对于数据的重复,需要定义常量来替换;对应的重复内容有了名称,提升了可读性。

对于自动化作业对软件进行测试、构建和发布的持续集成也是消除重复,消除重复的手动作业,提升自动化水平。对于设计模式是一种防止重复思考的手法。

WET, Write every time, Write everything twice重复同样的事情,正好也是DRY的反义词。

对于数据库而言,不应该存储重复的数据,one fact only in one place;

2.3 YAGNI (You aren’t going to need it)

不用想太多,坚持只写当前需要的代码。比追求通用性,而复杂设计更应该重视单纯性,能用是第一位的,聚焦具体需求为基础的简单方案。

想的太远只会增加代码的复杂度,提高修改的成本。所以编码的时候,要思考回到原点,保持纯粹的单纯简单,以代码阅读者的角度做些思考。

2.4 PIE (Program intently & expressively)

编码更多要在表达上花心思,将软件运行方式直观地传达给阅读代码的人。代码是我们正确、完整地理解软件运行方式的唯一线索。代码是动态变更的,任何的文档都是跟不上的,因而编写可读性高的代码,用代码表达意图是唯一可取的方法。

读代码的次数远多于写代码的次数,所以追求的是代码可读性而不是代码的易写性。代码只写一次,但是多次阅读,读代码的效率应优先于写代码的效率和执行代码的效率。

表达意图的终极形态是文学编程,将代码本身当作文档的编程方法,用于描述文档的语言和编程语言结合在一起。代码即文档,文档即代码。代码要表达作品的意图,并且要有自我说明的能力。

2.5 SLAP (Single level of abstraction principle)

抽象是分等级的,根据功能的复杂程度对象化概念进行分离,然后统一各层的抽象级别。统一各抽象级别之后,代码就可以像图书一样供人阅读。

将代码分成级别统一的函数,能使代码具有概括性和可读性。函数一览起到目录作用,使代码拥有概括性。分割后的函数时小块代码,提升了代码的可读性。抽象度相同的处理在同一个地方,代码更加顺畅更加可读。函数结构化之后,各函数处理以调用比自己低一个级别的函数为中心。这种由其他函数调用组成的函数称为复合函数,符合函数要尽量小,如果想通过名称来表达意图,即使处理只有一行,也可以写成函数。

面向对象中,容器时抽象类及其继承类。抽象类存放抽象级别较高的概念,继承类存放级别较低的概念。设计结构和编写内容时两件事情,需要独立的模式去实现。

2.6 OCP (Open & closed principle)

代码需要满足对扩展开发,对修改关闭的属性。扩展开放表示代码的行为可以扩展,修改关闭表示对代码进行行为扩展时,其他代码完成不会受到影响。

面向对象的多态是实现OCP的代表技术,领域模型是稳定的,识别系统中的稳定成分和不稳定部分,在这两部分之间通过稳定的接口将交界点包裹起来。受保护变化则使用这层接口防护墙将变更带来的影响抵御在外。

2.7 Naming (Naming is important)

命名,一个合适的名字意味着元素被正确理解并正确地设计了出来。一个合适的名字表示设计已经完成了一大半。意图传达地核心概念就是命名,让人充分理解某个东西是怎么样做出来的,所以需要最大限度地在名字上下功夫。

编码的时候并不是向读代码才去读的,在充分理解代码之后的修改或添加功能才是我们真正的目的。在读代码时,混乱的名字会占用所有的脑部资源,让编码困难。

名字要能够搜索出来,不能是单个字母或数字,要避免常见的搜索在代码中产生无数个结果。名字要能够念出来,需要在团队中统一概念,传播给各个角色这些概念,大家在理解上是一致的。名字应该是基于标准术语来命名,避免大家心里映射才能理解元素表达的意思。

3 UNIX思想

UNIX是被认为是大量经验的结晶,是编写优秀代码的实用性技术集合。所以在这会讲明白其蕴涵的思想是什么,以及怎么做,而不去分析为什么,因为这就是历史的沉淀,一些约定俗成的规定,被证明是正确的,得到全世界开发者的赞同,并不断的完善。

对我有些触发内容的点是,3.9 表达性原则和3.17 可扩展性原则。

3.1 模块化原则

代码中关联性较强的元素聚集在一起就是模块,模块接口要简单明了,而不要冗余繁杂,最少和最小的接口提供最简练的功能。

多个简单模块组装成软件,而每次修改都把影响控制在模块内。

3.2 清晰原则

代码不应该复杂,应该清晰,便于理解。在读代码时,不要对难以读懂的部分再三解读。第一次需要解读可能那个时因为碰巧没看懂,如果第二次还需要解读,那就需要小重构了。此时可以添加注释解释为什么,或者把代码修改的更加容易理解。

3.3 组合原则

软件应该尽量做成一个简单的过滤器,接受某种数据流后,加工输出另一种数据流的软件。这种输入输出应该是简单明了的。通过不同的软件组合方式,就能够完成多种多样的任务。

3.4 分离原则

是常说的领域模型,因为每个领域内沉淀的内容是稳定的。同时为机制编写有效的测试代码,机制的寿命比较长,同时独立的机制是可以被其他软件重复使用的。

3.5 简单原则

代码设计的足够简单,只能规定只在必要的情况下添加复杂代码。团队间应该营造简单即美丽的文化氛围。

3.6 简约原则

简约和简单不同在于不写大代码,一方面指代码量大,也指代码复杂指数大。要吝啬,尽量少写,让代码维持小的状态。随着代码的添加而变大,就要尽快对代码进行分割。

3.7 透明性原则

设计软件时,要从外部清楚看到软件的运行状态,不仅让软件正确运行,还要让人能够看到它是在正确地运行。软件需要一眼就能理解软件正在做什么和软件是怎么做地,同时要呈现并监控其内部状态。正式地代码中加入能够简化调试地机制是一件好事,需要认真对待。这是对于系统可观测能力地提升。

3.8 健壮性原则

软件不仅能在一般条件下正常运行,还能再预想之外地条件下提供适当地服务。要想代码具有健壮性,需要保证人们能轻松识别其内部结构。

3.9 表达性原则

代码中地信息要尽量用数据而非逻辑来表达。将信息固定在数据一侧,可以提高逻辑地可读性和健壮性。数据不论多么复杂都能轻松地模型化,用数据来表达信息也更加简单。使用换算表地数组初始化语句与switch语句相比,数据结构一方面更容易理解。

我们应当把代码中无法避免的复杂部分交给数据表示。当遇到数据和逻辑必须要有一个复杂化的情况时,毫不犹豫选择的是让数据复杂化。这也就是我们的领域模型,通过模型上的数据来承担过程逻辑。

3.10 最小意外原则

接口的设计尽量不要让使用者感到意外,让接口运行符合用户的预期。对于一些约定俗成的东西,我们去遵循就好了,我们往往希望看到熟悉的东西就是自己熟知的东西,而不是还需要一层映射才能理解。当然有时候场景的区分需要通过不同来显示区分,比如get从一个模块获取数据,而fetch用以表示获取远程模块的数据。这个fetch和git中的概念就是保持一致的。

3.11 沉默原则

软件应该将希纳是的内容控制在最少的程度,除非遇到必须提醒的情况。只输出重要的信息,内部运行的信息不要参杂其中。对于错误发生时只将真正错误的部分作为标准错误输出,不输出所需范围之外的任何数据。对于调试而需要的信息,可以通过一个冗余模式的开关,默认让其处于关闭状态。

3.12 修复原则

软件运行过程中,出现错误且修复失败,就应该立即停止处理,同时还应该有明显的提示。对修复失败情况下继续处理,就会出现最坏的结果,故障可能在不知不觉中破坏所有的数据。

软件在发生错误时应尽早让人立刻知晓,软件在无法自行修复的时候,需要把控制权交给用户进行判断,这点很重要。

3.13 经济原则

程序员的时间时宝贵资源,值得珍惜。任何浪费程序员的时间都是不经济的。

3.14 生成原则

生成原则指编写用于生成代码的代码,减少手动操作,编写用于生成代码地代码。只在优先的范围内生成代码也是很有效果的,重复的、形式固定的代码尤其适合由代码生成器生成。

3.15 优化原则

在代码优化之前,先要保证代码能够正常运行。代码尚且不能充分运行阶段,对细节进行打磨是一种吃力不讨好的做法。先要确保代码正确运行,再去追求所谓的运行速度,确保足够的简单。代码越简单,可优化的地方就越容易被找到。

3.16 多样性原则

容许存在多种方法,在软件中没有唯一正确的方法。我们不能相信任何宣传软件开发存在唯一正确方法的言论。应该认可多样性,调动思维,不断寻找更好的方案。

3.17 可扩展性原则

通过代码可扩展,代码能满足多样的需求。可以采用可插拔的设计,对于扩展可插拔接口部分,要采用灵活的设计,并且在代码一旁加上“如果需要xxx”的注释。另外,可扩展性并不是添加非必要的功能,而是要编写能在需要的时候轻松添加相应功能的代码。

可扩展性同样适用于数据格式,要让数据格式具有自我描述和可扩展性。让数据格式由几个独立的自我描述部分组成能够取得更好的效果,而不依赖顺序,甚至依赖其他数据。这样做的好处,可以在不搞混所读取的代码的前提下添加新的部分和删除旧的部分。

REF


  1. 罗塞塔石碑(古埃及托勒密王朝著名石碑)_百度百科 (baidu.com) ↩︎

Liao lile
Liao lile
架构师 | 系统工程师

目前主要方向在微服务、服务网格等云原生相关领域技术