持续交付技术5:怎么写出好代码

文摘   2024-07-23 20:39   广东  

作为一个程序员,写出高质量的好代码是最基本的要求,本文主要讲解好代码有那些特征和怎么写出好代码,写出好代码必须掌握的代码设计知识和工具。

一、什么是好代码


(一)好代码的特征

好代码或高质量代码通常具备以下特征,我总结为六个维度,分别是:

  • 可读可维护:关注如何减少代码的修改成本

  • 灵活可扩展:关注如何让软件更容易适应变化

  • 可复用:关注功能复用能力,用到时可以直接调用

  • 可测试:关注如何降低发现bug和提高bug的定位效率的能力

  • 简洁高效:关注如何让代码跑的更高效

  • 可靠与健壮:关注怎么让代码不出问题,或减低出了问题的影响

1、可读可维护

关注点:关注如何减少代码的修改成本

代码特征:

代码应该容易阅读和理解,具有良好的命名习惯,使用有意义的变量、函数和类名。

代码结构清晰,逻辑流程直观,避免复杂的嵌套和冗长的函数。

代码应该是模块化的,实现高内聚低耦合,每个部分负责一个单一的功能,便于后续的修改和扩展。

代码应该易于调试,有清晰的错误处理和日志记录。

代码应该遵循特定的编程规范和最佳实践,如编码风格指南、设计模式等。

提升方法:

1)统一的编码规范

2)命名自注释

3)过程逻辑文档化:像文档一下把相关的类和函数逻辑清晰的组织在一起

4)模块化:领域驱动设计DDD,限界上下文

5)关注点分离:变化点隔离,防腐层设计

2、灵活可扩展

关注点:关注如何让软件更容易适应变化

代码特征:代码不写死,依赖抽象不依赖实现,使用配置文件或者参数化,实现依赖注入。

提升方法:

1)先设计再实现

2)面向接口编码

3)模块化设计

4)利用设计模式提高可变更性

3、可复用

关注点:注功能复用能力,用到时可以直接调用

代码特征:

代码应该设计得足够灵活,能够容易地添加新的功能或修改现有功能。

应该使用抽象和封装来提高代码的可重用性。

提升方法:

1)功能实现单一职责

2)模块化:组件模块化,类和函数模块化

3)业务和非业务逻辑分类

4)继承,多态,抽象,封装的应用

5)应用模板方法,桥接,享元等设计模式

4、可测试:

关注点:关注如何降低发现bug和提高bug的定位效率的能力

代码特征:

代码应该容易编写测试案例,包括单元测试和集成测试,确保代码的各个部分都能正常工作。

测试覆盖率应该较高,尤其是对于关键的业务逻辑。

代码语义化和功能单一,容易初始化,输出结果容易被见证,分类独立逻辑,分离实例状态,分离外部服务调用

提升方法:

1)单一职责

2)依赖注入

3)移除代码坏味道:过长参数,超长方法,大类

4)减少使用单例/静态等方法

5)与数据源解耦

6)显式依赖

5、简洁高效:

关注点:关注如何让代码跑的更高效

代码特征:

代码应该尽可能简洁,避免过度复杂的设计和实现。

应该尽量使用简单直接的方法来解决问题。

数据结构和算法高效,尽量减少不必要的计算和资源消耗。

对于性能关键的部分,应该进行优化以提高效率。

提升方法:

1)优化算法和数据结构选择

2)充分利用语言特征:lambda,foreach

3)减少资源消耗和优化内存管理:资源释放,日志IO,关注加载的资源

4)关注慢SQL

6、可靠与健壮:

关注点:关注怎么让代码不出问题,或减低出了问题的影响

代码特征:

代码应该能够在不同的输入和异常情况下保持稳定运行。

应该有恢复机制,能够在出现问题时迅速回到正常状态。

代码应该稳定,能够在不同的环境和条件下正常运行。

应该有健壮的错误处理机制,较强的容错性,能够妥善处理异常情况。

提升方法:

防御性编程:有输入验证,有边界处理,有日志记录

故障模式化设计:有异常处理机制


(二)怎么写出高质量好代码

首先,要提升写好代码意识

写好代码类似我们写作文时的时候写一手好字一样,是程序员一项必要的基础工作,是程序员的一个基本要求,高质量代码对更重要的是团队合作和产品交付整体效能都有很大的影响。

第二,要知道什么才是好代码

这就要有标准,那就是我们常常看到的各种各样的规范,但我觉得要有几个简单的原则,太多了,记不住,有几条原则简单的原则,可以时不时拿来判断,当前做得对不对。

然后就是去实践规范,这里需要一些技巧、一些工具,来帮助我们更好地遵循规范。

第三,掌握编码技能

需要掌握一些更加细化,更加能落地的编码方法论,包括面向对象设计思想,设计原则,设计模式,编码规范,重构技巧等,同时还要理解业务领域知识。

这些内容其实并不是很多,基本的内容花不了太多的时间。学习过程中多归纳总结,在实际工作中多思考,有意识的应用。

第四,在我们编码过程中一定要先设计再实现

先画4+1视图后UML图,考虑完全之后再开始写代码。

首先是业务流程图,它能快速构建起我们对业务的认知,带着对业务的理解再来看代码,事半功倍。然后是用例图,清晰地表达出我们系统的职责、边界、服务对象,结合业务流程图,能快速构建起我们对系统职责的认知。接着是架构图,从我们日常的设计需求来看,架构图是需要的。好的架构图能快速给人搭建起理解的框架,再来看系统的细节部分,就很好理解。架构图推荐 C4 规范,它是我目前接触的表达最清晰的架构图规范。接着再用时序图、状态图、ER图等把关键和复杂部分的设计表达出来。

最后,要学会不断重构

拥有好代码只有两种途径:

  • 第一种途径:先有好的设计--->然后用优秀的编码去实现--->再把优秀的编码风格延续下去

  • 第二种途径:对于历史已经是糟糕的代码--->不断去重构,向优秀的设计方案和代码风格不断逼近--->再延续下去

好代码离不开设计,如果完全不懂得设计,好代码将会无从谈起。但随着项目的推进,无论是开发者有意还是无意的,慎重还是草率的,都会逐渐积累技术债。跟人欠债过多会破产一样,技术债达到一定程度就会导致项目无法继续进行下去,因此我们要时不时地偿还债务。偿还债务的方式就是要识别坏味道,然后有针对性地进行重构。

二、必须掌握的编码设计知识


(一)SOLID 原则

SOLID 原则是面向对象设计和编程的5个核心原则。本文的目的不是详细讲解 SOLID 原则是什么或者深入讨论为什么你要遵循这些原则,而是指出在代码审查中怎么发现没有遵循这些原则的味道。

SOLID 代表:

  • S - 单一功能原则

  • O - 开闭原则

  • L - 里氏替换原则

  • I - 接口分离原则

  • D - 依赖反转原则


SOLID 原则,并非单纯的 1 个原则,而是由 5 个设计原则组成的,它们分别是:单一职责原则、开闭原则、里式替换原则、接口隔离原则和依赖反转原则,依次对应 SOLID 中的 S、O、L、I、D 这 5 个英文字母。

一,单一职责原则

单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。英文描述是:A class or module should have a single reponsibility。翻译成中文:一个类或者模块只负责完成一个职责(或者功能)。

一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。

评价一个类的职责是否足够单一,并没有一个非常明确的、可以量化的标准。实际上,在软件开发中,没必要过于未雨绸缪,过度设计,可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构。

二,开闭原则

开闭原则的英文全称是 Open Closed Principle,简写为 OCP。英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。翻译成中文:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。

1,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。

2,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。

三,里式替换原则

里式替换原则的英文翻译是:Liskov Substitution Principle,缩写为 LSP。英文描述是:Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。翻译成中文:子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

里式替换原则跟多态的区别

虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。

四,接口隔离原则

接口隔离原则的英文翻译是“ Interface Segregation Principle”,缩写为 ISP。Robert Martin 在 SOLID 原则中是这样定义它的:“Clients should not be forced to depend upon interfaces that they do not use。”直译成中文就是:客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。

这里的“接口”有下面三种情况:

1,一组 API 接口集合

2,单个 API 接口或函数

3,OOP 中的接口概念

如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。

如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。

如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

单一职责原则与接口隔离原则区别

单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

五,依赖反转原则

依赖反转原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.

翻译成中文,大概意思就是:高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

所谓高层模块和低层模块的划分,指在调用链上,调用者属于高层,被调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的。实际上,这条原则主要还是用来指导框架层面的设计。

拿 Tomcat 这个 Servlet 容器作为例子来解释。

Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Sevlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。

关联概念总结:

1.控制反转

实际上,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。

2. 依赖注入

依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。

3. 依赖注入框架

我们通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

4. 依赖反转原则

依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。


(二)软件设计之正交四原则

数学中的正交,是指相互垂直的两个向量,简单来讲就是平面上的两个垂直线段,其中一个线段变长或减短或者转圈圈,另外一根是不变的也不影响它们的垂直度的。表现为空间的独立性,在软件中我们可以理解为两个只有交叉点而互不影响的模块,是一种相互正交的模块化思想,模块内有更高的内聚,模块外有更低的耦合,只有一个不变的交叉点,这个交叉点就是我们所说的依赖点(或API接口)。

软件中的正交设计之正交四原则,最终目的是为了实现软件的高内聚低耦合架构。它是一种与范式,语言无关的设计原则。为了解决软件系统向未来演进的过程中,如何让软件在尽量长的时期范围内更容易应对变化,降低项目后期的人力投入成本。

正交四原则:

1 消除重复

2 分离关注点

3 缩小依赖范围

4 向着更稳定的方向依赖

消除重复

重复是万恶之源(Duplication is root of all evil)。

什么是重复代码?重复是不同的代码元素对同一知识进行了多次描述,无论描述方式是否一致。

消除重复的过程,本身就是一个提高系统可重用性的过程,而后者又会进一步降低未来产生重复的可能性。

消除重复原则对应SOLID原则中的单一职责原则(SRP)。

分离关注点

所谓关注点,小到一个函数,大到一个类,再或者是一个包,甚至更大的一个软件分层,都可以看作是一个关注点。从行为的角度看,是一个个功能职责,即在某个层面上,可以清晰描述它完成一件什么样的具体事情。

分离关注点,也即分离不同方向变化,是整个模块化思想的延伸,也就是单一职责和组合复用的表现。

缩小依赖范围

软件系统需要通过组件或模块之间的相互协作来实现各种功能。两个组件间的读写交互就会产生依赖关系。

依赖不可消除;否则,软件系统将毫无价值。

但是,为了实现一个功能,作为软件设计人员应尽可能地缩小依赖范围,避免不必要的依赖。

如何缩小依赖范围?如尽可能减少依赖点的数量,依赖点应该包含尽可能少的知识,依赖点应该高内聚,不应该强迫依赖方依赖它不需要的东西。

缩小依赖范围体现了高内聚,是迪米特法则的表现。

向着更稳定的方向依赖

依赖点越稳定,依赖方受依赖点变化的影响就越小。

站在需求的角度,而不是实现方式的角度定义依赖点(如API),会让依赖点更稳定。

需求是不断变化的,必须对需求进行抽象和建模,找出其中本质的东西,可以能使API的实现更稳定。

向着更稳定的方向依赖,是接口分离原则(ISP)的另一种说法。


(三)软件实现常用五大算法

1、递归与分治

递归算法:直接或者间接不断反复调用自身来达到解决问题的方法。这就要求原始问题可以分解成相同问题的子问题。

示例:阶乘、斐波纳契数列、汉诺塔问题

斐波纳契数列:又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、……在数学上,斐波纳契数列以如下被以递归的方法定义:F1=1,F2=1,Fn=F(n-1)+F(n-2)(n>2,n∈N*))。

分治算法:待解决复杂的问题能够简化为几个若干个小规模相同的问题,然后逐步划分,达到易于解决的程度。

1、将原问题分解为n个规模较小的子问题,各子问题间独立存在,并且与原问题形式相同

2、递归的解决各个子问题

3、将各个子问题的解合并得到原问题的解

示例:棋盘覆盖、找出伪币、求最值

棋盘覆盖:在一个(2k)*(2k)个方格组成的棋盘上,有一个特殊方格与其他方格不同,称为特殊方格,称这样的棋盘为一个特殊棋盘。要求对棋盘的其余部分用L型方块填满

2、动态规划

动态规划与分治法相似,都是组合子问题的解来解决原问题的解,与分治法的不同在于:分治法的子问题是相互独立存在的,而动态规划应用于子问题重叠的情况。

动态规划方法通常用来求解最优化问题,这类问题可以有很多可行解,每个解都有一个值,找到具有最优值的解称为问题的一个最优解,而不是最优解,可能有多个解都达到最优值。

设计动态规划算法的步骤:

1、刻画一个最优解的结构特征

2、递归地定义最优解的值

3、计算最优解的值,通常采用自底向上的方法

4、利用算出的信息构造一个最优解

示例:0-1背包问题,钢条切割问题等。

3、贪心算法

贪心算法是就问题而言,选择当下最好的选择,而不从整体最优考虑,通过局部最优希望导致全局最优。

贪心算法的要素

1)贪心选择性质:可以通过局部最优选择来构造全局最优解。换言之,直接做出在当前问题中看来最优的选择,而不必考虑子问题的解。

2)最优子结构:一个问题的最优解包含其子问题的最优解。

贪心算法的设计步骤:

1)将最优化问题转换为这样的形式:对其做出一次选择后,只剩下一个子问题需要求解

