【温故】设计原则:接口隔离原则

前言

最近在读《敏捷软件开发 原则、模式与实践》,里面提到了几大设计原则,于是想复习下,并且用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);
    }
}

参考文献

敏捷软件开发 原则、模式与实践

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值