前置知识
- 已学习六大设计原则
- 了解设计模式基础
- 有项目经历
前言
在前面的几篇设计原则文章中,我们分别讲述了经典的六大设计原则。但是事实上,我们在开发中还有几个重要的设计原则,在这篇文章中,一并给大家讲述。
这几个原则分别是 KISS 原则、YAGNI 原则、DRY 原则 。
下面,本文将逐个阐述这几个原则的重点,以及不同。
KISS 原则
KISS 原则,其英文阐述是:Keep It Simple and Stupid.
译为中文就是:尽量保持简单。
什么尽量保持简单?如何界定简单?
事实上,KISS 原则是较为广泛的原则,譬如其应用在建筑上面叫做轻奢,而应用在手机设计上面叫做极简风,他们所遵循的都是简单原则。
那么在编程中的 KISS 原则,具体讲的是什么的简单呢?
可能有人对此做严格定义,代码行数少的、或者说逻辑简单的,就是符合KISS 原则。但实际上,KISS 原则和之前的设计原则一样,都是 具体问题具体分析的,在不同的业务场景下判定是否符合 KISS 原则的标准也是不同的。
而判断是否符合 KISS 原则,有以下两条标准
- 一般情况下,逻辑简单、实现难度小、可读性好的代码,符合 KISS 原则。由于软件开发往往是会有时限的,所以我们都会需要追求性能和时间的平衡。那么基于这一点,我们认为业务代码的实现有 Java包 或者 现成库 的,直接调用来实现业务即可;这样子的代码便是逻辑简单,实现难度小的代码,同时使用这些方法也会让我们的代码可读性较好。若没有可调用的类库,我们再做简单的处理即可。
- 而当业务问题本身就复杂且重要的时候,我们的代码逻辑复杂视为是遵循 KISS 原则的。因为复杂的问题就是要用复杂的方法来解决。但是我们要界定清楚该问题是否为影响系统的主要问题,用复杂的方法解决该问题是否能大幅提高我们系统的性能;如果不能,那使用复杂的逻辑来解决这个问题就不符合 KISS 原则了,那倒不如使用快捷的类库性价比高,而不是自己编写的某种高性能的算法。
如何编写满足 KISS 原则的代码?
那么我们应该如何编写满足 KISS 原则的代码呢?我们在日常开发中注意以下几点即可。
- 不要重复造轮子。使用语言自带的类库或者业内著名的开源库,实现业务的性价比是最高的,出错几率小也易于维护。
- 统一开发的技术栈。不要使用合作者不熟悉的技术或者领域开发,要统一团队的开发水平和能力范围,才不会出现你认为简单,我认为复杂、可读性差的问题。
- 不要过度优化。过度优化会造成代码可读性变差,且浪费时间。
- Code Review 。团队内定期 Code Review 可以发现很多问题。合作者认为你的代码不简单,难以阅读。那99%确定你的代码不符合 KISS 原则,不够简单了。
YAGNI 原则
YAGNI 原则,其英文阐述是:You Ain’t Gonna Need It
直译是:你不需要它。其意义是:不要去编码设计目前用不到的功能或代码,其核心意思是:不要过度设计
这个含义,好像和上面的 KISS 原则含义接近,那他们之间有何不同呢?
这两个含义其实是不同的。KISS 原则讲的是 “如何做”,如何尽量保持简单;而 YAGNI 原则讲的是“要不要做”,目前不需要的就不要做。
这个原则**如何应用呢?**例如,我在建立项目框架的时候,会提前引入大量的开源库,我是凭借经验引入的,这样子是违背 YAGNI 原则了。但当设计 EventBus
消息类的时候,我预测以后会有不同的消息类需要发送;虽然我不清楚是什么消息类,但是我对目前的功能做了抽象,预留了拓展接口。这样子是遵守了 YAGNI 原则。我们可以看到,前者属于过度设计,但是后者却并未过度设计。
DRY 原则
DRY 原则,其英文阐述是:Don’t Repeat Yourself
直译过来是:不要重复你自己。意思就是:不要写重复的代码。
那么问题来了,哪种算重复的代码 ?
不要写哪种重复的代码?
重复包括 实现逻辑的重复 、功能语义的重复、代码执行的重复
- 实现逻辑的重复
实现逻辑的重复指的是:两段代码实现的业务功能不同,但是他们内部实现的代码是一致的。
//姓名检查 public static void checkNameInput(TextInputLayout textInputLayout) { if (textInputLayout != null) { if (textInputLayout.getEditText().getText().length() > 10) { textInputLayout.setError("输入内容超过上限"); } else if (textInputLayout.getEditText().getText().length() < 2) { textInputLayout.setError("最少2位"); } else { textInputLayout.setError(null); } } } //Id检查 public static void checkIdInput(TextInputLayout textInputLayout) { if (textInputLayout != null) { if (textInputLayout.getEditText().getText().length() > 10) { textInputLayout.setError("输入内容超过上限"); } else if (textInputLayout.getEditText().getText().length() < 2) { textInputLayout.setError("最少2位"); } else { textInputLayout.setError(null); } } } 复制代码
- 例如上方的两端代码,他们内部实现的逻辑是一样的,但是他们不违背 DRY 原则 。因为他们的功能是不同的,一个是姓名检查,一个是Id检查。我们也无需将他们合并为一个函数,因为我们的业务随时可能变化,Id检查功能检测的输入上线有可能是要变化的,现在将其合并之后很可能还要重新拆解他们。而想减少重复的代码,做其他更细粒度的功能拆解,再使得他们组合完成一个大功能即可。
- 功能语义的重复
功能语义的重复指的是:虽然他们内部实现的代码是不同的,但是两段代码实现的业务功能是相同的。
//姓名检查 public static void checkNameInput(TextInputLayout textInputLayout) { if (textInputLayout != null) { if (textInputLayout.getEditText().getText().length() > 10) { textInputLayout.setError("输入内容超过上限"); } else if (textInputLayout.getEditText().getText().length() < 2) { textInputLayout.setError("最少2位"); } else { textInputLayout.setError(null); } } } //姓名检查 public static void nameInputCheck(TextInputLayout textInputLayout) { if (textInputLayout != null) { return; } if (textInputLayout.getEditText().getText().length() < 2) { textInputLayout.setError("最少2位"); } else if (textInputLayout.getEditText().getText().length() > 10) { textInputLayout.setError("输入内容超过上限"); } } 复制代码
- 上面两个函数的名称和实现逻辑都不同,但是他们违背了 DRY 原则。由于他们实现的是同一个功能,也许是出自两个工程师之手,但是我们必须更改,将其统一为使用同一个函数。因为也许后续业务需求的更改,需要更改这个功能,但是由于有两个不同的实现,我们很可能忘记某个更改某个实现,导致出现业务更替不全面出现bug。
- 代码执行的重复
代码执行的重复指的是:原本只需要某个功能一次的代码,出现了重复执行的问题。
这个重复就不做代码解释了,相信大家都容易理解。这种重复也算违背了 DRY 原则,因为重复的代码如果是IO操作或者说网络操作,那么对系统执行时间的影响就会非常大,这时候我们就需要重构除去这种重复。
由上述可知,功能语义的重复、代码执行的重复 都是违反 DRY 原则的
何为代码复用?
我们上面说了那么多代码重复,那么和我们常说的 代码复用 又冲突么?还有,代码复用性又是什么呢?
代码复用:一种行为,在开发新功能的时候,尽量复用已经存在的代码
代码复用性:表示一段代码可被复用的特性或能力,我们编码时候需要让代码尽量可复用
复用性和复用是不同的,前者是从代码开发者的角度来看的,后者是从使用者角度来看的。
“不重复”并不代表“可复用“。DRY 中强调不重复,但不一定就说里面的代码一定可复用。所以说这是两个概念的东西
那如何提高代码可复用性呢?
- 减少代码耦合
对于高度耦合的代码,当我们希望复用其中的一个功能,想把这个功能的代码抽取出来成为一个独立的模块、类或者函数的时候,往往会发现牵一发而动全身。移动一点代码,就要牵连到很多其他相关的代码。所以,高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。- 满足单一职责原则
我们前面讲过,如果职责不够单一,模块、类设计得大而全,那依赖它的代码或者它依赖的代码就会比较多,进而增加了代码的耦合。根据上一点,也就会影响到代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。- 模块化
这里的“模块”,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。- 业务与非业务逻辑分离
越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。所以,为了复用跟业务无关的代码,我们将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。- 通用代码下沉
从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,我们只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层。- 继承、多态、抽象、封装
在讲面向对象特性的时候,我们讲到,利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,我们可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。- 应用模板等设计模式
一些设计模式,也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复用。
除了刚刚我们讲到的几点,还有一些跟编程语言相关的特性,也能提高代码的复用性,比如泛型编程等。实际上,除了上面讲到的这些方法之外,复用意识也非常重要。在写代码的时候,我们要多去思考一下,这个部分代码是否可以抽取出来,作为一个独立的模块、类或者函数供多处使用。在设计每个模块、类、函数的时候,要像设计一个外部 API 那样,去思考它的复用性。tips:第一次编写代码的时候,我们不考虑复用性;第二次遇到复用场景的时候,再进行重构使其复用。