设计模式之高质量代码
目录
公号:码农充电站pro
主页:https://codeshellme.github.io
如果有人问你,“什么样的代码是好代码”,你会怎样回答呢?
0,什么是高质量代码
我觉得回答这个问题,应该从两个方面考虑。
- 从业务角度考虑。首先,在公司开发一款软件,应该是业务在驱动。所以,从这个角度来说,代码第一个应该满足的是业务需求,如果连最基本的业务需求都满足不了,别的也就无从谈起。
- 从纯代码层面考虑。本篇我们重点要介绍的也就是这个问题。
那从纯代码层面来说,什么样的代码才是好代码呢?
通常会有以下几个判断标准:
- 可维护性:在当前代码的基础上,做修正或改进,是否容易做到?
- 可扩展性:当有了新的需求,在不对当前代码做大的改动的前期下,是否容易满足?
- 可复用性:代码是否能较容易的迁移到别的地方使用?不重复造轮子。
- 可读性:当其他人阅读代码,或者过一段时间自己再阅读,是否容易理解?
- 灵活性:是否足够灵活,易调整?
- 简洁性:是否简单,不复杂?
- 可测试性:是否容易测试正确性?
好的代码,不一定要满足以上所有的条件。但一条也不满足的代码,基本上就不是好代码了。
1,如何编写高质量代码
无规则不成方圆,写代码也是如此。要写出好的代码,需要遵守一些规则,主要有以下几个方面:
- 设计原则
- 设计模式
- 编码规范
- 持续重构
1.1,设计原则
每种设计模式都遵守了一个或多个设计原则。常用的设计原则有以下几种:
SOLID:
- 单一职责原则(SRP):一个类的职责要单一明确。
- 开闭原则(OCP):代码应该对扩展开发,对修改关闭(尽量减少对原有代码的修改)。
- 里式替换原则(LSP):能够使用父类对象的地方,也应该能使用子类。
- 接口隔离原则(ISP):接口的使用者不应该被强迫依赖它不需要的接口(使用多个专门的接口比使用一个单一的总接口要好)。
- 依赖倒置原则(DIP):高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。抽象不要依赖具体实现细节,具体实现细节依赖抽象。
其它:
- KISS 原则:尽量保持代码简单。
- YAGNI 原则:不要编写当前用不到的功能/代码,不要做过度设计。但并不是不需要考虑代码的扩展性。
- DRY 原则:避免重复性代码。
- LOD(迪米特) 原则:最小知识原则,每个模块只关心与自己关系密切的模块的有限知识。
另外在面向对象编程中,也有两个比较重要的编程原则:
- 基于接口而非实现编程:设计接口的时候要自顶向下,统揽全局,不拘泥于细节。
- 多用组合少用继承:继承的最大问题在于,当继承层次过深层,过于复杂,就会影响到代码的可读性和维护性。
在编码的过程中,要时常想着这些原则,思考自己的代码是否符合其中的某项或多项原则。
1.2,设计模式
常见的设计模式有23 种,下面会详细介绍。
1.3,编码规范
编码规范注重的是代码细节,主要目的是让代码具有可读性。整体上来说,好的代码,对外应该有一个统一的代码风格,代码风格不一定有好坏之分,但一定有是否统一之别。
另外,代码命名也很重要,大到项目命名,目录命名,包名等。小到类名,接口名,方法名,对象名,变量名等。命名最基本的要求是用词标准达意,让人一看知道大概的用途是什么。
还有,必要的地方要有必要的注释,对于他人及自己回头看代码都有帮助。
1.4,持续重构
随着项目需求的增加变化,代码结构,代码量也都会跟着变化。代码重构需要我们不断的从整体架构的角度审视整个项目代码的结构,是否已经变得混乱无序。只有不断的对代码进行重构,才能使得代码持续的具有可维护性,可读性等标准。
2,如何发现代码的问题
经过上文,我们已经知道了高质量代码的标准是什么。那么,当我们编写完一部分代码后,应该怎样判断自己写的代码是否是高质量呢?
文章开头已经提到过,好的代码应该从业务和纯代码两个角度来衡量,下面我们就从这两个角度来看,一般要对代码做哪些检查?
业务角度:
- 代码能否满足业务需求(逻辑是否正确,是否有bug)?
- 代码是否健壮,能否应对边界条件(特殊情况)?
- 软件性能是否足够,算法是否最优?
- 代码是否有线程安全问题,能够支持并发?
- 是否具备事物安全?
从纯代码角度考虑:
- 代码结构,目录划分是否清晰合理?
- 是否满足可维护性,可扩展性等标准?
- 是否遵循设计原则?是否过渡设计?
- 是否遵守代码规范,风格是否统一?
- 有无必要使用设计模式,运用是否得当?
- 有无单元测试,测试是否全面?
当我们完成某一阶段的代码后,可以尝试从以上几点来检查代码是否过关。
下面主要介绍设计模式。
3,设计模式
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
使用设计模式,可以使得我们编写的代码具有一个良好的结构,从而写出优雅的代码。设计模式关注的是,类与类之间以及对象与对象之间如何交互。
常见的设计模式有23 种,但并不是每一种模式都常用。这23 种设计模式可分为3 大类,分别是:
- 创建型:用于解决对象的创建问题。
- 结构型:用于处理类或对象之间的组合关系。
- 行为型:用于处理类或对象之间怎样交互及分配职责的问题。
下面介绍每类之中都包含哪些设计模式。
3.1,创建型
创建型包含5 种设计模式:
- 单例模式:保证一个类只能有一个实例,并提供一个全局访问点。
- 工厂方法:定义了一个创建对象的接口,由子类决定实例化哪一个类,使得类的实例化推迟到子类中。
- 简单工厂:严格来说不是一种模式,但经常用于封装创建对象的过程。
- 抽象工厂:提供了创建一系列相关对象的接口,而无需指定它们具体的类。
- 创建者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
- 原型模式(不常用):用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。
3.2,结构型
结构型包含7 种设计模式:
- 装饰者模式:动态的给一个对象添加其它功能。从扩展性来说,这种方式比继承更有弹性,更加灵活,可作为替代继承的方案。
- 适配器模式:将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
- 代理模式:为对象提供一个代理,来控制对该对象的访问。
- 桥接模式:将抽象部分与它的实现部分分离,使它们可以独立的变化。
- 组合模式(不常用):可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
- 外观模式(不常用):提供了一个统一的接口,用来访问子系统中的一群接口。它定义了一个高层接口,让子系统更容易使用。
- 享元模式(不常用):“享元”即共享单元,当一个系统中出现大量重复对象的时候,将对象设计成享元,以减少内存中对象的数量,节省内存。
3.3,行为型
行为型包含11 种设计模式:
- 观察者模式:定义了对象之间的一对多关系,以便当一个对象的状态发生变化时,所有依赖它的对象都能得到通知,并自动更新。
- 策略模式:定义一系列的算法,将它们一个个封装起来,让它们之间可以互相替换。可使得算法的变化独立于使用它的客户。
- 迭代器模式:提供一个方法,可以顺序访问集合中的元素,而不暴露集合的内部表示。
- 模板模式:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
- 状态模式:允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
- 职责链模式:多个对象可以处理同一个请求,这些对象连成一条链,并沿着这条链传递这个请求,直到有一个对象处理它。
- 访问者模式(不常用):表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下,定义作用于这些元素的新操作。
- 命令模式(不常用):将“请求”封装成对象,以便使用不同的请求,队列或日志来参数化其它对象。另外还支持可撤销的操作。
- 备忘录模式(不常用):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。以便以后恢复该对象。
- 中介模式(不常用):用一个中介对象来封装一系列的对象交互,从而最小化对象之间的交互关系,降低代码复杂度。
- 解释器模式 (不常用):为某个语言定义它的语法表示,并定义一个解释器,来解释这种语法。
4,UML 建模
设计模式中经常会用到UML 图来表示类与类之间的关系,下面介绍6 种UML 规定的类关系,分别是:
- 泛化
- 实现
- 聚类
- 组合
- 关联
- 依赖
各种关系之间的强弱性为:
- 泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖。
- 泛化与实现表示的关系最强。
- 依赖表示的关系最弱。
4.1,泛化关系
泛化可理解为继承关系。如下代码中,类B继承了类A。
public class A { ... }
public class B extends A { ... }
泛化关系用一个实线三角箭头表示,关系图如下,箭头由B 指向A,表示B继承了A:
4.2,实现关系
实现类和接口之间的关系,称为实现关系。
如下代码中,类B 实现了接口A。
public interface A {...}
public class B implements A { ... }
实现关系用一个虚线三角箭头表示,关系图如下,箭头由B 指向A,表示B 实现了A:
4.3,聚合关系
聚合是一种包含关系,A 类对象包含B 类对象,B类对象的生命周期可以不依赖A 类对象的生命周期。
如下代码中,A 类对象包含了B 类对象。
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
聚合关系用一个实线空心菱形箭头表示,关系图如下,箭头由B 指向A,表示A 中聚合了B:
4.4,组合关系
组合是一种包含关系。A 类对象包含B 类对象,B 类对象的生命周期依赖A 类对象的生命周期,B 类对象不可单独存在。
如下代码中,A 类对象包含了B 类对象。
public class A {
private B b;
public A() {
this.b = new B();
}
}
组合关系用一个实线实心菱形箭头表示,关系图如下,箭头由B 指向A,表示A 中组合了B:
4.5,关联关系
关联是一种非常弱的关系,包含聚合、组合两种关系。如果B 类对象是A 类的成员变量,那么B 类和A 类就是关联关系。
如下两种代码都是关联关系。
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
// 或者
public class A {
private B b;
public A() {
this.b = new B();
}
}
关联关系用一个实线箭头表示,关系图如下,箭头由A 指向B,表示A 关联B:
4.6,依赖关系
依赖是一种比关联关系更弱的关系,包含关联关系。只要B 类对象和A 类对象有任何使用关系。
如下三种代码都是依赖关系。
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
// 或者
public class A {
private B b;
public A() {
this.b = new B();
}
}
// 或者
public class A {
public void func(B b) { ... }
}
依赖关系用一个虚线箭头表示,关系图如下,箭头由A 指向B,表示A 依赖B:
本篇文章主要介绍了什么是高质量代码,如何发现代码中的问题,以及如何编写高质量代码。另外重点介绍了23 种设计模式都有哪些,及每种设计模式的含义。最后介绍了设计模式中经常用到的UML 关系都有哪些。
希望本篇文章对你有所帮助,如果哪里有任何问题也欢迎指正,谢谢!
(完。)
欢迎关注作者公众号,获取更多技术干货。
文章作者 @码农加油站
上次更改 2020-11-12