0x2、KISS原则
Keep It Simple and Stupid.
→ 代码尽量保持简单
并不是代码行数越少就越简单 → 还要考虑逻辑复杂度、实现难度、代码可读性等。也不是代码逻辑复杂就违背KISS原则 → 本身就复杂的问题,用复杂的方法解决就不违背(如KMP算法)。
同样的代码,在某个业务场景下满足KISS原则,换个场景可能就不满足了。
如何写出满足KISS原则的代码:
- 不要使用同事可能不懂的技术来实现代码(如正则,编程语言中过于高级的语法);
- 不要重复造轮子,而是善用已经有的工具库类;
- 不要过度优化,过度使用一些奇技淫巧 (位运算代替算数运算,复杂的条件语句代替if-else)
代码是否足够简单也是挺主观的判断,可以通过代码Code Review间接验证。
顺带提提 YAGNI原则
,You Ain't Gonna Need It → 你不需要它,核心思想就是:
不要做过度设计,当前不需要的就不要做!(比如引入一堆当前不需要的依赖库)
!!!不代表不需要考虑代码的扩展性,还是要预留好扩展点,等需要的时候再去实现。
0x3、DRY原则
Don't Repeat Yourself → 不要重复自己,编程中的三种代码重复:
① 实现逻辑重复
实现逻辑重复,但功能语义重复不重复,并不违反DRY原则,比如:有两个方法,一个用于验证用户名合法性,一个用于验证密码合法性,而验证逻辑现在都是一致的:判空 → 长度(4-64) → 由数组或字母组成。那问题来了:
验证逻辑的代码重复了,违反了DRY原则吧?把两个方法合成一个,岂不美哉?
恰恰相反,合了的话就违背单一职责原则和接口隔离原则了,而且合并了以后产品改需求的时候你可能又得拆,比如:用户名长度改为(4-20),支持emoji表情,23333。
另外,并没有违反DRY原则,语义上是不同的:一个验证用户名,一个验证密码,对于上面这种更好的解法是抽象成更细粒度函数的方式来解决,将:长度和字符限制的逻辑抽取成另外两个函数,动态传参。
② 功能语义重复
比如检查IP地址是否合法,项目里写了两套不同校验逻辑的方法,逻辑不重复,功能重复,违反DRY原则。
另外,这样的操作也是在 "埋坑",项目里一会调这个一会调那个,增加了接盘仔阅读的难度,以为有更深的考量,结果却是代码设计的问题。而且还有个坑,哪天判断IP是否合法的规则改了,改了一个忘了改另一个,或者根本不知道有另一个,就会出现一些莫名其妙的BUG。
③ 代码执行重复
比如验证用户登录是否成功的逻辑:
查数据库看用户名是否存在 → 存在,查数据库判断用户名和密码是否存在 → 存在,查用户信息返回
上面的查了3次数据库,实际上2次就可以了,检查是否存在那一步可以跳过,I/O操作是比较耗时的,尽量减少此类操作。
另外,有时可能重复调用了某个函数(比如校验email是否有效),可以试着把代码重构,移除重复代码。
代码复用性 (Code Reusability)
先区分概念:
- 代码复用 → 开发新功能尽量复用已存在的代码;
- 代码复用性 → 一段代码可被复用的特性或能力,写代码时应尽量让代码可复用。
- DRY原则 → 不要写重复代码;
如何提高代码复用性
减少代码耦合
;满足单一职责原则
;模块化
; (不局限与一组类构成的模块,还可以理解为单个类、函数)业务与非业务逻辑分离
; (越与业务无关的代码越易复用,抽取成通用的框架、类库、组件等)通用代码下沉
; (分层角度: 越底层代码越通用,应设计得足够可复用,杜绝下层代码调用上层代码)继承、多态、抽象、封装
;应用模板等设计模式
。
0x4、迪米特法则 (LOD,Law Of Demeter)
在讲解这个原则前,先了解下常说的 高内聚
和 低(松)耦合
:
① 高内聚
相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。
② 低耦合
类与类间的依赖关系简单清晰,即使两个类有依赖关系,一个类的代码改动,不会或者很少导致依赖类的代码改动。
③ 高内聚和低耦合的关系
高内聚 → 指导类本身的设计,低耦合 → 指导类与类间依赖关系的设计;
高内聚有助于低耦合,低耦合又需要高内聚的支持。
④ 最小知识原则
迪米特法则,单从名字上,根本猜不出这个原则讲什么,它的命名典故:
1987年秋由Ian Holland在美国东北大学为一个叫迪米特的项目设计的。
它还有一个更达意的名字,叫做 最小知识原则
,解读如下:
不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类间,尽量只依赖必要的接口。
有点懵逼,举个经典案例来帮助理解,超市购物流程的模拟
// 钱包类 public class Wallet { private float balance; // 钱包余额 public Wallet(float money) { this.balance = money; } // 依次是获取、设置、增加、减少余额的方法 public float getBalance() { return balance; } public void setBalance(float balance) { this.balance = balance; } public void increaseBalance(float deposit) { balance += deposit; } public void decreaseBalance(float expend) { balance -= expend; } } // 顾客类 public class Customer { private String name; private Wallet wallet; public Customer(String name, Wallet wallet) { this.name = name; this.wallet = wallet; } // 依次是设置&获取 名字和钱包的方法 public String getName() { return name; } public void setName(String name) { this.name = name; } public Wallet getWallet() { return wallet; } public void setWallet(Wallet wallet) { this.wallet = wallet; } } // 收银员类 public class Cashier { public void charge(Customer customer, float payment) { System.out.println("您需要支付:" + payment + " 元"); Wallet wallet = customer.getWallet(); if (wallet.getBalance() > payment) { wallet.decreaseBalance(payment); System.out.println("扣款成功,你钱包还剩下:" + wallet.getBalance()); } else { System.out.println("扣款失败,你钱包只有:" + wallet.getBalance()); } } } // 测试用例 public class Shopping { public static void main(String[] args) { Customer customer = new Customer("杰哥", new Wallet(100.0f)); Cashier cashier = new Cashier(); cashier.charge(customer, 66.6f); } } // 运行输出结果: // 您需要支付:66.6 元 // 扣款成功,你钱包还剩下:33.4
结果正常输出,看上去代码没啥毛病,对吧?但实际上去违背了迪米特法则,想想上面的流程:
结账时:顾客把钱包给收银员 → 收银员检查余额是否足够支付 → 够的话扣完里面的前然后顺带告诉下你余额???
你钱包里有多少钱关收银员屁事?这样的设计明显是不合理的:
收银员只管有没有收到足够的钱就好,顾客管好自己的钱包掏钱就好,通过 钱 这个要素解耦:
public class Customer { // ...新增一个支付现金的方法 public float payCash(float amount) { if(wallet != null) { if(wallet.getBalance() > amount) { wallet.decreaseBalance(amount); return amount; } } return 0; } } public class Cashier { // 修改此方法 public void charge(Customer customer, float payment) { System.out.println("您需要支付:" + payment + " 元"); float customerPay = customer.payCash(payment); if(customerPay == payment) { System.out.println("扣款成功,欢迎下次光临~"); } else { System.out.println("支付金额和待支付金额不一致!"); } } } // 运行输出结果如下: // 您需要支付:66.6 元 // 扣款成功,欢迎下次光临~
稍微动一下,利用迪米特法则,解耦且提高了代码的重用性,比如顾客改成微信支付、代付等,都不会收银员的收钱的行为。
不止是类设计用到了迪米特法则,平时常说的架构分层也是它的体现:
每层模块只能调用自己层中的模块,跳过某一层直接调用另一层中的模块其实就是违反了分层架构的原则。
当然迪米特法则也不是完美的:
拆分时容易引入很多过小的中间类和方法;不同模块间传递消息效率可能降低(需要跨越多个中间层模块);
⑤ 扩展:面向切面编程(AOP,Aspect Oriented Programming)
简单点说就是:在不修改已有程序代码功能的前提下给程序动态添加功能的一种技术。
迪米特法则是在 程序设计时(静态) 降低代码耦合,AOP则是在 程序运行期间(动态)。
OOP与AOP的区别
- OOP → 强调对象的内在自恰性,更适合业务功能,比如商品、订单、会员。
- AOP → 对于**
统一的行为动作
**,如日志记录、性能统计等,关注系统本身的行为,而不影响功能业务的实现和演进。
小结
内容有点多,整理下,方便记忆:
- 单一职责原则(SRP) → 类/模块只完成一个职责;
- 开闭原则(OCP) → 对扩展开放(提供方),对修改关闭(调用方) → 封装可变部分,提供抽象化的不可变接口供调用者调用;
- 里式替换原则(LSP) → 子类对象可以替换父类对象,同时保证程序的逻辑行为不变和正确性不被破坏;
- 接口隔离原则(ISP) → 不要给调用者提供一些它不需要的接口或方法;
- 依赖反转原则(DIP) → 高层模块不要直接依赖底层模块,而是模块间抽象出一个协议,通过实现这个协议来互相依赖;
- KISS原则 → 代码尽量保持简单;
- YAGNI原则 → 不要过度设计;
- DRY原则 → 不要重复自己,区分逻辑重复、语义重复、代码执行重复!
- 迪米特法则(LOD) → 不该有依赖关系的类不要依赖,有依赖关系的类尽量只依赖必要接口;