接口分离原则(Interface Segregation Principle)

接口分离原则(Interface Segregation Principle)用于处理胖接口(fat interface)所带来的问题。如果类的接口定义暴露了过多的行为,则说明这个类的接口定义内聚程度不够好。换句话说,类的接口可以被分解为多组功能函数的组合,每一组都服务于不同的客户类,而不同的客户类可以选择使用不同的功能分组。

ISP 原则承认了对象设计中非内聚接口的存在。但它建议客户类不应该只通过一个单独的类来使用这些接口。取而代之的是,客户类应该通过不同的抽象基类来使用那些内聚的接口。在不同的编程语言中,这里所指的抽象基类可以指 "接口(interface)"、"协议(protocol)"、"签名(signature)" 等。

本篇文章我们将探讨 "fat" 胖接口或称为 "polluted" 被污染的接口在类设计中所带来的问题。将涉及描述这些接口是如何被创建的,如何改进类的设计以便隐藏它们。最后,我们将通过一个案例来学习 "fat" 接口的产生过程,以及我们如何通过使用 ISP 原则来校正它。

接口污染(Interface Pollution)

假设我们在设计一个安保系统。在这个系统中,Door 对象包含锁定(Lock)和解锁(Unlock)行为,同时也了解门的开关状态(IsDoorOpen)。

1   class Door
2   {
3     public:
4       virtual void Lock() = 0;
5       virtual void Unlock() = 0;
6       virtual bool IsDoorOpen() = 0;
7   };

类 Door 被设计为抽象类,这样客户类可以通过实现它的接口(virtual)来使用,而无需依赖于特定的 Door 实现。

现在假设有一种实现称为 TimedDoor。TimedDoor 需要在门开了一段时间之后发出一个声音报警(Sound Alarm)。为了实现这个功能,TimedDoor 需要与另一个叫做 Timer 的对象进行通信。

 1   class Timer
 2   {
 3     public:
 4       void Regsiter(int timeout, TimerClient* client);
 5   };
 6   class TimerClient
 7   {
 8     public:
 9       virtual void TimeOut() = 0;
10   };

当 Timer 对象检测计时时间已经超时时,它就调用 Register 函数。Register 函数的参数包括超时的时长和一个 TimerClient 对象的指针。TimerClient 类拥有一个 TimeOut 方法,用以当超时发生时被调用。

那么如何让 TimerClient 与 TimedDoor 进行通信,以便当超时发生时 TimedDoor 能够被通知到呢?理论上讲有很多种选择。图 1 中展示了一个常见的方案。

图 1

我们强制让 Door 继承自 TimerClient,然后 TimedDoor 自然也继承自 TimerClient。这让 TimerClient 可以将自己注册到 Timer 对象中以提供 TimeOut 接口。

这个方案很好理解,但是有很多问题。其中最主要的问题是,Door 类直接依赖了 TimerClient。然而,并不是所有的 Door 的衍生类都需要考虑时间和超时问题。而实际上,在我们这么设计之前, Door 类根本不关心任何有关时间的问题。然后,那些不需要使用时间功能的 Door 的衍生类也将不得不为 TimeOut 提供一个实现,即使是空实现。此外,使用这些衍生类的客户类也将不得不引用 TimerClient 的定义,即使它们并没有使用这些功能。

图 1 展示了在面向对象设计中的一个常见的问题,尤其是在类似于 C++ 这样的静态类型语言中。确切的说,这个问题属于接口污染(interface pollution)问题,Door 的接口已经被一个并不需要的接口所污染。而在实际使用中,新增的接口仅是 Door 的一个子类所需要的功能。如果这样演进下去,每当一个衍生类需要使用一个新的接口时,我们就将这个接口添加到它的 Door 基类中,持续的污染使得基类的接口变得越来越臃肿。

而且,每当将一个新的接口添加到基类定义中时,基类的所有其他衍生类也相应实现了该接口。一般来说,新增的接口在基类中已经有了默认实现或空实现,而子类如果不需要使用该功能可以不提供实现。我们知道,这样的设计已经违背了里氏替换原则(Liskov Substitution Principle),降低了代码的可维护性和可复用性。

隔离客户类意味着隔离接口

Door 和 TimerClient 所表示的接口其实是被不同的客户类所使用。Timer 使用 TimerClient,而负责控制门的那些类会使用 Door 。因此它们服务的对象是不同的,所以接口也应当保持隔离。为什么这么说呢?在下面的描述中我们会知道,客户类所以依赖的接口之间是可以相互影响的。

