接口隔離原則(Interface Segregation Principle, ISP):使用多個專門的接口,而不使用單一的總接口,即客戶端不應該依賴那些它不需要的接口。
從接口隔離原則的定義可以看出,他似乎跟SRP有許多相似之處。 是的其實ISP和SRP都是強調職責的單一性, 接口隔離原則告訴我們在定義接口的時候要根據職責定義“較小”的接口,不要定義“高大全”的接口。也就是說接口要盡可能的職責單一,這樣更容易復用,暴露給客戶端的方法更具有“針對性”, 比如定義一個接口包括一堆訪問數據庫的方法, 有包括一堆訪問網絡的方法,還包括一些權限認證的方法。 把這么一攤子風牛馬不相及的方法封裝到一個接口里面,顯然是不合適的, 如果客戶程序只想用到數據訪問的一些功能,但是調用接口的時候你把訪問網絡的方法和權限認證的方法暴露給客戶,這使得客戶程序感到“疑惑”,那么這個接口就不ISP,它很顯然的構成了接口污染。
注意: 這里所說的接口是廣義上的接口,他是一組契約, 是提供給程序交互的一組約定,並非各種語言interface 關鍵字定義的一組方法的結集合。但是這里所說的接口可以用各種語言的關鍵字interface 來定義,當然也可以用抽象類,類等等來定義。
假設有個客戶提出了軟件系統的需求:
1. 用戶可以使用第三方QQ,微信,微博登錄到系統。
2.系統中包括人員管理人員管理。
3.訪問第三方的API獲取一些數據。
好了拿到這個需求后首先經過分析,簡單的原型設計,數據庫設計之后開始編寫代碼了。 通常第一步定義接口。很快接口就定義出來了如下:
public interface IObject { void Connection(string connectionString); SqlDataReader ExcuteSql(string sql); string LoginWithQQ(string token); string LoginWithWeibo(string token); string LoginWithWeiXin(string token); string GetDataFromAPI(string url, string token); }
這個看起來還不錯,接口已經定義了,寫個具體類繼承一下這個接口並實現所有的方法,現在就可以實現業務,寫界面了。 等過了幾天客戶說 在給我加上支付寶登錄。那好再加一個支付寶登錄接口,代碼現在長這樣子:
public interface IObject { void Connection(string connectionString); SqlDataReader ExcuteSql(string sql); string LoginWithQQ(string token); string LoginWithWeibo(string token); string LoginWithWeiXin(string token); string GetDataFromAPI(string url, string token); string LoginWithAlipay(string token); }
再在實現類中實現一下LoginWithAlipay方法 就好了。
時間在推移,一天客戶說再給我加個百度登錄,好吧套路有了加一個就是了,有啥了不起。 時間依舊。。。 客戶說加個 facebook 登錄, 。。。加個 Linkedin。。。, 尼瑪 沒完沒了了, 現在接口已經變成這樣子了:
public interface IObject { void Connection(string connectionString); SqlDataReader ExcuteSql(string sql); string LoginWithQQ(string token); string LoginWithWeibo(string token); string LoginWithWeiXin(string token); string GetDataFromAPI(string url, string token); string LoginWithAlipay(string token); string LoginWithTwitter(string token); string LoginWithFaceBook(string token); string LoginWithRenRen(string token); string LoginWithBaidu(string token); string LoginWithDropbox(string token); string LoginWithGithub(string token); //這里省略10000字 stringLoginWithLinkedin(string token); }
有一天這個接口自己都不想看了,太多方法了,更何況實現類中的代碼都七八千行了。
於是決定重構, 現在回頭看看這個接口早就應該重構了,甚至一開始定義的時候就應該拆分,接口的名字都不知道怎么命名(一般在寫代碼的時候類,接口,方法的名字不知道怎么命名的時候就是該重構的時候了)竟然起了IObject這么奇葩的名字,這個設計顯然是爛到家了, 他幾乎違背了我們講過的所有設計原則, 必須到了要重構的時候了。
來吧,重構吧,經過分析第一步先根據功能來划分將IObject接口拆分成三個“小”接口:
1.數據庫操作相關的抽取到一個接口中(IDatabaseProvider)。
2.第三方API調用相關的方法抽取到一個接口中(IThirdpartyAPIProvider)。
3.第三方登陸相關的方法抽取到一個接口中(IThirdpartyAuthenticationProvider)。
現在代碼變成這個樣子:
public interface IDatabaseProvider { SqlDataReader ExcuteSql(string sql); string LoginWithQQ(string token); } public interface IThirdpartyAPIProvider { string Get(string url, string token); } public interface IThirdpartyAuthenticationProvider { string LoginWithQQ(string token); string LoginWithWeibo(string token); string LoginWithWeiXin(string token); string LoginWithAlipay(string token); string LoginWithTwitter(string token); string LoginWithFaceBook(string token); string LoginWithRenRen(string token); string LoginWithBaidu(string token); string LoginWithDropbox(string token); string LoginWithGithub(string token); //這里省略10000字 string LoginWithLinkedin(string token); }
這下看起來好多了, 但是IThirdpartyAuthenticationProvider 代碼還很多,還很丑陋,有沒有辦法再進一步重構呢? 答案是肯定。 第二步 我們可以將第三方登錄的接口中的LogigWithxxx方法提到一個單獨的接口中,其他具體站點的接口再繼承這個接口,代碼如下:
public interface IThirdpartyAuthenticationProvider { string Login(string token); } public interface IQQAuthenticationProvider:IThirdpartyAuthenticationProvider{} public interface IWeiboAuthenticationProvider:IThirdpartyAuthenticationProvider{} public interface IWeiXinAuthenticationProvider:IThirdpartyAuthenticationProvider{} public interface IAlipayAuthenticationProvider:IThirdpartyAuthenticationProvider{} public interface ITwitterAuthenticationProvider:IThirdpartyAuthenticationProvider{} public interface IFaceBookAuthenticationProvider:IThirdpartyAuthenticationProvider{} public interface IRenRenAuthenticationProvider:IThirdpartyAuthenticationProvider{} public interface IBaiduAuthenticationProvider:IThirdpartyAuthenticationProvider{} public interface IDropboxAuthenticationProvider : IThirdpartyAuthenticationProvider { } public interface IGitHubAuthenticationProvider:IThirdpartyAuthenticationProvider{} //這里省略10000字 public interface ILinkedinAuthenticationProvider : IThirdpartyAuthenticationProvider { }
這這下就好多了。 我們分析一下重構后的代碼有什么好處:
1. 接口的職責更單一了,調用目標更清晰了,每一個接口就專門做一件事情。符合SRP了。
2. 在操作數據庫的時候不會在IDatabase接口中調到其它的第三方API調用和第三方登錄認證相關的方法,每一個幾口更專注了。符合ISP了。
3.在添加新的第三方登錄的時候不需要在修改原來的實現 類了,核心業務邏輯只需要加一個接口和接口的實現類就可以了。符合OCP了。
4. 提升了代碼的穩定性,可維護性和可擴展性。
當然任何事情都具有兩面性,如果將一件好事做到極端有可能就會走向反面, 比方說定義一個User實體的接口:
public interface IIdProperty { int Id { get; set; } } public interface IFirstNameProperty { string FirstName { get; set; } } public interface ILastNameProperty { string LastName { get; set; } } public interface IAgeProperty { int Age { get; set; } } public interface IBirthdayProperty { DateTime Birthday { get; set; } } public class User:IIdProperty,IFirstNameProperty,ILastNameProperty,IAgeProperty,IBirthdayProperty { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public DateTime Birthday { get; set; } }
把每個屬性都定義成一個接口那就不可取了,也是沒有意義的,反而給維護或擴展帶來不必要的麻煩,這就是使用接口時要注意的地方:
在使用接口時要注意控制接口的粒度,接口定義的粒度不能太細,也不能太粗。 接口粒度太細,系統中就會出現接口泛濫,接口和實現類急劇膨脹,反而不易維護;接口粒度太粗,就會違背ISP,系統的靈活性就會降低,不易維護和擴展。
關聯閱讀: