④ 接口隔离原则 (ISP,Interface Segregation Principle)
客户端不应该被强迫依赖它不需要的接口
,这里的客户端可以理解为 接口的调用者或使用者
,对应的服务端就是 接口的设计者或提供者
。
这里的 接口
只是一个方便描述的词汇,为了将我们的注意力从具体实现细节中抽离出来,可以将其理解为下面三种东西:
1) 一组API接口集合
比如:提供了一组用户相关的API给其他系统使用,包含注册、登录、获取用户信息等。现在,后台管理系统要实现一个删除用户的功能,直接在原有用户Service加上这个接口可以解决这个问题,但也带来了安全隐患。
所有用到这个用户Service的系统都可以调用这个接口,不加限制地被其他系统调用,很可能造成用户误删。
最好的解决方法是接口鉴权方式限制,而在代码设计层面,则可以参照隔离原则,将删除接口单独放到另外一个Service中,然后此Service只打包提供给后台管理系统使用。
2) 单个API接口或函数
函数的设计要功能单一,不要将多个不同的功能逻辑放在一个函数中实现。比如下面的代码示例:
public class Statistics { private Long max; private Long min; private Long average; private Long sum; // ...省略getter和setter等方法 } public Statistics count(Collection dataSet) { Statistics statistics = new Statistics(); // 计算max // 计算min // 计算average // 计算sum等 return statistics; }
这里的count()函数是否符合职责单一,还得看场景,比如每次统计count()中所有的统计信息都涉及,那就是职责单一的。
如果有些只用到max、min,有些只用到sum、average,那每次调用count()都要计算一遍所有统计信息,就很必要了,应该将其拆分成粒度更细的多个统计函数,如:
public Long max(Collection dataSet) { //... } public Long min(Collection dataSet) { //... }
单一职责原则 跟 接口隔离原则 的区别:
两者有点类似,但前者针对的是 模块、类、接口的设计,后者则更侧重于 接口的设计,思考角度也不同,它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定,如果调用者 只使用了部分接口或接口的部分功能,那接口的设计就不够职责单一。
3) OOP 中的接口概念
举例:项目中用到三个外部系统:Redis、MySQL、Kafka,每个系统对应一个配置信息的类:
public class RedisConfig { private ConfigSource configSource; //配置中心(比如zookeeper) private String address; private int timeout; private int maxTotal; //省略其他配置: maxWaitMillis,maxIdle,minIdle... public RedisConfig(ConfigSource configSource) { this.configSource = configSource; } public String getAddress() { return this.address; } //...省略其他get()、init()方法... public void update() { //从configSource加载配置到address/timeout/maxTotal... } } public class KafkaConfig { //...省略... } public class MysqlConfig { //...省略... }
接着增加需求:Redis 和 Kafka 配置信息需要热更新,MySQL不需要,抽象一个更新接口 Updater
public interface Updater { void update(); } public class RedisConfig implements Updater() { void update() { /*...具体实现*/ } } public class MysqlConfig implements Updater() { void update() { /*...具体实现*/ }} public class Application { ConfigSource configSource = new ZookeeperConfigSource(/*省略参数*/); public static final RedisConfig redisConfig = new RedisConfig(configSource); public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource); public static final MySqlConfig mysqlConfig = new MysqlConfig(configSource); public static void main(String[] args) { ScheduledUpdater redisConfigUpdater = new ScheduledUpdater(redisConfig, 300, 300); redisConfigUpdater.run(); ScheduledUpdater kafkaConfigUpdater = new ScheduledUpdater(kafkaConfig, 60, 60); kafkaConfigUpdater.run(); } }
接着又加了一个新需求:MySQL 和 Redis 需要监控功能,Kafka不需要,抽象一个监控接口 Viewer
public interface Viewer { String outputInPlainText(); Map output(); }
同理 MySqlConfig 和 RedisConfig实现此接口重写方法,ScheduledUpdater只依赖Updater接口而不用被强迫依赖不需要的Viewer接口,满足接口隔离原则。
如果不遵守这个原则,而使用一个大而全的Config接口,让每个Config继承,这样的结果是做了一些 无用功。
MySQL不需要热更新,却需要实现热更新的update()方法,Kafka不需要监控,也要实现监控相关的方法。出之外,往Config中添加新的接口,所有的实现类都要改动。
⑤ 依赖反转原则 (DIP,Dependence Inversion Principle)
高层模块不要依赖低层模块,应该通过抽象来互相依赖,抽象不要依赖具体实现细节,具体实现细节依赖抽象
。
看定义有点难理解,以上图为例:
上层是灯,下层是墙里的电线,灯直接依赖电线的话,意味着你要手动把灯焊到电线上,灯才能亮起来(高层依赖低层)。
挺智障的对吧?它们间的交互其实就是 连接,不关心灯这边要怎么连,电线那边要怎么连,而是 抽象 出一个 协议/约束/规范 → 连接插座。
插座可不管你是灯、冰箱、4平方线还是6平方线(不依赖具体实现细节),但你要连接的话都得按插座规范来走(具体实现细节依赖抽象)。
使用DIP的意义:
- 有效地控制代码变化的影响范围 → 统一接口,接口不变,外部系统怎么变,内部系统不用变。
- 使代码具有更强的可读性和可维护性 → 代码通过统一抽象后,功能相同的处理都在同一个地方。
用上面的灯和电线写个例子:
public class Lamp { void weld(String origin) { System.out.println("焊接到" + origin); } } public class Wire { String pull() { return "墙里电线"; } } public class ConnectProcessor { private Lamp lamp; private Wire wire; public ConnectProcessor(Lamp lamp, Wire wire) { this.lamp = lamp; this.wire = wire; } public void connect() { lamp.weld(wire.pull()); } // 测试用例 public static void main(String[] args) { Lamp lp = new Lamp(); Wire we = new Wire(); ConnectProcessor processor = new ConnectProcessor(lp, we); processor.connect(); // 输出:焊接到墙里电线 } }
高层组件ConnectProcessor,低层组件Lamp和Wire,代码看似简单,却有两个问题,第一个:ConnectProcessor复用性差,要复用的地方要写很多重复代码,引入抽象隔离变化,定义一个单独的的IConnectProcessor接口,ConnectProcessor实现此接口:
public interface IConnectProcessor { void connect(Lamp lamp, Wire wire); } public class ConnectProcessor implements IConnectProcessor { @Override public void connect(Lamp lamp, Wire wire) { lamp.weld(wire.pull()); } public static void main(String[] args) { Lamp lp = new Lamp(); Wire we = new Wire(); ConnectProcessor processor = new ConnectProcessor(); processor.connect(lp, we); } }
清爽不少,接着第二个问题:高层组件依赖低层组件,后者发生改变也会影响前者,所以要对低层组件也进行抽象:
public interface ILamp { void weld(String origin); } public interface IWire { String pull(); } public class Lamp implements ILamp { @Override public void weld(String origin) { System.out.println("焊接到" + origin); } } public class Wire implements IWire { @Override public String pull() { return "墙里电线"; } } public interface IConnectProcessor { void connect(ILamp lamp, IWire wire); } public class ConnectProcessor implements IConnectProcessor { @Override public void connect(ILamp lamp, IWire wire) { lamp.weld(wire.pull()); } public static void main(String[] args) { ILamp lp = new Lamp(); IWire we = new Wire(); ConnectProcessor processor = new ConnectProcessor(); processor.connect(lp, we); } }
从ConnectProcessor依赖Lamp和Wire (上层依赖低层),到抽象出规则IConnectProcessor,然后模块的具体实现都依赖这个规则,这就是 依赖倒置 !!!
说完依赖反转,再说说经常听到的其他三个名词以防混淆:
1) 控制反转(IOC,Inversion Of Control)
一种较笼统的 设计思想
,一般用来指导 框架
层面的设计,就是将 程序执行流程的控制交给框架完成
。
2) 依赖注入(DI,Dependency Injection)
实现IOC的 设计模式
,在类外创建依赖对象,通过不同方式将对象提供给类(构造函数、属性、方法)。
3) 依赖注入框架(DI Framework)
用于实现自动依赖注入的框架,管理对象的创建及生命周期,并提供向类注入依赖项的具体实现,不用开发者手动创建和管理对象,现成的注入框架有很多,如Java Spring,Android中的ButterKnife、Dagger2等。