先扯两句
其实今天还真没什么可扯的部分,之所以保留这个部分,单纯的就是为了发一下《设计模式》——目录,然后让我们进入正题。
定义
一向最讨厌定义的我,有的时候发现,没有定义还真就不行,比如前段时间同事们讨论区块链的种种的时候,我就一懵逼的默默点赞,一句话插不上,只因为我不清楚现在这么火的区块链究竟是个什么东西。
接口隔离原则也是一样,对于大多数人来说第一个懵逼的就是——级,接口究竟是个什么鬼!当然,年轻的我其实也并不是不了解什么是接口,毕竟平时写Android代码的时候经常会用到的OnClickListener就是接口:
public interface OnClickListener {
void onClick(View var1);
}
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
public void onClick(View view) {
}
}
可以看到,当我们创建接口的时候,是使用的interface,而当我们想要在某个类中实现这个接口的时候,则需要使用implements,这个就是我们最常使用的接口,也是被广泛认可的接口概念。
可是这也就是为什么前面小老儿我第一次说“年轻的我”的原因,因为在《设计模式之禅》中所介绍的接口彻底颠覆了我的认知:
- 实例接口(Object Interface),在Java中生命一个类,然后用new关键字产生一个实例,它是对一个类型的事物的描述,这就是一个接口……Person zhangSan = new Person();产生的实例中,Person类就是zhangSan的接口……Java中的类也是一种接口。
怎么样,是不是与我一样懵逼,但是这个是定义类的东西,在看这章的时候,不管我理解没理解,反正就是默认这种说法没错了,至于具体是不是这么归类的?请原谅我前面说的,自己是比较讨厌定义的,所以后续也没有对这个定义的说法继续求证,大家如果谁知道这种说法,或者其他的说法的,可以一起分享一下。
- 类接口(Class Interface),Java中经常使用的interface关键字定义的接口。
这个想必大家都熟悉了,就是我前面举的例子,就不需要额外说明了吧。当然,知道了接口,下面就可以看看,说明是接口隔离了:
- Client should not be forced to depend upon interfaces that they don't use(客户端不应该依赖它不需要的接口)
- The dependency of one class to another one should depend on the smallest possible interface(类间的依赖关系应该建立在最小的接口上)
关于这部分的解释,还是先引用书中的解释吧(别问我为什么不先说自己的理解,打死我也不会承认自己没看懂的):
- 依赖它需要的接口,客户端需要什么接口就提供什么接口把不需要的接口剔除掉,那就需要对接口进行细化,保证其纯洁性。
- 类间的依赖关系应该建立在最小的接口上,它要求是最小的接口,也就是要求接口细化,接口纯洁
不知道大家看了这个定义之后是什么感受,反正给我的第一反应就是当个接口也太难了!
虽然上面两种说法总结来说,实际上只有一个目的,那就是“接口细化”,这样做的意义是什么呢?除了懒汉外,作为一个骄傲的吃货,遇到这种问题,第一反应自然是以吃的东西去类比了。老北京炸酱面是比较有名的面食了,而对于我来说最让我感兴趣的并不是老北京炸酱面里的酱是怎么来的,而是其在名字中都没有提到的——菜码
图片取自:饿了 · 北京人做的真 · 老北京炸酱面(多图)
这里除了我们的主角:面和酱意外,还有豆芽菜、小水萝卜缨、青蒜、香椿芽、青豆嘴、黄瓜丝、芹菜等。对于我这种大东北的来说,其实这些都一样,毕竟怎么搭配都我都是能吃的。但是总还是有一些有忌口的,比如香椿的味道、蒜的味道等。
如果有忌口的人来了饭店,我们如果直接端上一碗料都完全拌在一起的炸酱面的时候,很可能就会激怒这些有忌口的客人,从而失去一部本客源。而如果我们如同上图中的方式上菜,客户可以根据自己的喜好添加对应的菜码,给了相同的菜码,既满足了我这种有多少吃多少,甚至恨不得再多吃点的客人,同时也可以满足那些有忌口的客人。唯一麻烦的地方不过是多刷几个碗而已。
而上面接口隔离的原则所描述的实际上就与炸酱面菜码分碗有相似的好处,我们需要做的就是将所可能用到的接口尽可能细化,就好像一种种的菜一样,然后供使用者根据自己的需求挑选最适合的搭配,而不是将所有待实现的方法都放在同一个接口中。
与单一职责原则的区别
而说到接口细化,其实在前面的《单一职责原则》中其实已经说过了,需要根据待实现的方法的职责,将他们划分到不同的接口中去实现,这其实就是接口细化的一种处理方案。但是既然已经说过一次了,为什么这里又要新定义一个接口隔离原则来实现相似的功能呢,难倒就是单纯的用来凑数吗?
当然不是,这里就要说一下《单一职责原则》与《接口隔离原则》只见的差异了。
接口隔离原则要求的是类与接口职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要求接口的方法尽量少。
这里还是使用炸酱面的例子,其实对于炸酱面来说,如果按照单一职责原则来说的话,其实总共分为三种职责:面、酱、菜码(因为不同地方,不同店铺的菜码实际上不同的,甚至还有一些炸酱面是没有菜码的),也就是说服务员只需要端上来一碗面、一碗酱、一碗面就可以了。而接口隔离原则,大家还是看前面的图就知道会端上来多少个碗了,而每个碗对应的就是代码中的一个接口。
这里以对蒜有忌口的情况封装一下我们的炸酱面类:
/**
* 酱
*/
interface Sauce {
void SauceAdded();
}
/**
* 面条
*/
interface Noodle {
void NoodleAdded();
}
/**
* 黄瓜
*/
interface Cucumber {
void CucumberAdded();
}
/**
* 蒜
*/
interface Garlic {
void GarlicAdded();
}
/**
* 为对蒜有忌口顾客提供的炸酱面
*/
class NoodlesWithSoyBeanPasteForAvoidFood implements Sauce, Noodle, Cucumber {
@Override
public void SauceAdded() {
System.out.println("酱味不错");
}
@Override
public void NoodleAdded() {
System.out.println("面条劲道");
}
@Override
public void CucumberAdded() {
System.out.println("黄瓜清爽");
}
}
/**
* 为对蒜没有忌口顾客提供的炸酱面
*/
class NoodlesWithSoyBeanPasteForOthers implements Sauce, Noodle, Cucumber, Garlic {
@Override
public void SauceAdded() {
System.out.println("酱味不错");
}
@Override
public void NoodleAdded() {
System.out.println("面条劲道");
}
@Override
public void CucumberAdded() {
System.out.println("黄瓜清爽");
}
@Override
public void GarlicAdded() {
System.out.println("蒜的味道也还好");
}
}
当然需要说明的是,这里并不是提倡大家一个接口只写一个方法,毕竟:
设计是有限度的,不能无限地考虑未来的变更情况,否则就会陷入设计的泥潭中而不能自拔。
因此在封装的时候,还是要量力而行,可以在架构的时候多头脑风暴几次后,先以结论搭建。在过程中,可以根据发现的一些未考虑到的情况,进行一些微调(主要是拆分,尽量不要合并)。而当版本稳定后,没有绝对的需求的情况下,即便发现有一些需要拆分的接口尽可能也先不要继续调整了,可以积累一定的需求后再一起完善,或者等到该模块重构时进行完善。
保证接口的纯洁性
接口隔离原则是对接口进行规范约束,其包含以下四层含义:
- 接口要尽量小
- 接口要高内聚
- 定制服务
- 接口设计是有限度的
接口要尽量小
关于这一点,前面已经说了很多了,翻译过来就是需要尽可能减少每个接口中的方法数量。
根据接口隔离原则拆分接口是,首先必须满足单一职责原则
就比如还是之前的炸酱面,黄瓜应该是这个世界上比较少人不爱吃的蔬菜了吧,所以如果颗粒度按照有没有人爱吃来分类,将黄瓜与酱、面划分到同一个接口中,而将其他忌口风险比较高的菜码放在一个接口中可以吗?答案自然是不可以了,你看人家的名字就知道了——“炸酱面”,为啥不叫“黄瓜炸酱面”?所以当我们只有酱和面的时候,它依然可以成为炸酱面。而如果只有黄瓜和酱,那充其量只能算是黄瓜蘸酱。所以可以看出,相对于炸酱面来说,黄瓜不是必需品,所以这就不满足单一职责原则了。
接口要高内聚
关于什么是高内聚,其实应该还有一个词语,叫做低耦合。这个定义相关的内容,大家如果有兴趣的话,可以去youngxs的什么是高内聚,低耦合中看一些介绍,当然也可以直接百度百科走起——高内聚低耦合。
而如果需要举一些例子的话,还是饭店中的事,我在跟服务员点菜的时候,服务员不仅要记录我点了一碗炸酱面,还要记录我的桌位是11号(别问我为什么是11号)、专业点的还会放一个上菜倒计时的沙漏。而服务员记录的这些信息在点菜的单子中与炸酱面的关系就是高内聚。因为我除了点菜外就不再需要说多余的话,炸酱面就可以按时送道我的餐桌上供我享用。
具体到接口隔离原则就是,要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本。
定制服务
这里说的是定制服务,其实就是刚刚说到的低耦合,也就是说,两个类之间,相关提供的调用方法越少越好,最好是除了必须要提供的意外,最好是多一个都没有。
你猜我会用什么举例子?没错,还是炸酱面!当服务员去为客户点餐的时候,想要知道客户有什么忌口,会怎么问呢?
- “请问您有什么忌口呢?”
- “请问您吃面吗?” “请问您吃酱吗?” “请问您吃黄瓜吗?” “请问您吃蒜吗?”……
不知道大家怎么想,我如果遇到第二种问法绝对直接摔门离开了。就算来吃面的时候不饿,这么一圈问下来,估计也早饿死了,所以为了活着,我也不得不走啊!
定制服务就是单独为一个个体提供优良的服务。我们在做系统设计时也需要考虑对系统之间或模块之间的接口采用定制服务。采用定制服务就必然有一个要求:只提供访问者需要的方法
接口设计是有限度的
- 接口的设计粒度越小,系统越灵活,这是不争的事实
- 灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低
所以在实际应用中,建议具体问题具体分析
最佳实践
1.一个接口只服务于一个子模块或业务逻辑
2.通过业务逻辑压缩接口中的public方法,接口市场去回顾
3.已经被污染了的接口,尽量去修改,若变更风险较大,则采用适配器模式进行转化处理
4.了解管径,拒绝盲从。每个项目或产品都有特定的环境因素,环境不同,接口拆分的标准就不同,所以拆分接口前请先深入了解业务逻辑
上面的3、4条有所删减,实在是打字太累了,就把一部分个人认为冗余的话省略掉了。