前言
一个软件轻量简单的软件架构是非常重要的,它可以让我们花最小的代价就能满足业务上的需求。那如何保证轻量简单呢?那今天就和大家分享下这其中的秘密,也就是3个重要的指导原则,KISS原则,YAGNI原则和DRY原则,你们都知道并且理解吗?
KISS原则
KISS原则, 英文全称Keep it simple and stupid。核心思想就是尽量保持简单。
KISS原则指导我们在软件设计的时候要尽量保持简单,使用一些成熟的、适合业务的技术方案。另外从一个使用者的角度来思考,你设计时要思考如何让自己的架构设计变得简单,足够易用,比如你开发的框架是不是对于接入成本低甚至0成本? 你设计的框架是否不侵入业务代码?
不仅软件架构设计层面,在代码层面也处处要体现KISS原则。代码的可读性和可维护性是衡量代码质量非常重要的两个标准。而 KISS 原则就是保持代码可读和可维护的重要手段。代码足够简单,也就意味着很容易读懂,bug 比较难隐藏。即便出现 bug,修复起来也比较简单。
我们来看下面校验IP是否合法的3种实现方式哪个最“KISS”,大家觉得是哪个呢?
方式一
方式二
方式三
- 方式一代码量最少,正则表达式本身是比较复杂的,写出完全没有 bug 的正则表达本身就比较有挑战;另一方面,并不是每个程序员都精通正则表达式。对于不怎么懂正则表达式的同事来说,看懂并且维护这段正则表达式是比较困难的。这种实现方式会导致代码的可读性和可维护性变差,所以,从 KISS 原则的设计初衷上来讲,这种实现方式并不符合 KISS 原则。
- 方式二使用了 StringUtils 类、Integer 类提供的一些现成的工具函数,来处理 IP地址字符串,逻辑清晰,可读性好。
- 方式三不使用任何工具函数,而是通过逐一处理 IP 地址中的字符,来判断是否合法,容易出bug,不好理解。
小结: 综合来看,第二种方式更符合KISS原则。并不是代码越少越好,还要考虑代码是否逻辑清晰、是否容易理解、是否够稳定。
那如何写出满足 KISS 原则的代码?
- 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。
- 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出bug 的概率会更高,维护的成本也比较高。
- 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。
YAGNI原则
YAGNI 原则的英文全称是:You Ain’t Gonna Need It。中文大意是说你根本不需要这么做。核心思想就是不要过度设计。
那什么是过度设计呢?比如说你的应用,现在还是处在一个单体应用的阶段,那么这时候我就考虑啊,分布式事务该怎么做,那或者说你的数据库还是一个单库的时候,我就在思考。如何去做数据异构,甚至是你的业务逻辑在并不复杂的阶段的时候,你就再去思考如何去上一个工作流引擎,或者是规则引擎。那再比如现在图形数据库非常的火,那你就在想方设法的把图形数据库引入到你自己的项目当中。那这些是什么呢啊?说好听点叫面向简历编程,说难听点就是什么?脱裤子放屁,多此一举。我们提倡面向未来的架构是什么意思啊?是去规划你未来技术发展的路线,当这一天需要到来的时候,你有能力去实现它。而不是说你现在就把未来不需要的东西去搬到自己的项目当中。
所以千万不要为了技术而技术,一定要让技术服务于业务,谨遵YAGNI原则,过度设计是灾难。
DRY原则
DRY原则,英文全称Don’t Repeat Yourself,直译过来就是不要重复你自己。那这里的重复是什么意思?就是指代码一模一样的才算重复吗?实际不是的,我这里从实现逻辑重复、功能语义重复和代码执行重复三个点来理解重复的意思。
实现逻辑重复
我们看下下面的例子:
- 校验用户名
- 校验密码
虽然上面两个方法的代码逻辑是重复的,但是他们语义不重复,一个是用来校验用户名,一个是用来校验密码的,所以是符合DRY原则。如果我们强行把他们封装成isValidUsernameOrPassword()方法,反而不符合单一职责原则,万一哪天密码的校验逻辑变了怎么办呢?所以并不是说代码一样就一定违反了DRY原则。反而有时候代码不一样,恰好违反了DRY原则。
功能语义重复
直接上例子,项目中不同的同事由于不知道就写了两个校验IP的方法。
- 代码一
- 代码二
尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。我们应该在项目中,统一一种实现思路,所有用到判断 IP 地址是否合法的地方,都统一调用同一个函数。不然哪天校验规则变了,很容易只改了其中一个,另外一个漏改,就会出现莫名其妙的bug。
代码执行重复
我们看下面一个登录的代码例子。
你有没有发现email的校验逻辑被执行了2次,这种重复执行的情况我们也可以认为违反了DRY原则。
说了这么多,那有什么好的办法提高代码复用性,保证DRY原则呢?
- 使用现成的轮子,不轻易造轮子
其实最关键的就是写代码带脑子,用到一个方法先看看有没有现成的,不要看看不看,就动手在那里造轮子。
- 减少代码耦合
对于高度耦合的代码,当我们希望复用其中的一个功能,想把这个功能的代码抽取出来成为一个独立的模块、类或者函数的时候,往往会发现牵一发而动全身。移动一点代码,就要牵连到很多其他相关的代码。所以,高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。
- 满足单一职责原则
我们前面讲过,如果职责不够单一,模块、类设计得大而全,那依赖它的代码或者它依赖的代码就会比较多,进而增加了代码的耦合。根据上一点,也就会影响到代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。
- 模块化
这里的“模块”,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。
- 业务与非业务逻辑分离
越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。所以,为了复用跟业务无关的代码,我们将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。
- 通用代码下沉
从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,我们只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层。
- 继承、多态、抽象、封装
在讲面向对象特性的时候,我们讲到,利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,我们可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。
- 应用模板等设计模式
一些设计模式,也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复用。
总结
本文和大家介绍了软件工程领域中保持简单架构设计的三个原则,其实所有的这些原则就是为了达到一个很简单的目的,try everything to keep it simple,软件设计上的简单轻量是非常重要的。