ISP
什么是ISP,之前总结过,详细内容可回顾《SOLID之ISP》[1]
简单总结:多餐少吃,不要大接口,使用职责单一的小接口。
just so easy!
不就是把大接口拆成小接口嘛!
然而,最近在review之前的代码时,发现了点问题。
简单介绍下背景业务知识,项目是处理发票业务,在公司报销过的人都了解,我们团建、出差,公办支出都会让商家开具一张发票,作为报销凭证。
那么一张发票在被上传到报销软件,行为分为几个部分:
1、上传识别:从一张发票图片,被OCR,识别出一份结构化数据
2、修改:修改发票信息,包括删除、编辑识别出的发票内容,甚至手工填写一张发票信息
3、验真:会调用国税接口,验证一下发票的真伪
4、查询:查看发票详情
每一部分都会有几个方法,为了避免胖接口,自然会拆分成职责更专注的小接口
使用IDEA绘制出类结构:
InvoiceVerifyService:表示发票验真职责
InvoiceDiscernService:表示发票识别职责
InoviceService:表示发票查询、编辑等职责
思路清晰,结构中正。
可在项目中却出现了一段这样的代码:
代码语言:javascript复制if(invoiceService instanceof InvoiceVerifyService){
InvoiceVerifyService verifyService = (InvoiceVerifyService)invoiceService;
}
看着instanceof关键字,就倍感别扭。要么抽象得不对,要么结构不对。
如果没有拆分成三个接口,肯定不需要这样的判断。
所以还得重新审视一下ISP。
ISP:接口隔离原则,里面两个关键词:“接口”和“隔离”;“隔离”相对比较简单,从单一职责角度,把职责不相关的行为拆分开。而“接口”则需要重新审视一下。
接口
其实每个人对接口的理解是不一样的,从分类上讲,大该两类,一是狭义:常被理解为像Java语言中的interface,或者模块内部的使用;二是广义:系统间交互契约。
Martin Fowler给了两种类型接口:RoleInterface和HeaderInterface
A role interface is defined by looking at a specific interaction between suppliers and consumers. A supplier component will usually implement several role interfaces, one for each of these patterns of interaction. This contrasts to a HeaderInterface, where the supplier will only have a single interface
大致也是这个意思。
广义
主要是系统间交互的契约。类似于一个系统的facade对外提供的交互方式。
就算你不设计接口,并不代表没有接口。不局限于语言层面的interface,而是一种契约。
最重要的原则是KISS原则,最小依赖原则或者叫最少知识原则,让人望文知义。
追求简单自然,符合惯例。
比如一个微服务用户系统提供了一组跟用户相关的 API 给其他系统使用,比如:注册、登录、获取用户信息等。
还包含了后台管理系统需要的删除用户功能,如果接口不作隔离,具体代码如下所示:
代码语言:javascript复制
public interface UserService {
boolean register(String cellphone, String password);
boolean login(String cellphone, String password);
UserInfo getUserInfoById(long id);
UserInfo getUserInfoByCellphone(String cellphone);
boolean deleteUserByCellphone(String cellphone);
boolean deleteUserById(long id);
}
然而,删除操作只限于管理后台操作,对其他系统来讲,不仅是多余功能,还有危险性。
通过使用接口隔离原则,我们可以将一个实现类的不同方法包装在不同的接口中对外暴露。应用程序只需要依赖它们需要的方法,而不会看到不需要的方法。
代码语言:javascript复制
public interface UserService {
boolean register(String cellphone, String password);
boolean login(String cellphone, String password);
UserInfo getUserInfoById(long id);
UserInfo getUserInfoByCellphone(String cellphone);
}
public interface RestrictedUserService {
boolean deleteUserByCellphone(String cellphone);
boolean deleteUserById(long id);
}
public class UserServiceImpl implements UserService, RestrictedUserService {
// ...省略实现代码...
}
狭义
狭义常被理解为像Java语言中的interface,或者模块内部的使用。
单纯某一个接口,与单一职责一样,希望接口的职责单一,不要是胖接口、万能接口。
模块内部设计时,不管是模块调用模块,还是模块调用第三方组件。
我们一般有两种选择:
一、是直接依赖所基于的模块或组件;
二、是将所依赖的组件所有方法抽象成一个接口,让模块依赖于接口而不是实现。
其实这在之前对面向对象反思的文章中,提到过,打开我们90%的项目,所有的service都有对应的service接口和serivceImpl实现,整齐划一,美其名曰,面向接口编程。
然而,到项目生命周期结束,一个service都不会有两种实现。
所以,建议还是直接依赖实现,不要去抽象。如无必要,勿增实体。
如果我们大量抽象依赖的组件,意味着我们系统的可配置性更好,但复杂性也激增。
什么时候考虑抽象呢?
1、在需要提供多种选择的时候。比如经典的Logger组件。把选择权交给使用方。
这儿也有过度设计的情况,比如数据库访问,抽象对数据库的依赖,以便在MySQL和MongoDB之间切换,在绝大数情况下,这种属于过度设计。毕竟切换数据库本身就是件小概率事件。
2、需要解除一个庞大的外部依赖。有时我们并不是需要多个选择,而是某个依赖过重。我们在测试或其它场景会选择mock一个,以便降低测试系统的依赖
3、在依赖的外部系统为可选组件时。这个时候可以实现一个mock的组件,并在系统初始化时,设置为mock组件。这样的好处,除非用户关心,否则就当不存在一样,降低学习门槛。
回到文章篇头的问题,每个接口职责都是单一明确的,为什么还需要instanceof来判别类型?其实是更上层混合使用了
类似于:
代码语言:javascript复制Map<String,InvoiceService> invoiceServiceMap = SpringUtils.getBeans();
客户端使用时,得拆分开:
代码语言:javascript复制Map<String,InvoiceVerifyService> invoiceServiceMap = SpringUtils.getBeans();
Map<String,InvoiceService> invoiceServiceMap = SpringUtils.getBeans();
Map<String,InvoiceDiscernService> discernServiceMap = SpringUtils.getBeans();
当需要具体能力时,可以从对应的集合中获取对应的Service。而不是通过instanceof去判断。通过空间的换取逻辑的明确性。
VS SRP
接口隔离原则跟单一职责原则有点类似,不过稍微还是有点区别。
单一职责原则针对的是模块、类、接口的设计。
而接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同。
它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
总结
表达原则的文字都很简单,但在实践时又会陷入落地时的困境。
这些原则的背后,也体现了架构之道,虚实结合之道。从实悟虚,从虚就实。
References
[1]
《SOLID之ISP》: http://www.zhuxingsheng.com/blog/solid-isp.html