当前位置:   article > 正文

设计模式学习笔记 - 设计原则与思想总结:1.总结回顾面向对象、设计原则、编程规范、重构技巧等知识点

设计模式学习笔记 - 设计原则与思想总结:1.总结回顾面向对象、设计原则、编程规范、重构技巧等知识点

概述

对前面的内容的回顾,温故而知新,包括:面向对象、设计原则、规范与重构三个模块的内容。
在这里插入图片描述


1.代码质量评判标准

如何评价代码质量的高低?

代码质量的评价具有很强的主观性,描述代码质量的词汇也有很多,比如可读性、可维护性、灵活、优雅、简洁。这些词汇是从不同的维度去评价代码质量的。它们之间有相互作用,并不是独立的,比如,代码的可读性好、可扩展性好就意味着代码的可维护性好。代码质量高低是一个综合各种因素得到的结论。我们并不能通过单一维度去评价一段代码的好坏。

最常用的评价标准有哪几个?

最常用到几个评判代码质量的标准有:可维护性、可读性、可扩展性、灵活性、简洁性、可复用性、可测试性。其中,可维护性、可读性、可扩展性又是提到最多、最重要的三个评价标准

如何才能写出高质量的代码?

要写出高质量的代码,我们就需要掌握一些更加细化、更加能落地的编程方法论,这就包含面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。

面向对象

1.面向对象概述

现在,主流的编程范式或者编程风格有三种,它们分别是面向过程、面向对象和函数式编程。面向对象这种编程风格又是这其中最主流的。现在比较流行的编程语言大部分是面向对象编程语言。大部分项目也是基于面向对象编程风格开发的。面向对象编程因为其具有丰富的特性(封装、抽象、继承、多态),可以实现很多复杂的设计思路,是很多设计原则、设计模式编码实现的基础。

2.面向对象四大特性

封装也叫作信息隐藏和数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方法来访问内部信息或者数据。它需要编程语言提供权限访问控制语法来支持,例如 Java 中的 private、protected、public 关键字。封装特性存在的意义,一方面是保护数据不被随意修改,提高代码的可维护性;另一方面是仅暴露有限的必要接口,提高类的易用性。

如果说封装讲的是如何隐藏信息,那抽象讲的就是如何隐藏方法的具体实现,让使用者只需要关心方法提供了哪些功能,不需要知道这些功能是如何实现的。抽象可以通过接口或者抽象类来实现。抽象存在的意义,一方面是修改实现类不需要改变定义;另一方面,它也是处理复杂系统的有效手段,能有效地过滤不需要关注的信息。

继承用来表示 is-a 的关系,分为两种模式:单根继承和多继承。单继承表示一个子类只能继承一个父类,多继承表示一个子类可以继承多个父类。为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持。继承主要用来解决代码复用的问题。

多态是指子类替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类、duck-typing。多态可以提高代码的可扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。

3.面向对象 VS 面向过程

面向对象相比面向过程编程的优势:

  • 对于大规模复杂程序的开发,程序处理流程并非一条主线,而是错综复杂的网状结构。面向对象编程比起面向过程编程,更能应对这种复杂类型的系统开发。
  • 面向对象编程相比面向过程编程,具有更加丰富的特性(封装、抽象、继承、多态)。利用这些特性编写出来的代码,更加易扩展、易维护、易复用。
  • 从编程语言跟机器打交道方式的演进规律中,可以总结出:面向对象编程比起面向过程编程,更加人性化、更加高级、更加智能。

面向对象编程一般使用面向对象编程语言来进行,但是不用面向对象编程语言,我们照样可以进行面向对象编程。反过来讲,即便我们使用面向对象编程语言,写出来的代码也不一定是面向对象编程风格的,也可能是面向过程编程风格的。

面向对象和面向过程两种编程风格不是完全对立的。在用面向对象编程语言开发的软件中,面向过程风格的代码并不少见,甚至在一些标准的开发库(如 JDK、Apache Commons、Google Guava)中,也有很多面向过程风格的代码。

不管使用面向过程还是面向对象来写代码,最终的目的还是写出易维护、易读、易复用、易扩展的高质量代码。只要我们能避免面向过程编程风格的一些弊端,控制好它的副作用,在掌控范围内为我们所用,就大可不用避讳在面向对象编程中书写面向过程风格的代码。

4.面向对象分析、设计与编程

面向对象分析(OOA)、面向对象设计(OOD)、面向对象编程(OOP),是面向对象开发的三个主要环节。

  • 面向对象分析就是要搞清楚做什么
  • 面向对象设计就是要搞清楚怎么做
  • 面向对象编程就是将分析和设计的结果翻译成代码的过程

需求分析的过程实际上是一个不断迭代优化的过程。我们不要视图一下就给出一个完美的解决方案,而是先给出一个粗糙的、基础的方案,有一个迭代的基础,然后再慢慢优化。这样一个思考过程能让我们摆脱无从下手的窘境。

面向对象设计和实现要做的事情就是把合适的代码放到合适的类中。至于到底选择哪种划分方法,判定的标准是让代码尽量满足 “高内聚、松耦合”、单一职责、对扩展开放对修改关闭等我们之前讲到的各种设计思想和原则,尽量地做到代码可复用、易读、易扩展、易维护。

面向对象分析的产出是详细的需求描述面向对象设计的产出是类。在面向对象设计这一环节,我们将需求转化为具体的类的设计。这个环节的工作可以拆分为下面四个部分。

  • 划分职责进而识别出有哪些类
    根据需求描述,我们把其中涉及的功能点一个个罗列出来,然后再去看哪些功能点职责相近,操作同样的属性,可否归为同一个类。
  • 定义类的属性和方法
    识别出需求描述中的动词,作为候选方法,再进一步过滤筛选出真正的方法。把功能点涉及的名词作为候选属性,然后同样进行过滤筛选。
  • 定义类与类之间的交互关系
    UML 统一建模语言定义了六种类之间的关系。分别是:泛化、实现、关联、组合、聚合、依赖。我们从更加贴近编程的角度,对类与类之间的关系做了调整,保留了四个关系:泛化、实现、组合、依赖
  • 将类组装起来并提供执行入口
    将所有的类组装在一起,提供一个执行入口。这个入口可能是一个 main() 函数,也可能是一个组合外部用的 API 接口。通过这个入口,可以出发整个代码跑起来。

5.接口VS抽象类

抽象类不允许被实例化,只能被继承。它可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫做抽象方法。子类继承父类必须实现抽象类中的所有抽象方法。

接口不能包含属性(Java 可以定义静态常量),只能包含方法,方法不能包含代码实现(Java8 以后可以有默认实现)。类实现接口的时候,必须实现接口中申明的所有方法。

  • 抽象类是对成员变量和方法的抽象,是一种 is-a 的关系,是为了解决代码复用问题。
  • 接口仅仅是对方法的抽象,是一种 has-a 的关系,表示具有一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高扩展性。

什么实时使用抽象类?什么时候使用接口?

  • 如果要表示 is-a 的关系,并且是为了解决代码复用问题,就用抽象类
  • 如果要表示 has-a 的关系,并且是为了解决抽象而非代码复用的问题,那就用接口

6.基于接口而非实现编程

应用这条原则,可以将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。

实际上,“基于接口而非实现编程” 这条原则的另一个表述方式是,“基于抽象而非实现编程”。后者的表述方式其实更能体现这条设计原则的设计初衷。在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。

越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能在不破坏原有代码设计的情况下灵活应对。而抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。

7.多用组合少用继承

为什么不推荐使用继承

继承是面向对象的四大特性之一,用来表示 is-a 关系,可以解决代码复用的问题。虽然继承有诸多作用,但继承层次过深、过复杂,也会影响到代码的可维护性。在这种情况下,我们应该尽量少用,甚至不用继承。

组合相比继承有哪些优势?

继承的主要作用有三个:表示 is-a 关系、支持多态特性、代码复用。而这三个作用都可以通过组合、接口、委托三个技术手段来达成。此外,利用组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题。

如何判断该用组合还是继承?

尽管鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。在实际的开发中,我们还是要根据具体的情况,来选择该用继承还是组合。

  • 如果类之间的结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承。
  • 反之,我们就尽量使用组合来替代继承。
  • 还有一些设计模式、特殊的应用常见,会固定使用组合或者继承。

8.贫血模型 VS 充血模型

平时做的 WEB 项目,大部分都是基于贫血模型的 MVC 三层架构,被称为传统开发模式。之所以称之为 “传统”,是相对于新兴的基于充血模型的 DDD 开发模式来说的。基于贫血模型的传统开发模式,是典型的面向过程的编程风格。相反,基于充血模型的 DDD 开发模式,是典型的面向对象的编程风格。

不过,DDD 也并非银弹。对于业务不复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的 DDD 开发模式有点大材小用,无法发挥作用。相反,对于业务复杂的系统开发来说,基于充血模型的 DDD 开发模式,因为前期需要在设计上投入更多的时间和精力,来提高代码的复用性和可维护性,所以相比于基于贫血模型的开发模式,基于充血模型的 DDD 开发模式更加有优势。

基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,主要区别在 Service 层。在基于充血模型的开发模式下,我们将部分原来在 Service 类中的业务逻辑移动到了一个充血的 domain 领域模型中,让 Service 类的实现依赖这个 domain 类。不过,Service 类并不会完全移除,而是保留一些不适合放在 domain 领域模型类中的功能。比如,负责与 Repository 层打交道、跨领域模型的业务聚合功能、幂等、事务等非功能性的工作。

基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,Controller 层和 Repository 层的代码基本上相同。这是因为,Repository 层的 entity 生命周期有限,Controller 层的 VO 只是单纯作为一种 DTO。两部分的业务逻辑都不会太复杂。业务逻辑主要集中在 Service 层。所以,Controller 层和 Repository 层继续沿用贫血模型的设计思路是没有问题的。

设计原则

1.SOLID 原则:SRP 单一职责原则

一个类只负责完成一个职责或者功能。单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时类职责单一,类依赖的和被依赖的类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、松耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明类的设计不满足单一职责原则:

  • 类中的代码行数、函数或者属性过多;
  • 类依赖的其他类过多,或者依赖类的其他类过多;
  • 私有方法过多;
  • 比较难给类起一个合适的名字;
  • 类中大量的方法都是集中操作类中的几个属性。

2.SOLID 原则:OCP 开闭原则

如何理解 “对扩展开发、对修改关闭”?

添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改代码已有代码(修改模块、类、方法、属性等)的方式来完成。

关于定义,我们有两点要注意。

  • 第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
  • 第二点是,同样代码的改动,在粗代码粒度下,可能被认为是 “修改”;在细代码粒度下,可能有被认为 “扩展”;

如何做到 “对扩展开发、对修改关闭”?

我们要时刻具备扩展意识、抽象意识、封装意识。在写代码时,我们要多花点时间思考下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更时,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。

很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码可扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。

3.SOLID 原则:LSP 里氏替换原则

子类对象(Object of subtype/derived class)能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

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

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

4.SOLID 原则:ISP 接口隔离原则

接口隔离原则的描述是:客户端不应该强迫依赖它不需要的接口。其中的 “客户端”,可以理解为接口的调用者。理解 “接口隔离原则” 的重点是理解其中的 “接口” 二字。这里有三种不同的理解。

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

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

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

5.SOLID 原则:DIP 依赖倒置原则

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

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

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

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

6.KISS、YAGNI 原则

KISS 原则的中文描述是:尽量保持简单。KISS 原则是保持代码可读性和可维护性的重要手段。KISS 原则中的 “简单” 并不是以代码行数来考量的。代码行数越少并不是代表代码越简单,我们还要考虑逻辑复杂度、实现难度、代码的可读性等。而且,本身就复杂的问题,用复杂的方法解决,也并不违背 KISS 原则。此外,同样的代码,在某个业务场景下不满足 KISS 原则,换一个应用场景可能就不满足了。

对于如何写出满足 KISS 原则的代码,总结了下面几条指导原则:

  • 不要使用同事不懂的技术来实现代码
  • 不要重复造轮子,善于使用已经有的工具类库
  • 不要过度优化

YAGNI 原则的英文全称是:You Ain’t Gonna Need It。直译就是:你不需要它。这条原则也算是万金油了。当用在软件开发中时,它的意思是:不要去设计用不到的功能,不要是编写当前用不到的代码。实际上,这条原则的核心思想是:不要过度设计

YAGNI 原则跟 KISS 原则并非一回事儿。KISS 原则讲的是 “如何做” 的问题(尽量保持简单),而 YAGNI 原则说的是 “要不要做” 的问题(当前不需要的就不要做)

7.DRY原则

DRY 原则中文描述是:不要重复自己,将它应用在编程中,可以理解为:不要写重复的代码

有三种代码重复的情况:实现逻辑重复、功能语义重复、代码执行重复。实现逻辑重复,但语义功能不重复的代码,并不违反 DRY 原则。实现逻辑不重复,但功能语义重复的代码,也算违反 DRY 原则。而执行代码重复的代码,也算违反 DRY 原则

此外,还j讲到了提高代码复用性的一些手段,包括:减少代码耦合、满足单一职责原则、模块化、业务与非业务逻辑分离、通用代码下沉、继承、多态、封装、抽象、应用模板等设计模式。

复用意识也非常重要。在设计每个模块、类、函数的时候,要像设计一个外部 API 一样去思考它的复用性。

我们在第一次写代码的时候,如果当下没有复用的需求,而未来的复用需求也不是特别明确,并且开发可复用代码的成本比较高,那我们就不需要考虑代码的复用性。在之后开发新的功能的时候,发现可以复用之前写的这段代码,那我们就重构这段代码,让其变得可复用。

相比于代码的复用,DRY 原则适用性更强一些。我们可以不写可复用的代码,但一定不能写重复的代码。

8.LOD原则

如何理解 “高内聚、松耦合”?

“高内聚、松耦合” 是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚” 用来指导类与类之间依赖关系的设计。所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓 “松耦合” 指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个代码的改动也不会或很少导致依赖类的代码改动。

如何理解 “迪米特法则”?

迪米特法则的描述为:不该有直接依赖关系的类,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望较少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。

规范与重构

1.重构概述

重构的目的:为什么重构(why)?

对于项目来说,重构可以保持代码质量持续处于一个可控状态,不至于腐化到无可救药的地步。对于个人而言,重构非常锻炼一个人的代码能力,并且是一件非常有成就感的事情。它是我们学习经典设计思想、原则、模式、编程规范等理论知识的练兵场。

重构的对象:重构什么(what)?

按照重构的规模,可以将重构分为大规模高层次的重构和小规模低层次的重构。

  • 大规模高层次重构包括对代码分层、模块化、解耦、梳理类之间的交互关系、抽象复用组件等等。这部分工作利用的更多的是比较抽象、比较顶层的设计思想、原则、模式。
  • 小规模低层次重构包括命名规范、注释、修正函数参数过多、消除超大类、提取重复代码等编程细节,主要是针对类、函数级别的重构。小规模低层次重构更多的是利用编码规范这一理论知识。

重构的时机:什么时候重构(when)?

一定要建立持续重构的意识,把重构作为开发必不可少的部分融入到开发中,而不是等到代码出现很大问题的时候,再大刀阔斧地重构。

重构的方法:如何重构(how)?

大规模高层次重构难度比较大,需要有组织、有计划地进行,分阶段小步快跑,时刻保持代码处于一个可运行的状态

小规模低层次重构,因为影响范围小,改动耗时短,所以,只要你愿意并且有时间,随时随地都可以去做

2.单元测试

什么是单元测试?

单元测试是代码层面的测试,用于测试 “自己” 编写的代码的逻辑正确性。单元测试顾名思义是测试一个 “单元”,这个 “单元” 一般是类或函数,而不是系统或模块。

为什么要写单元测试?

单元测试能有效地发现代码中的 bug、代码设计上的问题写单元测试的过程本身就是代码重构的过程。单元测试是对集成测试的有力补充,能帮助我们快速熟悉代码,是 TDD 可落地执行的折中方案。

如何编写单元测试?

写单元测试就是针对代码设计覆盖各种输入、异常、边界条件的测试用例,并将其翻译成代码的过程。可以利用一些测试框架来简化测试代码的编写。对于单元测试,我们需要建立以下正确的认知:

  • 编写单元测试尽管繁琐,但并不是太耗时;
  • 可以稍微放低单元测试的质量要求;
  • 覆盖率作为衡量单元测试好坏的唯一标准是不合理的;
  • 写单元测试一般不需要了解代码的实现逻辑;
  • 单元测试框架无法测试多半是代码的可测试性不好。

单元测试为何难落地执行?

  • 一方面,写单元测试本身比较繁琐,技术挑战不大,很多程序员不愿意去写。
  • 另一方面,国内研发比较偏向 “快、糙、猛”,容易因为开发进度紧,导致单元测试的执行虎头蛇尾,最后,没有建立单元测试的正确认识,觉得可有可无,单靠督促很难执行得很好。

3.代码的可测试性

什么是代码的可测试性

粗略的讲,所谓的代码的可测试性,就是针对代码编写单元测试的难易程度。

对于一段代码,如果很难为其编写单元测试,或者单元测试写起来很费劲,需要依靠单元测试框架很高级的特性,那往往就意味着代码设计得不够合理,代码的可测试性不好。

编写可测试性代码的最有效手段

依赖注入是编写可测试性代码的最有效手段。通过依赖注入,我们在编写单元测试代码的时候,可以通过 mock 的方法将不可控的依赖变得可控,这也是我们在编写单元测试的过程中最有技术挑战的地方。除了 mock 方式,我们还可以利用二次封装来解决某些代码行为不可控的情况。

场景的 Anti-Patterns

典型的、常见的测试不友好的代码有下面这 5 种:

  • 代码中包含未决行为逻辑;
  • 滥用全局变量;
  • 滥用静态方法;
  • 使用复杂的继承关系;
  • 高度耦合的代码。

4.大型重构:解耦

“解耦” 为何如此重要?

过于复杂的代码往往在可读性、可维护性上都不友好。解耦,保证代码高内聚、松耦合,是控制代码复杂度的有效手段。如果代码松耦合、高内聚,也就意味着,代码结构清晰、分层、模块化合理、依赖关系简单、模块或类之间的耦合小,那代码整体的质量就不会差。

代码是否需要 “解耦” ?

间接的衡量标准有很多,比如:

  • 改动一个模块或类的代码,受影响的模块或类是否有很多;
  • 改动一个模块或类的代码,依赖的模块或者类是否需要改动;
  • 代码的可测试性是否好…

直接的衡量标准是把模块与模块之间,及其类与类之间的依赖关系画出来,根据依赖关系图的复杂性来判断是否需要解耦重构。

如何给代码 “解耦”?

给代码解耦的方法有:封装与抽象、中间层、模块化,以及一些设计思想与原则,比如:单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特法则。当然还有一些设计模式,比如观察者模式。

5.小型重构:编码规范

前面讲了很多设计原则,后面还会讲到设计模式,利用好它们都可以有效地改善改代码的质量。但是,这些知识的合理应用非常依赖个人经验,有时候用不好会适得其反。但是编码规范正好相反,大部分都简单明了,在代码的细节方面,能立竿见影地改善质量。此外,前面也讲到,持续低层次小规模重构依赖的基本上都是这些编程规范,也是改善代码可读性的有效手段。

我总结罗列了 20 条编码规范,分为三个大类:命名与注释、代码风格、编程技巧。

命名与注释

  • 命名的关键是能准确的达意。对于不同作用域的命名,可以适当的选择不同的长度,作用域小的命名,比如临时变量等,可以适当的选择短一些的命名方式。此外,命名中也可以使用耳熟能详的缩写。
  • 借助类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命名。
  • 命名要可读、可搜索。不要使用生僻的、不好读的英文单词来命名。此外,命名要符合项目的统一规范,也不要用那些反直觉的命名。
  • 接口有两种命名方式。一种是在接口中带前缀 “I”,另一种是在接口的实现类中带后缀 “Impl”。两种命名方式都可以,关键要在项目中统一。对于抽象类的命名,我们更倾向于带有前缀 “Abstract”。
  • 注释的目的就是让代码更容易看懂,只要符合这个要求,你就可以写。总结一下的话,注释主要包含这样三个方面的内容:做什么、为什么、怎么做。对于一些复杂的类和接口,我们可能还需要写明“如何用”。
  • 注释本身有一定的维护成本,所以并非越多越好。类和函数一定要写注释,而且要写的尽可能全面详细些,而函数内部的注释会相对少一些,一般都是靠好的命名和提炼函数、解释性变量、总结性注释来做到代码易读。

代码风格

代码风格没有对错之分,不同的编程语言风格都不太一样,只要能在团队、项目中统一即可,不过,最好能跟业内推荐的风格、开源项目的代码风格一致。所以,这里就不展开罗列了,你可以对照这自己熟悉的编程语言的代码风格,自己复习一下。

编程技巧

  • 将复杂的逻辑提炼拆分成函数和类;
  • 通过拆分多个函数的方式来处理参数过多的情况;
  • 通过参数封装为对象来处理参数过多的情况;
  • 中不要使用函数参数来做代码执行逻辑的控制;
  • 移除过深的嵌套层次,方法包括:去掉多余的 if 或 else 语句,使用 continue、break、return 关键字提前退出嵌套,调整执行顺序来减少嵌套,将部分嵌套逻辑抽象成函数;
  • 用字面常量取代魔法值;
  • 利用解释性变量来解释复杂表达式。

统一编码规范

除了细节的知识点外,最后,还有一条非常重要的,那就是,项目、团队,甚至公司,一定要制定统一的编码规范,并且通过 Code Review 督促执行,这对提高代码质量有立杆见影的效果。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/421410
推荐阅读
相关标签
  

闽ICP备14008679号