Loading... > 接口隔离原则的英文翻译是“ Interface Segregation Principle”,缩写为 ISP。 LSP 则教导我们应该设计好类的继承关系。 如果没有设计特定的接口,你的一个个具体类就变成它的接口。 不应该强迫客户程序依赖它们不用的方法。接口应该小而完备。不要把不必要的方法public出去 接口隔离原则接口隔离原则(Interface segregation principle,简称 ISP)是这样表述的:`不应强迫使用者依赖于它们不用的方法。No client should be forced to depend on methods it does not use`. 指在接口中,不要放置使用者用不到的方法。之所以会出现这种情况,是因为他们根本没有思考过接口的问题,因为他们更关心的是一个个的具体类。 # 一、如何理解“接口隔离原则”? 接口隔离原则的英文翻译是“ Interface Segregation Principle”,缩写为 ISP。Robert Martin 在 SOLID 原则中是这样定义它的:“Clients should not be forced to depend upon interfaces that they do not use。”直译成中文的话就是:客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。 在这条原则中,我们可以把“接口”理解为下面三种东西: * 一组 API 接口集合 * 单个 API 接口或函数 * OOP 中的接口概念 ## 1、把“接口”理解为一组 API 接口集合 举个简单的例子: ```cpp class UserService { public: boolean register(String cellphone, String password); boolean login(String cellphone, String password); UserInfo getUserInfoById(long id); UserInfo getUserInfoByCellphone(String cellphone); } class UserServiceImpl : public UserService { //... } ``` 新需求:后台管理系统要实现删除用户的功能,希望用户系统提供一个删除用户的接口。 解决办法1: 我只需要在 UserService 中新添加一个 deleteUserByCellphone() 或 deleteUserById() 接口就可以了 解决办法2: 如果我们把它放到 UserService 中,那所有使用到 UserService 的系统,都可以调用这个接口。不加限制地被其他业务系统调用,就有可能导致误删用户。 将删除接口单独放到另外一个接口 RestrictedUserService 中,然后将 RestrictedUserService 只打包提供给后台管理系统来使用。 ```cpp class UserService { public: boolean register(String cellphone, String password); boolean login(String cellphone, String password); UserInfo getUserInfoById(long id); UserInfo getUserInfoByCellphone(String cellphone); } class RestrictedUserService { public: boolean deleteUserByCellphone(String cellphone); boolean deleteUserById(long id); } class UserServiceImpl : public UserService, RestrictedUserService { // ...省略实现代码... } ``` 类库接口的时候,**如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口**。 ## 2、把“接口”理解为单个 API 接口或函数 > 那接口隔离原则就可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现 ```cpp class Statistics { private: Long max; Long min; Long average; Long sum; Long percentile99; Long percentile999; //...省略constructor/getter/setter等方法... } public: Statistics* count(Collection<Long> dataSet) { Statistics* statistics = new Statistics(); //...省略计算逻辑... return statistics; } ``` 按照接口隔离原则,我们应该把 `count()` 函数拆成几个更小粒度的函数,每个函数负责一个独立的统计功能。 ```cpp public Long max(Collection<Long> dataSet) { //... } public Long min(Collection<Long> dataSet) { //... } public Long average(Colletion<Long> dataSet) { //... } // ...省略其他统计函数... ``` ### 接口隔离原则跟单一职责原则有点类似 不过稍微还是有点区别。单一职责原则针对的是模块、类、接口的设计。而接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同。它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。 ### 接口隔离原则与单一职责原则的区别 单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。 ## 3、把“接口”理解为 OOP 中的接口概念 如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。 # 一、胖接口减肥 假设有一个银行的系统,对外提供存款、取款和转账的能力。它通过一个接口向外部系统暴露了它的这些能力,而不同能力的差异要通过请求的内容来区分。 ```cpp class TransactionRequest { // 获取操作类型 TransactionType getType() { ... } // 获取存款金额 double getDepositAmount() { ... } // 获取取款金额 double getWithdrawAmount() { ... } // 获取转账金额 double getTransferAmount() { ... } } ``` 每种操作类型都对应着一个业务处理的模块,它们会根据自己的需要,去获取所需的信息,像下面这样: ```cpp class TransactionHandler { public: void handle(const TransactionRequest request); } class DepositHandler : public TransactionHandler { public: void handle(const TransactionRequest request) { double amount = request.getDepositAmount(); ... } } class WithdrawHandler : public TransactionHandler { public: void handle(const TransactionRequest request) { double amount = request.getWithdrawAmount(); ... } } class TransferHandler : public TransactionHandler { public: void handle(const TransactionRequest request) { double amount = request.getTransferAmount(); ... } } ``` 这样一来,我们只要在收到请求之后,做一个业务分发就好了: ```cpp TransactionHandler handler = handlers.get(request.getType()); if (handler != null) { handler.handle(request); } ``` 不过,作为业务处理中的接口,TransactionRequest 就显得“胖”了: getDepositAmount 方法只在 DepositHandler 里使用; getWithdrawAmount 方法只在 WithdrawHandler 里使用; getTransferAmount 只在 TransferHandler 使用。 然而,传给它们的 TransactionRequest 却包含所有这些方法。 ```cpp class TransactionRequest { ... // 获取生活缴费金额 double getLivingPaymentAmount() { ... } } ``` 还需要增加业务处理的方法 ```cpp class LivingPaymentHandler : public TransactionHandler { public: void handle(const TransactionRequest request) { double amount = request.getLivingPaymentAmount(); ... } } ``` 由于 TransactionRequest 的修改,前面几个写好的业务处理类:DepositHandler、WithdrawHandler、TransferHandler 都会受到影响。为什么这么说呢? 假如这段代码是用 C/C++ 这些需要编译链接的语言写成的,TransactionRequest 的修改势必会导致其它几个业务处理类重新编译,因为它们都引用了 TransactionRequest。 实际上,C/C++ 的程序在编译链接上常常需要花很多时间,除了语言本身的特点之外,因为设计没做好,造成本来不需要重新编译的文件也要重新编译的现象几乎是随处可见的。 怎样修改这段代码呢?既然这个接口是由于“胖”造成的,给它减肥就好了。根据 ISP,只给每个使用者提供它们关心的方法。所以,我们可以引入一些“瘦”接口: ```cpp //抽象类 class TransactionRequest { } class DepositRequest : public TransactionRequest { double getDepositAmount(); } class WithdrawRequest : public TransactionRequest { double getWithdrawAmount(); } class TransferRequest : public TransactionRequest { double getTransferAmount(); } class ActualTransactionRequest : public DepositRequest, WithdrawRequest, TransferRequest { ... } ``` 解决办法: 我们也可以改造对应的业务处理方法了:JAVA中允许,C++中不允许 ```cpp template<class T = TransactionRequest> class TransactionHandler { void handle(T request); } class DepositHandler : public TransactionHandler<DepositRequest> { public: void handle(DepositRequest request) { double amount = request.getDepositAmount(); ... } } class WithdrawHandler : public TransactionHandler<WithdrawRequest> { public: void handle(WithdrawRequest request) { double amount = request.getWithdrawAmount(); ... } } class TransferHandler : public TransactionHandler<TransferRequest> { void handle(TransferRequest request) { double amount = request.getTransferAmount(); ... } } ``` 新增生活缴费该如何处理呢?你可能已经很清楚了,就是再增加一个新的接口: ```cpp LivingPaymentRequest : public TransactionRequest { double getLivingPaymentAmount(); } class ActualTransactionRequest : public DepositRequest, WithdrawRequest, TransferRequest, LivingPaymentRequest { } ``` 然后,再增加一个新的业务处理方法: ```cpp class LivingPaymentHandler : public TransactionHandler<LivingPaymentRequest> { void handle(LivingPaymentRequest request) { double amount = request.getLivingPaymentAmount(); ... } } ``` 原本那个大的 TransactionRequest 被拆分成了若干个小接口,每个小接口就只为特定的使用者服务。这样做的好处就在于,每个使用者只要关注自己所使用的方法就行,这样的接口才可能是稳定的,“胖”接口不稳定的原因就是,它承担了太多的职责。 Note: **这个例子也可展开对 DIP 的讨论,为什么说一开始 TransactionRequest 是把依赖方向搞反了?** **因为最初的 TransactionRequest 是一个具体类,而 TransactionHandler 是业务类。** **我们后来改进的版本里引入一个模型,把 TransactionRequest 变成了接口,ActualTransactionRequest 实现这个接口,TransactionHandler 只依赖于接口,而原来的具体类从这个接口继承而来,相对来说,比原来的版本好一些。** # 二、你的角色 > 现在有了对 ISP 的理解,我们知道了,接口应该是尽可能稳定的。接口的使用者对于接口是一种依赖关系,被依赖的一方越稳定越好,而只有规模越小,才越有可能稳定下来。 在高层次上依赖于不需要的东西,这和类依赖于不需要的东西,其实是异曲同工的,由此可见,ISP 同样是一个可以广泛使用的设计原则。 最后修改:2025 年 07 月 01 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