目录
前言
最近在读《敏捷软件开发 原则、模式与实践》,里面提到了几大设计原则,于是想复习下,并且用C#把文中代码重新写一遍
接口隔离原则(ISP)
概念
不应该强迫客户端依赖它们不用的方法
一个简单的例子
想象有一个安全系统。在这个系统中,有一些Door对象,可以被加锁和解锁,并且Door对象知道自已是开着还是关着。
public class Door
{
/// <summary>锁门</summary>
public void Lock(){}
/// <summary>解锁</summary>
public void Unlock(){}
/// <summary>门是否开着</summary>
public bool IsDoorOpen(){}
}
现在,想象一个这样的实现,TimedDoor,如果门开着的时间过长,它就会发出警报声。为了做到这点,TimedDoor对象需要和另一个名为Timer的对象交互
/// <summary>
/// 定时器类 - 管理超时通知
/// </summary>
public class Timer
{
/// <summary>
/// 注册超时通知
/// </summary>
/// <param name="timeout">超时时间(毫秒)</param>
/// <param name="client">接收通知的客户端</param>
public void Register(int timeout, ITimerClient client){}
}
/// <summary>
/// 定时器客户端接口 - 需要超时通知的对象实现此接口
/// </summary>
public interface ITimerClient
{
/// <summary>超时回调方法</summary>
void TimeOut();
}
那么TimedDoor怎么实现呢,如果你想的是下面这种,那么就违反了接口隔离原则和里氏替换原则。(里氏替换原则详见我之前的文章)
public class Door : ITimerClient
{
// 其他方法
public void TimeOut() { /* 做些什么 */ }
}
public class TimedDoor : Door
{
// 其他方法
public void TimeOut() { /* 做些什么*/ }
}
/// <summary>
/// 普通门 - 不需要定时功能
/// 如果Door继承了ITimerClient,这里就必须实现TimeOut(退化实现)
/// </summary>
public class RegularDoor : Door
{
// 其他方法
// 被迫实现的退化方法 - 这就是接口污染!
public void TimeOut() { /* 什么都不做 */ }
}
解决方案一、使用多重继承分离接口
简单明了,大多数情况都应该这样设计
public class TimedDoor : Door, ITimerClient
{
// 其他方法
public void TimeOut() { /* 做些什么*/ }
}
public class Door
{
// 原封不动
}
解决方案二、使用委托(适配器模式)分离接口
这种方式比较绕,如果实在无法用方案一的情况下,再考虑。
/// <summary>
/// 定时门接口 - 定义定时门特有的行为
/// </summary>
public interface ITimedDoor
{
/// <summary>门超时处理方法</summary>
void DoorTimeOut();
}
/// <summary>
/// 定时门 - 继承Door,实现ITimedDoor(不是ITimerClient)
/// </summary>
public class TimedDoor : Door, ITimedDoor
{
private DoorTimerAdapter _adapter;
public TimedDoorWithAdapter(TimerV2 timer)
{
_timer = timer;
_adapter = new DoorTimerAdapter(this); // 创建适配器
}
public override void Unlock()
{
// 其他代码
// 通过适配器注册,TimedDoor本身不直接实现ITimerClientV2
_timer.Register(5000, _adapter);
}
}
/// <summary>
/// 门定时器适配器 - 承担ITimerClientV2的角色
/// 将Timer的调用转发给TimedDoor
/// </summary>
public class DoorTimerAdapter : ITimerClient
{
private readonly ITimedDoor _timedDoor;
public DoorTimerAdapter(ITimedDoor timedDoor)
{
_timedDoor = timedDoor;
}
// ITimerClientV2实现 - 将调用委托给真正的TimedDoor
public void TimeOut()
{
_timedDoor.DoorTimeOut();
}
}
ATM用户界面的例子
想象一个ATM的系统,ATM需要支持各种不同的交易,不同交易需要处理不同的UI。现在有个Transaction基类,我们至少需要存款,取款,转账操作。还有一个UI类,有以下方法。
void RequestDepositAmount(); // 请求存款UI,只有DepositTransaction使用
void RequestWithdrawalAmount(); // 请求取款UI,只有WithdrawalTransaction使用
void InformInsufficientFunds(); // 查询余额,只有WithdrawalTransaction使用
void RequestTransferAmount(); // 请求转账UI,只有TransferTransaction使用
如果这些方法都在同一个接口中,就违反了ISP。现在要设计存款,取款,转账类,如何设计能够避免呢。
遵循ISP的设计
直接看原文的设计
/// 存款UI接口 - 只包含存款相关操作
public interface IDepositUI
{
void RequestDepositAmount();
}
/// 取款UI接口
public interface IWithdrawalUI
{
void RequestWithdrawalAmount();
void InformInsufficientFunds();
}
/// 转账UI接口
public interface ITransferUI
{
void RequestTransferAmount();
}
/// 存款交易 - 只依赖IDepositUI
public class DepositTransaction : Transaction
{
private readonly IDepositUI _depositUI;
public DepositTransaction(IDepositUI depositUI)
{
_depositUI = depositUI;
}
public override void Execute()
{
Console.WriteLine("开始存款交易...");
_depositUI.RequestDepositAmount();
// 处理存款...
}
}
/// 取款交易 - 只依赖IWithdrawalUI
public class WithdrawalTransaction : Transaction
{
private readonly IWithdrawalUI _withdrawalUI;
public WithdrawalTransaction(IWithdrawalUI withdrawalUI)
{
_withdrawalUI = withdrawalUI;
}
public override void Execute()
{
Console.WriteLine("开始取款交易...");
_withdrawalUI.RequestWithdrawalAmount();
// 检查余额...
_withdrawalUI.InformInsufficientFunds();
}
}
/// 转账交易 - 只依赖ITransferUI
public class TransferTransaction : Transaction
{
private readonly ITransferUI _transferUI;
public TransferTransaction(ITransferUI transferUI)
{
_transferUI = transferUI;
}
public override void Execute()
{
Console.WriteLine("开始转账交易...");
_transferUI.RequestTransferAmount();
}
}
/// <summary>
/// 完整的UI类 - 实现所有分离的UI接口
/// 只有这个类和main函数需要知道所有接口的存在
/// </summary>
public class CompleteUI : IDepositUI, IWithdrawalUI, ITransferUI
{
public void RequestDepositAmount()
{
Console.WriteLine("UI: 请输入存款金额");
}
public void RequestWithdrawalAmount()
{
Console.WriteLine("UI: 请输入取款金额");
}
public void InformInsufficientFunds()
{
Console.WriteLine("UI: 余额不足!");
}
public void RequestTransferAmount()
{
Console.WriteLine("UI: 请输入转账金额");
}
}
多参数形式 vs 单参数形式
外部使用的时候更应该用多参数模式。
即使两个参数是同一个对象,也明确声明函数依赖哪些接口。
/// <summary>
/// 多参数形式 - 推荐做法
/// 函数明确声明它依赖哪些接口
/// </summary>
public class MultiParameterExample
{
// 明确声明需要DepositUI和TransferUI
public void ProcessTransaction(IDepositUI depositUI, ITransferUI transferUI)
{
depositUI.RequestDepositAmount();
transferUI.RequestTransferAmount();
}
}
/// <summary>
/// 单参数形式 - 不推荐
/// 强迫函数依赖于UI的所有部分
/// </summary>
public class SingleParameterExample
{
// 虽然只需要Deposit和Transfer功能,但依赖于整个UI
public void ProcessTransaction(CompleteUI ui)
{
ui.RequestDepositAmount();
ui.RequestTransferAmount();
}
}
// 使用对比
public class ParameterDemo
{
public void Demo()
{
var ui = new CompleteUI();
// 多参数形式 - 清晰,解耦
var multi = new MultiParameterExample();
multi.ProcessTransaction(ui, ui); // 传入同一个对象的不同接口视图
// 单参数形式 - 隐含依赖整个UI接口
var single = new SingleParameterExample();
single.ProcessTransaction(ui);
}
}
参考文献
敏捷软件开发 原则、模式与实践

4627

被折叠的 条评论
为什么被折叠?



