书接上回,本篇继续讲一下设计模式六大原则(有些书认为是7大原则)
接口隔离原则
要讲好这个原则,就得解释下几个概念。
接口(interface)
1>接口,抽象方法的集合,
2>接口,一种约束形式,为不相关的类提供约定好的处理服务(方法签名)
3>接口是类的拓展(一个类可以实现多个接口,意味着这个类可以扮演不同接口类型[接口多态])
隔离
汉语词典里面的解释
1>不让聚在一起,使断绝往来;
2>把患传染病的人或畜分开,避免接触。
接口隔离
这个解释起来有点拗口,先抛出结论,等下用代码说活。
1>一个类对另一个类的依赖应该建立在最小的接口上
2>客户端不应该依赖它不需要的接口
案例分析
需求:使用面向对象方式描述鱼和鸟各种行为动作
第一版:
行为接口
/**
* 行为接口:描述动物行为
*/
public interface IBehavior {
/**
* 飞行
*/
void fly();
/**
* 潜行
*/
void swim();
}
鱼:
public class Fish implements IBehavior{
public void fly() {
System.out.println("鱼:俺是折翼的鱼飞不起....");
}
public void swim() {
System.out.println("鱼:鱼翔浅底说的就是我...");
}
}
鸟:
public class Bird implements IBehavior {
public void fly() {
System.out.println("鸟:天高任我飞.....");
}
public void swim() {
System.out.println("鸟:羡慕鱼能游.....");
}
}
动物园:
public class Zoo {
public void showFly(IBehavior behavior){
behavior.fly();
}
public void showSwim(IBehavior behavior){
behavior.swim();
}
public static void main(String[] args) {
Zoo zoo = new Zoo();
Fish fish = new Fish();//鱼
Bird bird = new Bird();//鸟
zoo.showFly(bird);
zoo.showSwim(fish);
}
}
结果:
鱼:鱼翔浅底说的就是我...
鸟:天高任我飞.....
解析
上述设计存在2个问题点:
1:接口臃肿
在上案例中, 对Fish类来说,fly()方法是非必须的,对Bird类来说,swim()方法是非必须的,但是照java语法Bird跟Fish作为Animal子类必须去实现。这种臃肿(多余方法)接口设计,称之为胖接口,一点接口方法变动,会影响所有实现该接口的客户端类程序,开发中尽可能的避免这种臃肿接口设计。
2:客户端类依赖不需要的接口
按照Zoo类的设计,showFly() 方法应该要调用fly 相关的行为动作,showSwim()应该要调用swim 相关的行为动作,但是这2个方法都统一依赖IBehavior接口,语法上没错,错就在接口设计原则中,因为客户端类需要什么方法就提供什么功能,把不需要剔除,尽可能保证接口纯洁性,纯粹性。
第二版:
将IBehavior接口拆解成2个接口
/**
* 飞行为
*/
public interface IFlyBehavior {
/**
* 飞行
*/
void fly();
}
/**
* 游动行为
*/
public interface ISwimBehavior {
/**
* 潜行
*/
void swim();
}
如果有动物确实可以又飞,又游,IBehavior依然有效
/**
* 行为接口:描述动物行为
*/
public interface IBehavior extends IFlyBehavior, ISwimBehavior{
}
鸟:
public class Bird implements IFlyBehavior {
public void fly() {
System.out.println("鸟:天高任我飞.....");
}
}
鱼:
public class Fish implements ISwimBehavior{
public void swim() {
System.out.println("鱼:鱼翔浅底说的就是我...");
}
}
动物园:
public class Zoo {
public void showFly(IFlyBehavior behavior){
behavior.fly();
}
public void showSwim(ISwimBehavior behavior){
behavior.swim();
}
public static void main(String[] args) {
Zoo zoo = new Zoo();
Fish fish = new Fish();//鱼
Bird bird = new Bird();//鸟
zoo.showFly(bird);
zoo.showSwim(fish);
}
}
解析:
第二版设计使用接口拆跟接口继承方式解决胖接口问题,鱼,鸟他们只需要实现它们特有的行为接口即可, 如果需要fly跟swim行为,则可以使用IBehavior。当接口发生改动时,客户端类映射对应降低。
另外,也解决了接口依赖问题Zoo类的showFly,showSwim方法只需要依赖它们需要展示方法的接口,从而达到接口纯粹化目的
结论:
1>代码设计时,不应该强迫类实现它们不需要的接口方法,建议将臃肿接口按需拆分,按需实现。
2>客户端类对某个接口的需求越少越好,保证接口设计时做到专一。
思考:
问: 怎么设计意义在哪?
答:接口按需设计,按需实现,尽可能纯粹,尽可能专一,原因很简单:当某个客户端程序发生变化,迫使其接口跟随发生变更时,纯粹且专一的接口设计,可以降低因为接口变更导致其他客户端跟随变更的影响。这实际上就是避免接口污染问题很好设计方案。
注意:
接口设计不能太细,一个接口要具备一些基本的功能,能独一完成一个基本的任务,太细就有种过度设计意思。
接口隔离
回到本篇的主题,接口隔离:
1>客户端不应该依赖它不需要的接口
接口设计尽可能纯粹,专一,按需设计, 按需实现
2>一个类对另一个类的依赖应该建立在最小的接口上
客户端类按需设计,减少对接口依赖,缩减对接口依赖范围。
VS单一职责
从上面例子设计上看,细心的朋友会发现跟之前写的单一职责是同一个例子,是不是说接口隔离跟单一职责原则是同一个原则呢?可以说是,也可以说不是
是:
单一职责原则对接口做了约束,要求接口职责单一,代码上体现是接口单一化,细致化。
接口隔离原则也对接口做了月,也要求接口单一化,细节化。
不是:
1>单一职责原则除了对接口做约束外,也约束了,类,方法,而接口隔离约束对象只有接口
2>单一职责原则强调的是程序实现细节,业务功能实现尽可能独立;接口隔离原则针对是接口的抽象,强调类与接口交互。
总结:从代码落地上看2者是一致的,从架构设计上2者有区分的,一个强调内聚,一个强调耦合
运用
我们还是从JDK里面找例子,比如线程中2个较为特殊接口:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Runnable 接口是线程任务接口,与Thread配合完成线程任务定制,任务执行后,没有返回值。
Callable 接口,可调用接口,与Future + Thread/线程池 配合使用,在完成任务调用后有返回值。
public class App {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//Runable常规用法
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程干活了...");
}
});
thread.start();
//无返回的线程[非常规用法]
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("线程跑起来---无返回");
}
};
Callable callable = new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("线程跑起来---有返回");
return "这是返回值";
}
};
FutureTask taskCall = new FutureTask(callable);
FutureTask taskRun = new FutureTask(runnable, "没有返回值,给个默认值");
new Thread(taskCall).start();
new Thread(taskRun).start();
System.out.println(taskCall.get());
System.out.println(taskRun.get());
}
}
解析
看上面Runnable常规用法,也是我们平常写最多的一种用法,而非常规用法,则是加上FutureTask实现线程的控制。这里我们不研究线程怎么控制,仅仅研究接口设计。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
FutureTask taskCall = new FutureTask(callable);
FutureTask taskRun = new FutureTask(runnable, "没有返回值,给个默认值");
Runnable Callable 拆分成2个接口, Runnable任务调度无返回, Callable 任务调度有返回,FutureTask 正对这个2个接口设计独立依赖。这个也算接口隔离原则一种体现。
总结
接口隔离原则使用过程中要明确的:
1>客户端不应该依赖它不需要的接口
2>一个类对另一个类的依赖应该建立在最小的接口上
3>按需设计接口,按需实现接口
4>接口隔离原则符合高内聚低耦合的设计思想,从而使得类具有很好的可读性,可扩拓展行和可维护性
5>注意防止过度设计,一个接口要具备一些基本的功能,能独一完成一个基本的任务