設(shè)計(jì)模式:面向?qū)ο蟮幕A(chǔ)知識

主流的編程范式有三種:面向過程、面向?qū)ο蠛秃瘮?shù)式編程,我們現(xiàn)在使用的主流編程語言 C# 或 Java,都是面向?qū)ο笳Z言,所以常常說的設(shè)計(jì)模式也是在面向?qū)ο笳Z言這個(gè)前提之下。
面向?qū)ο蟮幕A(chǔ)知識和一些設(shè)計(jì)原則,我認(rèn)為是學(xué)習(xí)設(shè)計(jì)模式的基礎(chǔ),本文就聊下這些基礎(chǔ)知識。
在面試時(shí),一問到面向?qū)ο?,幾乎每個(gè)人都能脫口而出:封裝、繼承、多態(tài)。但大部分只能說出一個(gè)簡單的概念,而多態(tài)還有很多連概念都說不清楚。我們學(xué)習(xí)面向?qū)ο?,不止需要了解概念,更需要知道每個(gè)特性存在的意義和目的。
對于面向?qū)ο蟮奶匦?,面向?qū)ο蟮恼Z言都會給出相應(yīng)的支持,不同語言可能會有細(xì)微差別,下面的示例以 C# 語言為主。
封裝
我們先來思考下,平時(shí)寫代碼時(shí)有哪些是屬于封裝,是不是會有下面的一些場景:
1、將一些屬性字段放到一個(gè)類中;
2、將一些方法放到一個(gè)類中
3、將某些類組織到某個(gè)特定的命名空間下。
在 C# 9.0 版本中還提供了屬性的 init 特性,可以更方便地提供封裝性:
public?class?UserInfo
{
????public?string?Name?{?get;?init;?}
}
UserInfo?user?=?new?UserInfo?{?Name?=?"oec2003"?};
//當(dāng)?user?初始化完了之后就不能再改變?Name?的值
user.Name?=?"oec2004";
除了屬性、方法和類也有對應(yīng)的訪問修飾符,這些訪問修飾符的靈活運(yùn)用就達(dá)到了封裝的目的,用來隱藏信息或進(jìn)行數(shù)據(jù)的保護(hù)。
試想一下,如果我們對類中屬性或方法全部都使用 public ,調(diào)用方可以任意修改屬性和調(diào)用方法,這樣會使代碼變得不可控,屬性可能被很多地方以不同的方式進(jìn)行修改,代碼難以維護(hù)。而且不熟悉業(yè)務(wù)的開發(fā)人員如果隨意改動了一些關(guān)鍵屬性,可能引發(fā)嚴(yán)重的問題。
從另一個(gè)方面來說,類的共有屬性和方法暴露的越多,對于調(diào)用者來說就會越復(fù)雜,越容易出現(xiàn)問題,合理地進(jìn)行封裝,可以提高可讀性、可維護(hù)性,減少出錯(cuò)。
這時(shí),你是不是可以想想,平時(shí)寫代碼時(shí),屬性、方法、類是不是都不假思索地寫上了 public 了呢?
繼承
目前面向?qū)ο蟮恼Z言基本都支持繼承特性,只是語法上有些細(xì)微的差別,比如 C# 語言是使用冒號,Java 語言使用 extends 關(guān)鍵字。但都是表示 is-a 的關(guān)系。
在 C# 中一個(gè)類可以繼承多個(gè)接口,但只能繼承一個(gè)父類,我們通常說的 C# 只支持單繼承指的就是 C# 只能繼承一個(gè)父類,但在 C++ 、Python 等語言中類是可以繼承多個(gè)父類的。
我們經(jīng)常會跟開發(fā)人員講,不要到處復(fù)制代碼,代碼要做到能夠復(fù)用,發(fā)現(xiàn)同一個(gè)邏輯在兩個(gè)不同的類中的時(shí)候,可以抽象出來一個(gè)父類,讓這兩個(gè)類都繼承這個(gè)父類。這個(gè)思路沒有問題,也確實(shí)能解決我們的實(shí)際問題,提升代碼的復(fù)用性。
但隨著功能的增加,我們需要對類的屬性和方法進(jìn)行擴(kuò)展,會發(fā)現(xiàn)需要新添加的屬性或方法放在父類或子類都不合適,只能繼續(xù)進(jìn)行抽象,長此下去,繼承關(guān)系會變得非常復(fù)雜,變得難以維護(hù)。有條設(shè)計(jì)原則是這么說的:組合優(yōu)于繼承,其實(shí)就是為了解決這個(gè)問題。
組合和繼承的選擇是一種權(quán)衡,當(dāng)涉及的類經(jīng)常變化可能導(dǎo)致繼承層級向著復(fù)雜化演化時(shí),需要考慮采用組合的方式,如果相關(guān)類比較穩(wěn)定,繼承層級不深(一般不超過 3 層),就可以放心使用繼承。
在具體的模式中,組合模式、策略模式等就是使用組合的方式實(shí)現(xiàn),模板模式使用的是繼承方式實(shí)現(xiàn)。
多態(tài)
多態(tài)的字面意思就是同樣的一個(gè)語法調(diào)用,能夠表達(dá)多個(gè)不同的意思。如果說繼承的最大好處是復(fù)用,那么多態(tài)的好處就是方便擴(kuò)展。
在 C# 語言中兩個(gè)比較典型的多態(tài)場景就是方法的重寫和方法的重載:
重寫:存在繼承關(guān)系的類或接口,在子類中對父類的方法進(jìn)行重新構(gòu)建邏輯,但調(diào)用方法、參數(shù)、返回值保持一致,通常有下面幾種情況: 普通的父類中有用 virtual 關(guān)鍵字標(biāo)識的虛方法,在子類中使用 override 關(guān)鍵字進(jìn)行重寫; 子類對抽象類的抽象方法進(jìn)行重寫; 子類對接口中的方法進(jìn)行實(shí)現(xiàn)。 重載:類中的多個(gè)方法,方法名相同,但參數(shù)個(gè)數(shù)或類型不相同,稱之為重載方法。例如 C# 中的 File 類的 Open 方法就有三個(gè)重載,如下圖:

方法的重寫,在實(shí)際應(yīng)用中非常常見,比如零代碼平臺中的消息組件會有多種發(fā)送消息的方式,下面用一個(gè)示例代碼演示下:
public?interface?IMessage
{
????void?Send(string?msg);
}
public?class?EmailMessage?:?IMessage
{
????public?void?Send(string?msg)
????{
????????Console.WriteLine($"send?email?message?{msg}");
????}
}
public?class?WechatMessage?:?IMessage
{
????public?void?Send(string?msg)
????{
????????Console.WriteLine($"send?wechat?message?{msg}");
????}
}
class?Program
{
????static?void?Main(string[]?args)
????{
????????List?messageList?=?new?List();
????????messageList.Add(new?EmailMessage());
????????messageList.Add(new?WechatMessage());
????????
????????messageList.ForEach(s=>s.Send("test?message"));
????}
}
為什么說能提高擴(kuò)展性呢?如果這時(shí)消息組件需要擴(kuò)展發(fā)送短信的消息種類,只需要編寫短信類型的消息類實(shí)現(xiàn) IMessage 接口的 Send 方法即可。
還有一種場景,比如登陸的時(shí)候,有基于用戶名密碼的認(rèn)證、企業(yè)微信的認(rèn)證、釘釘?shù)恼J(rèn)證、和對接第三方的認(rèn)證,又應(yīng)該怎么設(shè)計(jì)呢?
我們雖然都在使用著面向?qū)ο蟮恼Z言,但很多的時(shí)候思維還是面向過程的,具體體現(xiàn)在:
實(shí)體類的屬性直接定義為 public ,set 和 get 都安排上,外部可以任意獲取和賦值,很多時(shí)候使用代碼生成工具直接生成實(shí)體類,默認(rèn)的 set 和 get 都是 public ,也沒有依據(jù)具體的業(yè)務(wù)進(jìn)行修改,嚴(yán)重破壞了封裝特性; 數(shù)據(jù)和行為的分離,也就是所謂的貧血模式,但真正的對象是數(shù)據(jù)和行為在一起的,我們可能每天都在寫這樣的代碼,一種面向過程式的代碼; 為了代碼復(fù)用,代碼中會存在大量的 Helper 類或者 Utils、Common 類,這些類通常是靜態(tài)類,里面有各種各樣的靜態(tài)方法,在往里面添加方法時(shí)需要思考下,真的必須放到這里嗎?這種類隨著時(shí)間的推移很容易變成巨型類,變得難以維護(hù); 按照功能驅(qū)動,比如頁面上的一個(gè)按鈕操作,對應(yīng)了一個(gè) API 接口,不管你的代碼是如何設(shè)計(jì)和分層,都是一層層往下直到數(shù)據(jù)庫訪問。
所以不要以為使用了面向?qū)ο蟮恼Z言就是在使用面向?qū)ο缶幊?,重要的是抽象的思維,這種抽象需要我們?nèi)ニ伎?,去全盤考慮,相比較面向過程顯得更難,所以懶惰的程序員更容易寫出面向過程的代碼。
面向?qū)ο蟮幕A(chǔ)知識是學(xué)習(xí)設(shè)計(jì)模式的根基,掌握基礎(chǔ)知識,然后愿意去思考,總結(jié)才能夠?qū)W習(xí)好設(shè)計(jì)模式,并將其應(yīng)用到實(shí)際的工作中。下一篇將介紹面向?qū)ο笾械某S迷O(shè)計(jì)原則,設(shè)計(jì)模式也都是基于這些設(shè)計(jì)原則演化而來。
希望本文對您有所幫助!
