介面隔離原則(Interface Segregation Principle, ISP):使用多個專門的介面,而不使用單一的總介面,即客戶端不應該依賴那些它不需要的介面。 從介面隔離原則的定義可以看出,他似乎跟SRP有許多相似之處。 是的其實ISP和SRP都是強調職責的單一性, 介面隔離原則告訴我們 ...
介面隔離原則(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,系統的靈活性就會降低,不易維護和擴展。
關聯閱讀: