敏捷软件开发:原则、模式与实践——第12章 ISP:接口隔离原则
第12章 ISP:接口隔离原则
不应该强迫客户程序依赖并未使用的方法。
这个原则用来处理“胖”接口所存在的缺点。如果类的接口不是内敛的,就表示该类具有“胖”接口。换句话说,类的“胖”接口可以分解成多组方法。每一组方法都服务于一组不同的客户程序。这样,一些客户程序可以使用一组成员函数,而其他客户程序可以使用其他组的成员函数。
ISP承认一些对象确实需要非内敛的接口,但是ISP建议客户不应该看到它们作为单一的类存在。相反,客户程序看到的应该是多个具有内敛接口的抽象基类。
12.1 接口污染
如果子类的接口中有子类不需要的方法,就说这个接口被污染了。在接口中加入方法,只为了能给它的其中一个子类带来好处。如果持续这样的话,那么每次子类需要一个新方法时,这个方法就会加到基类中去。这会进一步污染其基类的接口,使它变“胖”。
每次基类中加入一个方法时,派生类中就必须实现这个方法(或者定义一个默认的实现)。事实上,一种特定的相关实践,可以使派生类无需实现这些方法,该实践的方法就是把这些接口合并成一个基类,并在这个基类中提供接口中方法的退化实现。但是,这种实践违反了LSP,会带来维护和重用方面的问题。
12.2 分离客户就是分离接口
不应该强迫客户程序依赖并未使用的方法。如果强迫客户依赖于它们不使用的方法,那么这些客户程序就面临着由于这些未使用方法的改变带来的变更。这无意中导致了所有客户程序之间的耦合。换种说法,如果一个客户程序依赖于一个含有它不使用的方法的类,但是其它客户程序却要使用该方法,那么当其他客户程序要求这个类改变时,就会影响到这个客户程序。我们希望尽可能的避免这种耦合,因此我们希望分类接口。
12.3 类接口与对象接口
一个对象的客户程序不必通过该对象的接口来访问它,也可以通过委托或者通过该对象的基类来访问它。
12.3.1 使用委托分离接口
这里的“委托”不是我们说的“委托类型”的委托,原文翻译的容易产生歧义。其实就是用适配器来解耦客户程序和接口。
不过,这个解决方案还有些不太优雅。每次想要注册一个请求,都要去创建一个新的对象。
12.3.2 使用多重继承分离接口
通常我会优先选择这个解决方案。只有当Adapter对象所做的转换是必须的,或者不同的时候需要不同的转换时,才使用适配器的方案。
12.4 ATM用户界面的例子
ATM(自动取款机)需要一个非常灵活的界面。它的输出信息需要转换成不同的语言。输出信息可能显示在屏幕上,或者盲文书写板,或者通过语音合成器说出来。通过创建一个抽象类就可以实现这种需求:

同样可以把每个ATM执行的不同事务(存储、取款、转账)封装为类Transaction的派生类:

注意,这正好是LSP告我我们应该避免的情形。每个事务所使用的UI的方法,其他操作类都不会使用,这样对于任何一个Transaction的派生类的改动都会迫使对UI的相应改动,从而也影响到了其他所有Transaction的派生类及其他所有依赖于UI接口的类。这样的设计就有了僵化性和脆弱性。
这时需要分类ATM的UI接口:

这样,每次创建一个Transaction类的新派生类,抽象接口UI就需要增加一个相应的基类,并且因此UI接口以及所有它的派生类都必须改变。不过,这些类并没有被广泛使用。事实上,它们可能仅被main或者那些启动系统并创建具体UI实例之类的过程使用。因此,增加新的UI基类所带来的影响被减至最小。
查看如下代码:
public interface Transaction
{
void Execute();
}
public interface DepositUI
{
void RequestDepositAmount();
}
public class DepositTransaction : Transaction
{
privateDepositUI depositUI;
public DepositTransaction(DepositUI ui)
{
depositUI = ui;
}
public virtual void Execute()
{
/*code*/
depositUI.RequestDepositAmount();
/*code*/
}
}
public interface WithdrawalUI
{
void RequestWithdrawalAmount();
}
public class WithdrawalTransaction : Transaction
{
private WithdrawalUI withdrawalUI;
public WithdrawalTransaction(WithdrawalUI ui)
{
withdrawalUI = ui;
}
public virtual void Execute()
{
/*code*/
withdrawalUI.RequestWithdrawalAmount();
/*code*/
}
}
public interface TransferUI
{
void RequestTransferAmount();
}
public class TransferTransaction : Transaction
{
private TransferUI transferUI;
public TransferTransaction(TransferUI ui)
{
transferUI = ui;
}
public virtual void Execute()
{
/*code*/
transferUI.RequestTransferAmount();
/*code*/
}
}
public interface UI : DepositUI, WithdrawalUI, TransferUI
{
}
每个事务都必须以某种方式知晓它的特定UI版本。使每个事务在构造时给它传入特定于它的UI的引用就解决了这个问题。这使我们可以使用如下代码:
UI Gui; // global object;
void f()
{
DepositTransaction dt = new DepositTransaction(Gui);
}
虽然这很方便,但是同样要求每个事务都有一个整型对应UI的引用成员。在C#中,一种比较有诱惑力的做法是把所有UI组件放到一个单一的类中,如下:
public class UIGlobals
{
public static WithdrawalUI withdrawal;
public static DepositUI deposit;
public static TransferUI transfer;
static UIGlobals()
{
UI Lui = new AtmUI(); // Some UI implementation
UIGlobals.deposit = Lui;
UIGlobals.withdrawal = Lui;
UIGlobals.transfer = Lui;
}
}
不过,这种做法有一个负面效果。那就是UIGlobal类依赖于DepositUI、WithdrawalUI和TransferUI。UIGlobal类把我们分离的接口重新结合在一起了。
每个原则在应用时都必须小心,不能过度使用它们。如果一个类具有数百个不同接口,其中一些是根据客户程序分离的,另一些是根据版本分离的,那么该类就是难以琢磨的,这种难以琢磨性是非常令人恐惧的。
12.5 结论
胖类会导致它们的客户程序之间产生不正常的并且有害的耦合关系。当一个客户程序要求改胖类进行一个改动时,会影响到所有其他的客户程序。因此,客户程序应该仅仅依赖于它们实际调用的方法。通过把胖类的接口分解为多个特定于用户程序的接口,可以实现这个目标。每个特定于客户程序的接口仅仅声明它的特定客户或者客户组调用的那些函数。接着,该胖类就可以继承所有特定于客户程序的接口,并实现它们。这就解除了客户程序和它们没有调用的方法间的依赖关系,并使客户程序之间互不依赖。
摘自:《敏捷软件开发:原则、模式与实践(C#版)》Robert C.Martin Micah Martin 著
转载请注明出处:
作者:JesseLZJ
出处:http://jesselzj.cnblogs.com
敏捷软件开发:原则、模式与实践——第12章 ISP:接口隔离原则的更多相关文章
- 设计模式原则(4)--Interface Segregation Principle(ISP)--接口隔离原则
作者QQ:1095737364 QQ群:123300273 欢迎加入! 1.定义: 使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口. 2.使用场景: 类A ...
- 八、ISP 接口隔离原则
ISP应用的场景是某些类不符合SRP原则,但使用这些类的客户端应该根据它们的父类来使用(我感觉这句话应该改为:客户端应该根据它们的抽象类\接口来使用它们),而不是直接使用它们. 定义: 客户端不应该依 ...
- 六大设计原则(四)ISP接口隔离原则(上)
ISP的定义 首先明确接口定义 实例接口 我们在Java中,一个类用New关键字来创建一个实例.抛开Java语言我们其实也可以称为接口.假设Person zhangsan = new Person() ...
- ISP接口隔离原则
一.定义 不应该强迫客户程序依赖并未使用的方法 二.接口污染 接口污染,在C#.C++这样的静态类型语言中是很常见的.一个接口会被他不需要的方法污染.在接口中假如一个方法只是为了能给它的一个子类带来好 ...
- 【Scrum】-NO.40.EBook.1.Scrum.1.001-【敏捷软件开发:原则、模式与实践】- Scrum
1.0.0 Summary Tittle:[Scrum]-NO.40.EBook.1.Scrum.1.001-[敏捷软件开发:原则.模式与实践]- Scrum Style:DesignPattern ...
- 《敏捷软件开发-原则、方法与实践》-Robert C. Martin读书笔记(转)
Review of Agile Software Development: Principles, Patterns, and Practices 本书主要包含4部分内容,这些内容对于今天的软件工程师 ...
- 敏捷软件开发_实例2<四>
敏捷软件开发_实例2 上一章中对薪水支付案例的用例和类做了详细的阐述,在本篇会介绍薪水支付案例包的划分和数据库,UI的设计. 包的划分 一个错误包的划分 为什么这个包是错误的: 如果对classifi ...
- C#软件设计——小话设计模式原则之:接口隔离原则ISP
前言:有朋友问我,设计模式原则这些东西在园子里都讨论烂了,一搜一大把的资料,还花这么大力气去整这个干嘛.博主不得不承认,园子里确实很多这方面的文章,并且不乏出色的博文.博主的想法是,既然要完善知识体系 ...
- 第2章 面向对象的设计原则(SOLID):4_接口隔离原则(ISP)
4. 接口隔离原则(Interface Segregation Principle,ISP) 4.1 定义 (1)使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口.类间的 ...
随机推荐
- NativeScript工作原理
NativeScript是一个runtime,它提供一些机制可以使用JavaScript构建原生的IOS.Android甚至WP(未来会加入)应用.NativeScript有很多非常酷的功能,比如MV ...
- 【leetcode】3 SUM
3 SUM 原题: Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0? F ...
- [linux]删除目录下的一类文件
find 目录 -name "*.类型" | xargs rm -f 通过find命令,查找指定目录下的某一类型的文件.并通过管道传递给xargs,执行后面的rm -f命令. 最终 ...
- Linux - Ubuntu下JDK配置
系统版本: ubuntu 14.04 x64JDK版本: jdk-8u60-linux-x64 1.查看系统位数,输入以下命令即可 getconf LONG_BIT 2.下载对应的JDK文件,我这里下 ...
- 判断s2是否能够被通过s1做循环移位(rotate)得到的字符串是否包含
问题:给定两个字符串s1和s2,要求判断s2是否能够被通过s1做循环移位(rotate)得到的字符串包含.例如,S1=AABCD和s2=CDAA,返回true:给定s1=ABCD和s2=ACBD,返回 ...
- Fluent Nhibernate and Stored Procedures
sql:存储过程 DROP TABLE Department GO CREATE TABLE Department ( Id INT IDENTITY(1,1) PRIMARY KEY, DepNam ...
- How to create water Ripple effect using HTML5 canvas
https://www.script-tutorials.com/how-to-create-water-drops-effect-using-html5-canvas/ https://www.sc ...
- 机器学习实战 - 读书笔记(12) - 使用FP-growth算法来高效发现频繁项集
前言 最近在看Peter Harrington写的"机器学习实战",这是我的学习心得,这次是第12章 - 使用FP-growth算法来高效发现频繁项集. 基本概念 FP-growt ...
- phpcms—— 内容中的附件调用和添加远程地址的调用
phpcms中几个地址调用的方法 1,CSS路径有{CSS_PATH}2,图片路径有{IMG_PATH}3,JS路径有{JS_PATH} 4,那么附件的路径如何调用,使用下面的方式可以得到附件的路径前 ...
- mysql 5.6.33发布
2016-09-06,mysql 5.6.33社区版发布,修复的bug越发减少,而且基本上都是较少使用的特性.