作为一个程序员,写出高质量的好代码是最基本的要求,本文主要讲解好代码有那些特征和怎么写出好代码,写出好代码必须掌握的代码设计知识和工具。
一、什么是好代码
(一)好代码的特征
好代码或高质量代码通常具备以下特征,我总结为六个维度,分别是:
可读可维护:关注如何减少代码的修改成本
灵活可扩展:关注如何让软件更容易适应变化
可复用:关注功能复用能力,用到时可以直接调用
可测试:关注如何降低发现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. 依赖反转原则
依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。
(二)软件设计之正交四原则
向着更稳定的方向依赖,是接口分离原则(ISP)的另一种说法。
(三)软件实现常用五大算法
1、递归与分治
2、动态规划
3、贪心算法
4、回溯法
5、 分支限界法
三、本地代码检查
糟糕的源代码质量会给开发人员和产品所有者带来大量的头痛、过度工作和不眠之夜。规划和进行代码审查经常被管理层忽视,通常会对开发效率甚至整个业务产生长期的负面影响。
建议使用这些 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 还允许团队使用有向图和依赖矩阵来暴露架构缺陷、可视化和管理依赖关系。这只是该平台可以提供的众多有用功能中的一小部分。