接口隔离原则(The Interface-Segregation Principle)强调类的功能要单一,类的功能臃肿增加不必要的耦合,增加代码的脆弱性,还会增加编译依赖。该原则建议将方法分组,达到隔离接口的目的,具体的方法有委托和多重继承。
在Timed Door的例子中,开始的实现是Timed Door继承Door,Door实现Timer Client这个接口,这样Timed Door就可以通过Timer Client这个接口去使用Timer的功能。但这样做会导致Door依赖于Timer Client,而且并不是所有的Door都需要定时功能。所有的继承于Door的子类都需要提供退化的Timeout方法,违反了LSP。
注意,这里的TimerClient是Timer实现的需求,与Door无关。所有使用Timer的类对相当于Timer的client,需要实现TimerClient这个接口,用于Timer time out时调用。
Timer Client at Top of Hierarchy
Door和Timer Client中的接口分别由不同的client使用,既然使用者相互独立,接口也应该独立,因为使用者也会对接口产生影响。我们一般都会考虑接口对使用者的影响,但使用者也会对接口提出更多的要求。比如文中由于door的需求,需要Timer提供带timeOutId的注册接口。但这个接口的变化会导致Door的所有子类代码都需要修改,这个设计的确很糟糕。
接口隔离原则:使用者不应该被逼去依赖它们不使用的方法。
Clients should not be forced to depend on methods that they do not use.
通过委托或多重继承可以避免依赖不使用的方法。一般使用多重继承,因为委托会产生多余的性能和空间的耗用。这里的委托就是说将对TimerClient的实现委托给TimerClientAdapter去做。
TimedDoor例子中通过将定时接口和Door接口隔离,避免了其它Door子类依赖于定时接口。
Multiply inherited Timed Door
Door Timer Adapter
class="code_img_closed" src="/Upload/Images/2013121809/0015B68B3C38AA5B.gif" alt="" />logs_code_hide('ed93d3a8-415d-40d0-bbec-5ed8005be704',event)" src="/Upload/Images/2013121809/2B1B950FA3DF188F.gif" alt="" />1 public interface TimerClient { 2 public void timeOut(); 3 } 4 5 public class Timer2 extends TimerTask{ 6 7 private TimerClient mTc; 8 private Timer mTimer; 9 10 public void registry(int timeout, TimerClient tc) { 11 mTc = tc; 12 mTimer = new Timer(); 13 mTimer.schedule(this, timeout*1000); 14 } 15 16 @Override 17 public void run() { 18 // TODO Auto-generated method stub 19 mTc.timeOut(); 20 } 21 }Timer以及接口
1 public class TimedDoor2 extends Door implements TimerClient { 2 3 public TimedDoor2(int second) { 4 super(); 5 Timer2 t = new Timer2(); 6 t.registry(second, this); 7 } 8 9 public void doorTimeOut () { 10 if (!mIsLock) { 11 lock(); 12 } 13 } 14 15 @Override 16 public void timeOut() { 17 // TODO Auto-generated method stub 18 doorTimeOut(); 19 } 20 21 }TimerDoor多重继承实现
1 public class DoorTimerAdapter implements TimerClient { 2 private TimedDoor mTimedDoor; 3 4 public DoorTimerAdapter(TimedDoor td) { 5 mTimedDoor = td; 6 } 7 8 @Override 9 public void timeOut() { 10 // TODO Auto-generated method stub 11 mTimedDoor.doorTimeOut(); 12 } 13 14 } 15 16 public class TimedDoor extends Door { 17 18 public TimedDoor() { 19 super(); 20 Timer2 t = new Timer2(); 21 t.registry(5, new DoorTimerAdapter(this)); 22 } 23 24 public void doorTimeOut () { 25 if (!mIsLock) { 26 lock(); 27 } 28 } 29 30 } 31 32 public class Timer2 extends TimerTask{ 33 34 private TimerClient mTc; 35 private Timer mTimer; 36 37 public void registry(int timeout, TimerClient tc) { 38 mTc = tc; 39 mTimer = new Timer(); 40 mTimer.schedule(this, timeout*1000); 41 } 42 43 @Override 44 public void run() { 45 // TODO Auto-generated method stub 46 mTc.timeOut(); 47 } 48 } 49 50 public interface TimerClient { 51 public void timeOut(); 52 }TimerDoor委托实现
在这里说一下对接口的理解。之前对接口的使用一直局限在多态的使用上,即定义一个接口,加上若干实现,所有定义对象的地方可以使用接口定义,运行时创建不同的对象。因为这个局限,导致自己一直无法理解抽象类和接口到底有什么区别,觉得抽象类和接口是可以互换的。
仔细想想,接口定义的是行为。一个类实现一个接口,说明它具备这些行为能力,比如TimedDoor实现Timer Client接口后,说明它具备定时功能。一个类可以具备很多行为能力,还有多个类会具备相同的行为能力,比如你也可以开发一个实现Timer Client接口的TimedCar类,它也具备定时功能。在这里,就需要使用接口来将各种各样的行为进行分类,类需要什么功能,就实现什么样的接口。
抽象类中的定义的方法也可以称为行为,它与接口定义的行为有什么区别呢?抽象类是对类的抽象,接口是对行为的抽象。每个类都会有自己的一些基本行为,它是在我们对现实世界进行抽象时得到的,比如Door肯定具备open、close行为。同时各个类之间又会具备一些不同的行为,我们不能保证,不可能所有类具备同样的行为,这时就需要接口来作为补充。刚开始往往认为代码中有抽象类和接口才是面向对象编程,这是错误的。滥用抽象类和接口会增加程序的复杂度,我们应该只在需要时才进行重构使用抽象类和接口。
TimedDoor例子中是该类调用的接口需要隔离,即Door需要实现的功能太多了,非TimedDoor没必要实现TimerClient;ATM机的例子中则是UI中被Trasaction调用的接口需要分类隔离,即UI提供的功能太多了。
ATM需要在多种终端上实现不同类型的UI,比如盲文、触摸屏、按键等,这就要求UI的实现需要非常flexible。相反,ATM后台事务逻辑很固定,几乎不会变化。因此就把UI对象传递给事务,让事务去请求UI做事情,比如要求用户输入等。以前,我们一般都会认为UI去调用事务逻辑,这里刚好相反,很巧妙!
但之前将所有的UI行为(存钱UI、取钱UI等)放在一个接口中,任何一个Transaction发生变化时,如果影响了UI接口,那么就会将这种变化传递给其它Transaction,因为它们都依赖于UI。最典型的就是当增加一个Transaction时,UI中必须增加一个接口,这时就会导致所有Transaction重新编译。
ATM Transaction Hierarchy
Segregated ATM UI Interface
最后,当接口发生变化时,为了防止由此导致的大规模重新编译和重新部署,可取的方法是新增接口,而不是修改原有的接口。