2)证明做出贪心选择后,原问题总是存在最优解,即贪心选择总是安全的

3)证明做出贪心选择后,剩余的子问题满足性质:其最优解与贪心选择组合即可得到原问题的最优解,这样就得到了最优子结构。

示例:背包问题,均分纸牌,最大整数

4、回溯法

回溯法是一种搜索算法,从根节点出发,按照深度优先搜索的策略进行搜索,到达某一节点后 ,探索该节点是否包含该问题的解,如果包含则进入下一个节点进行搜索,若是不包含则回溯到父节点选择其他支路进行搜索。

回溯法的设计步骤:

1)针对所给的原问题,定义问题的解空间

2)确定易于搜索的解空间结构

3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数除去无效搜索。

示例:0-背包问题、旅行商问题、八皇后问题

5、 分支限界法

和回溯法相似,也是一种搜索算法,但回溯法是找出问题的许多解,而分支限界法是找出原问题的一个解。或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解

在当前节点(扩展节点)处,先生成其所有的儿子节点(分支),然后再从当前的活节点(当前节点的子节点)表中选择下一个扩展节点。为了有效地选择下一个扩展节点,加速搜索的进程,在每一个活节点处,计算一个函数值(限界),并根据函数值,从当前活节点表中选择一个最有利的节点作为扩展节点,使搜索朝着解空间上有最优解的分支推进,以便尽快地找出一个最优解。

分支限界法:

1)FIFO分支限界法

3)优先队列分支限界法:按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。

示例:装载问题,旅行售货员问题

三、本地代码检查


糟糕的源代码质量会给开发人员和产品所有者带来大量的头痛、过度工作和不眠之夜。规划和进行代码审查经常被管理层忽视,通常会对开发效率甚至整个业务产生长期的负面影响。

建议使用这些 Java 代码审查工具:

1.Checkstyle

Checkstyle是一种静态代码分析工具,用于软件开发中,用于检查 Java 源代码是否符合编码规则。它基本上自动化了冗长的代码检查过程,并帮助 Java 开发人员执行编码标准。

它可以找到从类或方法设计问题到代码布局和格式问题的任何内容。您可以在此处找到完整的检查列表。

Checkstyle 也有大量的插件,可以让团队将持续的代码检查集成到他们的项目中。

2.PMD

PMD或编程错误检测器是一种开源静态源代码分析器,可报告应用程序代码中发现的问题。

该工具用于通过使用标准规则或定义自定义规则集来检测代码中的常见错误。使用 PMD,团队可以检测围绕命名约定、未使用的变量和参数、空捕获块、不必要的对象创建等的常见缺陷。

PMD 有 JDeveloper、Eclipse、jEdit、JBuilder、Maven、Ant、Gradle、Jenkins、SonarQube 和许多其他工具和 IDE 的插件。

PMD 还包含CPD(或复制/粘贴检测器),用于检测重复代码。我们发现这是一个非常有用的附加组件,因为很难找到重复代码,尤其是在大型项目中。

出于多种原因,消除重复的代码块很重要,例如在重构期间删除不必要的繁重工作。在对代码库进行重大更改时,它给开发人员带来了很大的压力,要记住它们的位置并对其进行编辑。

此外,如果他们在一个团队中工作,则开发人员无法知道其他团队成员在哪里插入了重复的代码段。这会使未来的开发和维护任务变得更加复杂。

除了 Java,它还可以用于 C、C++、PHP、Python、JavaScript 以及其他编程语言。

3.SonarQube

SonarQube是一个开源代码质量检测平台。它用于通过代码静态分析执行自动审查,以检测错误、编码错误和安全漏洞。

该平台提供关于重复代码块、编码标准、单元测试、代码覆盖率、代码复杂性、注释、错误等的报告。

它是分析 Java 代码的流行选择,特别是对于使用 Maven 和 Gradle 的团队,但其他人也可以通过手动提供字节码进行分析来使用它。

SonarQube 目前支持总共 27 种编程语言,例如 Java、C#、PHP、JavaScript、TypeScript、C/C++、Ruby、Kotlin、Go 和 Python。

4.JArchitect

JArchitect是一个专门用于 Java 代码的静态分析工具。它支持大量代码度量,如参数数量、变量和代码行数、圈复杂度、传入和传出耦合等。

JArchitect 还允许团队使用有向图和依赖矩阵来暴露架构缺陷、可视化和管理依赖关系。这只是该平台可以提供的众多有用功能中的一小部分。



研发效能方法论
分享内容的四大方向:研发效能和软件工程方法论,软件工程技术,平台工程设计,通用五力