您的位置:首页 > 其它

设计模式六大原则之--接口隔离原则(ISP)

2014-04-18 11:24 337 查看
1.接口隔离原则:(Interface Segregation Principle, ISP)

定义:Clients should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口)。或

The dependcy of one class to another one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上)。或

使用多个专门的接口比使用单一的总接口要好。

2.理解:

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。 在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

单一职责与接口隔离的区别:

单一职责原则注重的是职责;而接口隔离原则注重对接口依赖的隔离。
单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节; 而接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建。

3.问题由来:
类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类C来说不是最小接口,则类B和类D必须去实现它们不需要的方法。[解决方案]将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

4.使用ISP的好处:

原则意义上的好处:接口如果能够保持粒度够小,就能保证它足够稳定,正如单一职责原则所飘洋过海榜的那样。(举例:多个专门的接口就好比采用活字制版,可以随时拼版拆版,既利于修改,又利于文字的重用。而单一的总接口就是雕版的印刷,一旦发现错别字,既难改,又需要整块重新雕刻。)

使用多个专门的接口还能够体现对象的层次,因为我们可以通过接口的继承,实现对总接口的定义。(例如,.NET框架中IList接口的定义。)
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface ICollection : IEnumerable
{
void CopyTo(Array array, int index);

// 其余成员略
}
public interface IList : ICollection, IEnumerable
{
int Add(object value);
void Clear();
bool Contains(object value);
int IndexOf(object value);
void Insert(int index, object value);
void Remove(object value);
void RemoveAt(int index);

// 其余成员略
}


如果不采用这样的接口继承方式,而是定义一个总的接口包含上述成员,就无法实现IEnumerable接口、ICollection接口与IList接口成员之间的隔离。假如这个总接口名为IGeneralList,它抹平了IEnumerable接口、ICollection接口与IList接口之间的差别,包含了它们的所有方法。现在,如果我们需要定义一个Hashtable类。根据数据结构的特性,它将无法实现IGeneralList接口。因为Hashtable包含的Add()方法,需要提供键与值,而之前针对ArrayList的Add()方法,则只需要值即可。这意味着两者的接口存在差异。我们需要专门为Hashtable定义一个接口,例如IDictionary,但它却与IGeneralList接口不存在任何关系。正是因为一个总接口的引入,使得我们在可枚举与集合层面上丢失了共同的抽象意义。虽然Hashtable与ArrayList都是可枚举的,也都具备集合特征,它们却不可互换。

如果遵循接口隔离原则,将各自的集合操作功能分解为不同的接口,那么站在ICollection以及IEnumerable的抽象层面上,可以认为ArrayList和Hashtable是相同的对象。在这一抽象层面上,二者是可替换的,如图2-9所示。这样的设计保证了一定程度的重用性与可扩展性。从某种程度来讲,接口隔离原则可以看做是接口层的单一职责原则。



倘若一个类实现了所有的专门接口,从实现上看,它与实现一个总接口的方式并无区别;但站在调用者的角度,不同的接口代表了不同的关注点、不同的职责,甚至是不同的角色。因此,面对需求不同的调用者,这样的类就可以提供一个对应的细粒度接口去匹配。此外,一个庞大的接口不利于我们对其进行测试,因为在为该接口实现Mock或Fake对象 时,需要实现太多的方法。

概括地讲,面向对象设计原则仍然是面向对象思想的体现。例如,单一职责原则与接口隔离原则体现了封装的思想,开放封闭原则体现了对象的封装与多态,而Liskov替换原则是对对象继承的规范,至于依赖倒置原则,则是多态与抽象思想的体现。在充分理解面向对象思想的基础上,掌握基本的设计原则,并能够在项目设计中灵活运用这些原则,就能够改善我们的设计,尤其能够保证可重用性、可维护性与可扩展性等系统的质量属性。这些核心要素与设计原则,就是我们设计的对象法则,它们是理解和掌握设计模式的必备知识。

5.难点:

接口要尽量小(核心定义),但“小”也有限,首先不能违反单一职责原则(接口定义出来是让类来实现的嘛,倘若如此,实现类怎么来SRP?)(去看SRP的7.2节);

接口要高内聚(高内聚:提高接口、类、模块的处理能力,减少对外的交互。例如,不讲任何条件、立刻完成任务的行为就是高内聚的表现),具体到接口隔离原则 ,就是要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本;

定制服务,一个系统或系统内的模块之间必然会有耦合,有耦合就要有相互访问的接口,在设计时,就需要为各个访问者定制服务(定制服务就是单独为一个个体提供优良的服务:只提供访问者需要的方法),本质也是ISP,按需拆分接口;

接口设计是有限度的,但无固化标准。

6.最佳实践:

一个接口只服务于一个子模块或业务逻辑;
通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“筋骨”,而不是“肥嘟嘟”的一大堆方法;
已经被 污染的接口,要尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理;
了解环境,拒绝盲从。

7.范例:
7.1 一个反例(接口臃肿),大意来自3.问题



图1 未遵循ISP的设计图
这个图的意思是:类A依赖接口I中的方法1,2,3; 类C依赖接口I中的方法1,4,5; 类B与类D分别是对类A与类C依赖的实现。 对于类B与类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也 必须要实现这些用不到的方法。代码如下:
interface I {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
class A{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method2();
}

public void depend3(I i){
i.method3();
}
}

class B implements I{
public void method1() {
System.out.println("类B实现接口I的方法1");
}
public void method2() {
System.out.println("类B实现接口I的方法2");
}
public void method3() {
System.out.println("类B实现接口I的方法3");
}
//对于类A来说,method4和method5不是必须的,但是由于接口A中有这两个方法,
//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
public void method4() {}
public void method5() {}
}

class C{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method4();
}
public void depend3(I i){
i.method5();
}
}

class D implements I{
public void method1() {
System.out.println("类D实现接口I的方法1");
}
//对于类C来说,method4和method5不是必须的,但是由于接口A中有这两个方法,
//所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
public void method2() {}
public void method3() {}

public void method4() {
System.out.println("类D实现接口I的方法4");
}
public void method5() {
System.out.println("类D实现接口I的方法5");
}
}

public class Client{
public static void main(String[] args){
A a = new A();
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D());
c.depend2(new D());
c.depend3(new D());
}
}
可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它们的类有没有用处,实现类中都必须去实现这些方法,这显然是不好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进拆分。在这里我们将原有的接口I拆分为三个接口,拆分后的设计如下所示:



图2 一个遵循ISP的设计图
对应的设计代码如下:
interface I1 {
public void method1();
}
interface I2 {
public void method2();
public void method3();
}
interface I3 {
public void method4();
public void method5();
}

class A{
public void depend1(I1 i){
i.method1();
}
public void depend2(I2 i){
i.method2();
}
public void depend3(I2 i){
i.method3();
}
}
class B implements I1, I2{
public void method1() {
System.out.println("类B实现接口I1中的方法1");
}
public void method2() {
System.out.println("类B实现接口I2中的方法2");
}
public void method3() {
System.out.println("类B实现接口I2中的方法3");
}
}
class C{
public void depend1(I1 i){
i.method1();
}
public void depend2(I3 i){
i.method4();
}
public void depend3(I3 i){
i.method5();
}
}
class D implements I1, I3{
public void method1() {
System.out.println("类D实现接口I1中的方法1");
}
public void method4() {
System.out.println("类D实现接口I3中的方法4");
}
public void method5() {
System.out.println("类D实现接口I3中的方法5");
}
}


7.2 一个在需求变化中,才发现接口粒度过大的例子:
星控找美女的过程:



图3 初步的星探找美女图类,美女必须 长得好看、身材好、有气质

但是随着人们审美水品的不断提升,人们对气质美女也产生了很大的认同感,即不太要求长相与身材,这时,新的类图如下:(实为7.1节的演化版)



图4 新类图,如果一开始能做到此,便能防患于未然
转自:/article/2368987.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: