ibankaccount 不该同时包含 deposit 和 withdraw 功能,因其导致接口膨胀:只读账户被迫实现无用的 withdraw 方法并抛 notsupportedexception,违反 isp 且引发运行时风险;应拆分为 idepositable、iwithdrawable、ibalancequeryable 等细粒度能力接口。

为什么 IBankAccount 不该同时包含 Deposit 和 IWithdrawalProcessor 功能
接口膨胀的典型表现:一个接口被迫让不相关的类实现用不到的方法。比如让只读账户(如查询专用账号)也必须实现 Withdraw,只能抛出 NotSupportedException —— 这违反了 ISP,也埋下运行时错误隐患。
关键判断标准:如果某个实现类有超过一个方法是“空实现”或“抛异常”,这个接口大概率该拆。
- 把存、取、查询拆成三个独立接口:
IDepositable、IWithdrawable、IBalanceQueryable - 组合使用时用多重继承(C# 支持一个类实现多个接口),而非强塞进单个大接口
- 避免用 “
IAccount” 这类宽泛命名;接口名应体现能力(IAuthenticatable)而非实体(IAccount)
C# 中如何识别「被迫实现」的接口方法
观察实现类中的方法体:只要出现 throw new NotSupportedException() 或 return default; 且该行为在业务逻辑中确实不该发生,就说明接口契约过宽。
常见信号:
-
void ProcessPayment()在日志服务类里被实现为空方法 -
IEnumerable<t> Search(string keyword)</t>在只支持 ID 查找的仓储中返回空集合 - 测试中频繁 mock 掉某几个方法,而只验证另外一两个 —— 那些被 mock 的很可能是冗余契约
用 interface 组合替代「大而全」接口的实操方式
不要试图设计一个终极 ICustomerService,而是按角色/场景切分:
- 面向前端 API:定义
ICustomerReadService(只含GetById、Search) - 面向支付流程:定义
ICustomerPaymentValidator(只含IsEligibleForRefund) - 依赖注入时按需注入,例如控制器构造函数参数写
ICustomerReadService readSvc, ICustomerPaymentValidator validator,而不是一个巨无霸ICustomerService
这样不仅满足 ISP,还天然支持单元测试隔离和未来替换(比如用缓存版 ICustomerReadService 替换 DB 版,完全不影响支付校验逻辑)。
注意 C# 8+ 默认接口方法对 ISP 的干扰
默认接口方法(void Log() => Console.WriteLine("default");)看似能缓解实现压力,但容易掩盖设计问题:它让本该拆分的职责继续挤在一个接口里。
除非是真正通用、无副作用、可安全被所有实现共享的行为(比如统一的日志前缀格式化),否则别用默认实现来“凑合”。ISP 关注的是契约清晰度,不是实现便利性。
尤其警惕:当某个默认方法内部调用了抽象方法(如 Log() 调用 GetLogContext()),这其实已经形成了隐式依赖,反而增加了子类的理解成本。









