淺談C#六大設(shè)計原則
筆者作為一個菜鳥,會嘗試以簡單的代碼和容易理解的語句去解釋這幾種原則的特性和應(yīng)用場景。
這六種原則分別為單一職責(zé)原則、接口隔離原則、里氏替換原則、迪米特法則、依賴倒置原則、開閉原則。
單一職責(zé)原則
單一職責(zé)原則(SRP:Single responsibility principle),規(guī)定一個類中應(yīng)該只有一個原因引起類的變化。
單一職責(zé)原則的核心就是解耦和增強(qiáng)內(nèi)聚性。
問題:
// 假設(shè)此類是數(shù)據(jù)庫上下文
public class DatabaseContext { }
public class Test
{
private readonly DatabaseContext _context;
public Test(DatabaseContext context)
{
_context = context;
}
// 用戶登錄
public void UserLogin() { }
// 用戶注銷
public void UserLogout() { }
// 新增一個用戶
public void AddUser() { }
// 修改一個用戶的信息
public void UpdateUser() { }
// 刪除一個用戶
public void DeleteUser() { }
}
Test 負(fù)責(zé) 職責(zé) P1(用戶登錄和退出)和 P2(用戶賬號管理) 兩個職責(zé),當(dāng)由于職責(zé) P1 的需求發(fā)生變化而需要修改類時, 有可能會導(dǎo)致正常職責(zé) P2 的功能發(fā)生故障。
上面的代碼中,兩個職責(zé)被耦合起來,擔(dān)任了多種功能。
一個類中應(yīng)該只有一個原因引起類的變化,也就要求一個類只應(yīng)該負(fù)責(zé)一個功能,類中地代碼是緊密聯(lián)系的。
上面的示例代碼非常簡單,我們可以很自然地將一個個類分為兩個部分。
// 假設(shè)此類是數(shù)據(jù)庫上下文
public class DatabaseContext { }
public class Test1
{
private readonly DatabaseContext _context;
public Test1(DatabaseContext context)
{
_context = context;
}
// 用戶登錄
public void UserLogin() { }
// 用戶注銷
public void UserLogout() { }
}
public class Test2
{
private readonly DatabaseContext _context;
public Test2(DatabaseContext context)
{
_context = context;
}
// 新增一個用戶
public void AddUser() { }
// 修改一個用戶的信息
public void UpdateUser() { }
// 刪除一個用戶
public void DeleteUser() { }
}
因此,單一職責(zé)原則的解決方法,是將不同職責(zé)封裝到不同的類或模塊中。
接口隔離原則
接口隔離原則(ISP:Interface Segregation Principle) 要求對接口進(jìn)行細(xì)分,類的繼承建立在最小的粒度上,確保客戶端繼承的接口中,每一個方法都是它需要的。
筆者查閱了國外一些資料,大多將接口隔離原則定義為:
“Clients should not be forced to depend upon interfaces that they do not use.”
意思是不應(yīng)強(qiáng)迫客戶依賴于它不使用的方法。
對于此原則的解釋,這篇文章講的非常透徹:
https://stackify.com/interface-segregation-principle/
這就要求我們拆分臃腫的接口成為更小的和更具體的接口,使得接口負(fù)責(zé)的功能更加單一。
目的:通過將軟件分為多個獨(dú)立的部分來減少所需更改的副作用和頻率。
筆者想到從兩方面論述:
其一,在描述多種動物時,我們可能會將不同種類的動物分類。但是這還不夠,例如在鳥類中,我們印象中鳥的特征是鳥會飛,但是企鵝不會飛~。
那么還要對物種的特征進(jìn)行細(xì)分,例如血液是什么顏色的、有沒有脊椎等。
其二,我們可以通過下面代碼表達(dá):
// 與登錄有關(guān)
public interface IUserLogin
{
// 登錄
void Login();
// 注銷
void Logout();
}
// 與用戶賬號有關(guān)
public interface IUserInfo
{
// 新增一個用戶
void AddUser();
// 修改一個用戶的信息
void UpdateUser();
// 刪除一個用戶
void DeleteUser();
}
上面的兩個接口,各種實(shí)現(xiàn)不同的功能,彼此沒有交叉,完美。
接下來我們看看兩個繼承了 IUserLogin 接口的代碼
// 對用戶登錄注銷進(jìn)行管理,資源準(zhǔn)備和釋放
public class Test1 : IUserLogin
{
public void Login(){}
public void Logout(){}
}
public class Test2 : IUserLogin
{
public void Login()
{
// 獲取用戶未讀消息
}
public void Logout()
{
}
}
對于 Test1 ,根據(jù)登錄和注銷兩個狀態(tài),進(jìn)行不同操作。
但是,對于 Test2,它只需要登錄這個狀態(tài),其它情況不關(guān)它事。那么 Logout() 對他來說,完全沒有用,這就是接口污染。
上面的代碼就違法了接口隔離原則。
但是,接口隔離原則有個缺點(diǎn),就是容易過多地將細(xì)分接口。一個項目中,出現(xiàn)成千上萬個接口,將是維護(hù)地災(zāi)難。
因此接口隔離原則要靈活使用,就 Test2 來說,多繼承一個方法無傷大礙,不用就是了。ASP.NET Core 中就存在很多這樣的實(shí)現(xiàn)。
public void Function()
{
throw new NotImplementedException();
}
《設(shè)計模式之禪》第四章中,作者對接口隔離原則總結(jié)了四個要求:
1 接口盡量?。翰怀霈F(xiàn)臃腫(Fat)的接口。
2 接口要高內(nèi)聚:提高接口、類、模塊的處理能力。
3 定制服務(wù):小粒度的接口可以組成大接口,靈活定制新的功能。
4 接口的設(shè)計有限度:難以有固定的標(biāo)準(zhǔn)去衡量接口的粒度是否合理。
另外還有關(guān)于單一職責(zé)原則和接口隔離原則的關(guān)系和對比。
單一職責(zé)原則是從服務(wù)提供者的角度去看,提供一個高內(nèi)聚的、單一職責(zé)的功能;
接口隔離原則是從使用者角度去看,也是實(shí)現(xiàn)高內(nèi)聚和低耦合。
接口隔離原則的粒度可能更小,通過多個接口靈活組成一個符合單一職責(zé)原則的類。
我們也看到了,單一職責(zé)原則更多是圍繞類來討論;接口隔離原則是對接口來討論,即對抽象進(jìn)行討論。
開閉原則
開閉原則(Open/Closed Principle)規(guī)定 :
“軟件中的對象(類,模塊,函數(shù)等等)應(yīng)該對于擴(kuò)展是開放的,但是對于修改是封閉的”
--《Object-Oriented Software Construction》作者 Bertrand Meyer
開閉原則意味著一個實(shí)體是允許在不改變它的源代碼的前提下變更它的行為。類的改動是通過增加代碼實(shí)現(xiàn),而不是修改源代碼。
開閉原則 有 梅耶開閉原則、多態(tài)開閉原則。
梅耶開閉原則
代碼一旦完成,一個類的實(shí)現(xiàn)只應(yīng)該因錯誤而修改,新的或者改變的特性應(yīng)該通過新建不同的類實(shí)現(xiàn)。
特點(diǎn):繼承,子類繼承父類,擁有其所有的方法,并且拓展。
多態(tài)開閉原則
此原則使用接口而不是父類來允許不同的實(shí)現(xiàn),您可以在不更改它們的代碼的情況下輕松替換它們。
現(xiàn)在大多數(shù)情況下,開閉原則指的是多態(tài)開閉原則。
多態(tài)開閉原則筆者在查閱資料是,發(fā)現(xiàn)這個接口指的不是 Interface ,指的是抽象方法、虛方法。
問:面向?qū)ο蟮娜筇匦允鞘裁??答:封裝、繼承、多態(tài)。
對,多態(tài)開閉原則就是指這個多態(tài)。不過,原則要求不應(yīng)對方法進(jìn)行重載(重寫)、隱藏。
這是一個示例:
// 實(shí)現(xiàn)登錄注銷
public class UserLogin
{
public void Login() { }
public void Logout() { }
public virtual void A() {/* 做了一些事*/}
public virtual void B() {/* 也做了一些事*/ }
}
public class UserLogin1 : UserLogin
{
public void Login(string userName) { } // 應(yīng)不應(yīng)該對父類的方法進(jìn)行重載?
public override void A() { } // √
public override void B() { } // √
public new void Logout() { } // 也許行?
}
多態(tài)開閉原則的好處是,引入了抽象,使得兩個類松耦合,而且可以使得在不修改代碼的前提下,使用子類替換父類(里氏替換原則)。
有時,會看到這樣的題目:接口和抽象類的區(qū)別?
筆者隱約記得有過一條這樣的解釋:接口是為了實(shí)現(xiàn)共同的標(biāo)準(zhǔn);抽象是為了代碼的復(fù)用。
當(dāng)然,接口和抽象,都可以實(shí)現(xiàn)里氏替換。
通過開閉原則,我們可以了解到多態(tài),也了解接口和抽象的應(yīng)用場景。
還有一個問題是,開閉原則要求是要修改或添加功能時,通過子類來實(shí)現(xiàn),而不是修改原有代碼。那么是否可以和應(yīng)該對父類的代碼進(jìn)行重載和隱藏?
而開閉原則的核心是構(gòu)造抽象,從而通過子類派生來實(shí)現(xiàn)拓展。貌似沒有說到這方面。
筆者覺得不太應(yīng)該。。。
先結(jié)合下面的里氏替換原則,我們再討論這個問題?
里氏替換原則
里氏替換原則(LSP:Liskov Substitution Principle)要求:凡是父類出現(xiàn)的地方,子類都可以出現(xiàn)。
這就要求了子類必須與父類具有相同的行為。只有當(dāng)子類能夠替換任何父類的實(shí)例時,才會符合里氏替換原則。
里氏替換原則的約束:
1 子類必須實(shí)現(xiàn)父類的抽象方法,但不能重寫父類中已實(shí)現(xiàn)的方法。
2 子類中可以增加方法拓展功能。
3 當(dāng)子類覆蓋或?qū)崿F(xiàn)(虛擬方法/抽象方法)父類的方法時,方法的輸入?yún)?shù)限制更加寬松并且返回值要比父類方法更加嚴(yán)格。
所以,我們看到開閉原則中的示例,子類應(yīng)不應(yīng)該重載父類的方法?應(yīng)不應(yīng)該使用 new 關(guān)鍵字隱藏父類的方法?為了確保子類繼承后,還具有跟父類一致的特性,不建議這樣做呢,親。
實(shí)現(xiàn)了開閉原則,自然可以使用里氏替換原則。
依賴倒置原則
依賴倒置原則(Dependence Inversion Principle)要求程序要依賴于抽象接口,不要依賴于具體實(shí)現(xiàn)。
我們可以從代碼中,慢慢演進(jìn)和推導(dǎo)理論。
// 實(shí)現(xiàn)登錄注銷
public class UserLogin
{
public void Login(){}
public void Logout(){}
}
public class Test1 : UserLogin { }
public class Test2
{
private readonly UserLogin userLogin = new UserLogin();
}
public class Test3
{
private readonly UserLogin _userLogin;
public Test3(UserLogin userLogin)
{
_userLogin = userLogin;
}
}
上面代碼中,Test1、Test2、Test3 都依賴 UserLogin 。先不說上面代碼有什么毛病,根據(jù)依賴倒置原則,應(yīng)該是這樣編寫代碼的
// 與登錄有關(guān)
public interface IUserLogin
{
void Login(); // 登錄
void Logout(); // 注銷
}
// 實(shí)現(xiàn)登錄注銷
public class UserLogin1 : IUserLogin
{
public void Login(){}
public void Logout(){}
}
// 實(shí)現(xiàn)登錄注銷
public class UserLogin2 : IUserLogin
{
public void Login(){}
public void Logout(){}
}
public class Test4
{
private readonly IUserLogin _userLogin;
public Test4(IUserLogin userLogin)
{
_userLogin = userLogin;
}
}
依賴倒置原則,在于引入一種抽象,這種抽象將高級模塊和底層模塊彼此分離。高層模塊和底層模塊松耦合,底層模塊的變動不需要高層模塊也要變動。
依賴導(dǎo)致原則有兩個思想:
1 高層模塊不應(yīng)該依賴于底層模塊,兩者都應(yīng)該依賴于抽象。
2 抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴于抽象。
因?yàn)橐蕾囉诔橄?,底層模塊可以任意替換一個實(shí)現(xiàn)了抽象的模塊。
里氏替換原則是要求子類父類行為一致,子類可以替換父類。
依賴倒置原則,每個方法的行為是可以完全不一樣的。
迪米特法則
迪米特法則(Law of Demeter)要求兩個類之間盡可能保持最小的聯(lián)系。
例如 對象A 不應(yīng)該直接調(diào)用 對象B,而是應(yīng)該通過 中間對象C 來保持通訊。
請參考 https://en.wikipedia.org/wiki/Law_of_Demeter
優(yōu)勢:松耦合,較少了依賴。
缺點(diǎn):要編寫許多包裝代碼,增加復(fù)雜讀,模塊之間的通訊效率變低。
筆者找了很多資料,發(fā)現(xiàn)都是 java 的。。。
一般來說,較少會提到迪米特原則,代碼符合依賴倒置原則和里氏替換原則等,也就算是符合迪米特法則了。
以上就是淺談C#六大設(shè)計原則的詳細(xì)內(nèi)容,更多關(guān)于C#六大設(shè)計原則的資料請關(guān)注腳本之家其它相關(guān)文章!
- C#實(shí)現(xiàn)六大設(shè)計原則之依賴倒置原則
- C#面向?qū)ο笤O(shè)計原則之組合/聚合復(fù)用原則
- C#面向?qū)ο笤O(shè)計原則之接口隔離原則
- C#面向?qū)ο笤O(shè)計原則之里氏替換原則
- C#面向?qū)ο笤O(shè)計原則之單一職責(zé)原則
- C#面向?qū)ο笤O(shè)計原則之開閉原則
- C#實(shí)現(xiàn)六大設(shè)計原則之迪米特法則
- C#實(shí)現(xiàn)六大設(shè)計原則之接口隔離原則
- C#實(shí)現(xiàn)六大設(shè)計原則之里氏替換原則
- C#實(shí)現(xiàn)六大設(shè)計原則之單一職責(zé)原則
- C#編程之依賴倒置原則DIP
相關(guān)文章
WPF自定義MenuItem樣式的實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于WPF自定義MenuItem樣式的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用WPF具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06
c#數(shù)據(jù)綁定之將datatabel的data添加listView
這篇文章主要介紹了c#將DataTabel的data添加ListView的示例,實(shí)現(xiàn)功能是通過響應(yīng)UI Textbox 的值向ListView 綁定新添加的紀(jì)錄。 ,需要的朋友可以參考下2014-04-04
C#使用FileStream循環(huán)讀取大文件數(shù)據(jù)的方法示例
這篇文章主要介紹了C#使用FileStream循環(huán)讀取大文件數(shù)據(jù)的方法,結(jié)合實(shí)例形式分析了FileStream文件流的形式循環(huán)讀取大文件的相關(guān)操作技巧,需要的朋友可以參考下2017-05-05
C#機(jī)器入門學(xué)習(xí)之判斷日報是否合格詳解
這篇文章主要給大家介紹了關(guān)于C#機(jī)器入門學(xué)習(xí)之判斷日報是否合格的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者使用c#具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03