当我们面对新的需求引起软件的变化时,我们通常会考虑这些对接口的更改是否将影响到它们的客户类。例如,我们可能会关心对 TimerClient 的更改是否会影响到它所有的客户类,因为有时就是它的客户类强制让接口发生变化的。

例如,Timer 的客户类可能会调用多次 Register 函数。假设 TimedDoor 侦测到了门已经被打开,则它会调用 Timer 的 Register 方法来注册 TimeOut 回调。如果在超时之前门关上了,关了一会后又重新打开了。这将导致我们又 Register 了一个新的 TimeOut 回调,而之前的那个还没有被触发。然后当超时发生时, TimedDoor 的 TimeOut 函数被调用,使得 Door 发出了一个错误的报警消息。

我们可以通过下述代码中的约定机制来处理上面描述的情况。为 Register 添加一个 timeOutId 参数作为识别 TimerClient 的标识。当回调 TimeOut 时携带这个注册时设置的 timeOutId,使得 TimerClient 的子类知道具体调用的是谁的 TimeOut 函数。

 1   class Timer
 2   {
 3     public:
 4       void Regsiter(int timeout,
 5       int timeOutId,
 6       TimerClient* client);
 7   };
 8   class TimerClient
 9   {
10     public:
11       virtual void TimeOut(int timeOutId) = 0;
12   };

显然,这个改动会影响所有 TimerClient 的用户,而实际上这个问题是一个设计疏漏。然而,如果保持图 1 中的设计,就是为了添加一个 timeOutId,将有可能导致 Door 和它所有的客户类都会被影响到(至少要重新编译吧!)。那为什么为了解决 TimerClient 中的一个 Bug,而将影响到那些不需要使用时间超时机制的 Door 的衍生类呢?当对程序的一个改动触发了完全不相关的模块的改动时,这种代价和变化所带来的后果就变得无法预测,而且这种变化显然也会引入更大的风险。

接口分离原则(The Interface Segregation Principle)

Clients should not be forced to depend upon interfaces that they do not use.

接口分离原则描述为 "客户类不应被强迫依赖那些它们不需要的接口"。

当客户类被强迫依赖那些它们不需要的接口时,则这些客户类不得不受制于这些接口。这无意间就导致了所有客户类之间的耦合。换句话说,如果一个客户类依赖了一个类,这个类包含了客户类不需要的接口,但这些接口是其他客户类所需要的,那么当其他客户类要求修改这个类时,这个修改也将影响这个客户类。通常我们都是在尽可能的避免这种耦合,所以我们需要竭尽全力地分离这些接口。

类接口和对象接口

我们再来看下 TimedDoor 类。这个类的对象包含两个独立的接口,但同时被两个不同的客户类所使用,也就是 Timer 和使用 Door 的用户。这两个接口必须在同一个对象上实现,因为这两个接口的实现需要处理同样的数据。那么我们该如何使其符合 ISP 原则呢?如何将那些必须保持在一起的接口进行分离呢?

解决这个问题的基本方式就是让对象的客户类不通过对象的接口来访问,而是通过委托(delegation)或者基类对象来访问。

通过委托进行分离(Separation through Delegation)

我们可以采用 Adapter 设计模式来解决 TimedDoor 的问题。具体方式是通过从 TimerClient 衍生一个 Adapter 对象,其将操作转递至 TimedDoor 对象。如图 2 所示。

图 2

当 TimedDoor 想要 Register 一个 TimeOut 回调至 Timer 时,先创建一个 DoorTimerAdapter,然后将其Register 给 Timer。当 Timer 调用 DoorTimerAdapter 的 TimeOut 方法时,DoorTimerAdapter 将这个调用转递给 TimedDoor。

这个方案满足了 ISP 原则,并且避免了 Door 的使用者与 Timer 之间的耦合。而且当 Timer 被修改时,Door 的用户将不再会被影响到。此外,TimedDoor 也无须一定非要与 TimerClient 保持相同的接口。DoorTimerAdapter 能够将 TimerClient 接口翻译成 TimedDoor 接口。因此,这是一个非常通用的解决方案。

 1   class TimedDoor : public Door
 2   {
 3     public:
 4       virtual void DoorTimeOut(int timeOutId);
 5   };
 6   class DoorTimerAdapter : public TimerClient
 7   {
 8     public:
 9       DoorTimerAdapter(TimedDoor& theDoor)
10         : itsTimedDoor(theDoor)
11         {}
12       virtual void TimeOut(int timeOutId)
13         {itsTimedDoor.DoorTimeOut(timeOutId);}
14     private:
15       TimedDoor& itsTimedDoor;
16   };

尽管如此,上述方案还是不够优雅。它要求每当我们想注册一个 TimeOut 时都需要创建一个新的对象。创建对象显然多了些开销。

通过多继承进行分离(Separation through Multiple Inheritance)

图 3 中展示了多继承的使用方式。在这种模型下,TimedDoor 同时继承了 Door 和 TimerClient。尽管两个基类的客户类都能够使用 TimedDoor,但它们都没有直接依赖 TimedDoor 类,而是通过独立的接口来使用相同的对象的。

图 3

1   class TimedDoor : public Door, public TimerClient
2   {
3     public:
4       virtual void TimeOut(int timeOutId);
5   };

我个人是比较推荐这个方案的。多继承没有想象的那么恐怖。事实上,我发现它在这种条件下还特别有用。而且,对于上面图 2 中的方案,我只在当 DoorTimerAdapter 中所执行的转换是必要的情况下才会推荐使用。

ATM 用户接口案例

现在我们来看一个较有实际意义的案例,古老的 ATM(Automated Teller Machine)问题。ATM 机的用户接口需要非常灵活的设计,因其界面输出可能需要被翻译成多种不同的语言和呈现方式。比如,它有可能使用显示屏进行展现,也有可能使用盲人点字面板,或者通过语音合成技术进行输出。为了支持这些功能,我们可以通过创建一个抽象基类,包含所有支持的功能接口,并且接口函数设置的 virtual 以供子类扩展。

图 4

ATM 还支持不同的交易(transaction)类型,每种交易类型都衍生自基类 Transaction。这样就可以得到 DepositTransaction、WithdrawTransaction 和 TransferTransaction 等。每种交易对象都会调用 UI 模块进行操作。例如,DepositeTransaction 对象调用 UI 的 RequestDepositAmount 成员函数,而 TransferTransaction 对象则调用 UI 的 RequestTransferAmount 成员函数。如图 5 所示。

图 5

注意到,这种情形恰好是 ISP 原则告诉我们一定要避免的。每种 Transaction 类型都使用了 UI 的一部分功能。这使得对 Transaction 的某一个衍生类的更改将导致相应的 UI 跟着更改。

这种耦合可以通过分离 UI 的接口来避免。我们可以将 UI 的接口分成多个独立的抽象基类接口,例如 DepositUI、WithdrawUI 和 TransferUI 等。然后 UI 类则通过多继承来实现这些抽象基类。如图 6 所示。

图 6

诚然,无论何时创建 Transaction 的一个新的衍生类,都需要相应创建一个抽象的 UI 基类。从而使 UI 及其所有衍生类也必须跟着修改。尽管如此,这些类其实并没有广泛的分布在应用程序中。实际上,它们可能仅在 main 函数或者系统的启动 bootstrap 中才会被实例化。所以,添加新的 UI 基类是可控的。

总结

本篇文章中,我们探讨了关于胖接口 "fat interface" 所带来的问题,也就是接口不是为特定的客户类服务,而服务了多个不同的客户类。胖接口使本应该被隔离的客户类之间产生了耦合。通过应用 Adapter 设计模式,采用委托(delegation)或多继承方式,胖接口可以被分离成多个抽象的基类接口,从而打破客户类之间的不必要的耦合。

面向对象设计的原则

 SRP
单一职责原则


Single Responsibility Principle

 OCP
开放封闭原则


Open Closed Principle

 LSP
里氏替换原则


Liskov Substitution Principle


ISP


接口分离原则


Interface Segregation Principle

 DIP
依赖倒置原则


Dependency Inversion Principle

参考资料

本文《接口原则(Interface Segregation Principle)》由 Dennis Gao 翻译改编自 Robert Martin 的文章《ISP: The Interface Segregation Principle》,未经作者本人同意禁止任何形式的转载,任何自动或人为的爬虫行为均为耍流氓。

接口分离原则(Interface Segregation Principle)

时间: 2024-10-12 17:11:57

接口分离原则(Interface Segregation Principle)的相关文章

接口隔离原则 Interface Segregation Principle

接口隔离原则介绍: 客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上 示例: 错误示例: package com.kittenplus.principle.segregation; public class seregation2 { public static void main(String[] args) { } interface Interface1{ void operation1(); void operation2(); void operation

接口隔离原则(Interface Segregation Principle)ISP

using System; using System.Collections.Generic; using System.Text; namespace InterfaceSegregationPrinciple { //接口隔离原则(Interface Segregation Principle)ISP //Clients should not be forced to depend upon interfaces that they don's use.(客户端不应该依赖它不需要的接口) /

设计模式六大原则(4):接口隔离原则(Interface Segregation Principle)

接口隔离原则: 使用多个专门的接口比使用单一的总接口要好. 一个类对另外一个类的依赖性应当是建立在最小的接口上的. 一个接口代表一个角色,不应当将不同的角色都交给一个接口.没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染. "不应该强迫客户依赖于它们不用的方法.接口属于客户,不属于它所在的类层次结构."这个说得很明白了,再通俗点说,不要强迫客户使用它们不用的方法,如果强迫用户使用它们不使用的方法,那么这些客户就会面临由于这些不使用的方法的改变所带来的改变. 定义:

架构中的设计原则之接口分离原则(ISP) - 《java开发技术-在架构中体验设计模式和算法之美》

接口分离原则 接口分离原则的核心思想是:不应该强迫客户程序依赖它们不需要使用的方法.英文缩写ISP,即Interface Segregation Principle.其实接口分离原则的意思就是:一个接口不需要提供太多的行为,一个接口应该只提供一种对外的功能,不应该 把所有的操作都封装到一个接口中. 这里的"接口"指的不仅仅是通过interface关键字定义的接口,接口分为如下两种. 对象接口.java中声明的一个类,通过new关键字产生的一个实例,它是对一个类型的事物的描述,这也是一种

面向对象设计原则 接口分离原则(Interface Segregation Principle)

接口隔离原则 使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口. 从接口隔离原则的定义可以看出,他似乎跟SRP有许多相似之处. 是的其实ISP和SRP都是强调职责的单一性, 接口隔离原则告诉我们在定义接口的时候要根据职责定义“较小”的接口,不要定义“高大全”的接口. 也就是说接口要尽可能的职责单一,这样更容易复用,暴露给客户端的方法更具有“针对性”. 遵守接口隔离原则,会使代码量增加不少,源码中也是这样. 当方法随着业务需求的增加而不断增加的话,如果我们不应用接口隔

六大设计原则--接口隔离原则【 Interface Segregation Principle】

声明:本文内容是从网络书籍整理而来,并非原创. 定义 第一种定义: Clients 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 Segregation Principle,ISP)

使用多个小的专门的接口,而不要使用一个大的总接口. 具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免"胖"接口.一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染. 接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单一性.而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法.属性等: 而某些时候,实现类型并非需要所有的接口定义,在设计上这是"浪费",而且在实施上这会带来潜在的问题,

ISP(Interface Segregation Principle),接口隔离原则

它要求如下: ①  一个类对另一个类的依赖性要建立在最小接口上. ②  使用多个专门的接口比使用单一的总接口要好. ③  没有关系的接口不可合并成一个臃肿的大接口. ④  对于臃肿的大接口应分成几个合适的小接口. 接口分离方法: 1.使用委托分离接口:把请求委托给别的接口的实现类来完成需要的职责. 2.使用多重继承分离接口:通过实现多个接口来完成需要的职责. 总结: 大接口导致客户程序之间产生不必要的耦合关系,牵一发而动全身.按照接口隔离原则设计接口,把大接口进行分解,使客户程序只依赖它需要的方

4.接口隔离原则(Interface Segregation Principle)

1.定义 客户端不应该依赖它不需要的接口: 一个类对另一个类的依赖应该建立在最小的接口上. 2.定义解读 定义包含三层含义: 一个类对另一个类的依赖应该建立在最小的接口上: 一个接口代表一个角色,不应该将不同的角色都交给一个接口,因为这样可能会形成一个臃肿的大接口: 不应该强迫客户依赖它们从来不用的方法. 接口隔离原则有点像单一职责原则,但是也有区别,在单一职责原则中,一个接口可能有多个方法,提供给多种不同的调用者所调用,但是它们始终完成同一种功能,因此它们符合单一职责原则,却不符合接口隔离原则