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

方法的重寫,在實際應用中非常常見,比如零代碼平台中的消息組件會有多種發送消息的方式,下面用一個示例代碼演示下:
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<IMessage> messageList = new List<IMessage>();
messageList.Add(new EmailMessage());
messageList.Add(new WechatMessage());
messageList.ForEach(s=>s.Send("test message"));
}
}
為什麼說能提高擴展性呢?如果這時消息組件需要擴展發送簡訊的消息種類,只需要編寫簡訊類型的消息類實現 IMessage 介面的 Send 方法即可。
還有一種場景,比如登陸的時候,有基於用戶名密碼的認證、企業微信的認證、釘釘的認證、和對接第三方的認證,又應該怎麼設計呢?
我們雖然都在使用著面向對象的語言,但很多的時候思維還是面向過程的,具體體現在:
- 實體類的屬性直接定義為 public ,set 和 get 都安排上,外部可以任意獲取和賦值,很多時候使用代碼生產工具直接生產實體類,默認的 set 和 get 都是 public ,也沒有去依據具體的業務進行修改,嚴重破壞了封裝特性;
- 數據和行為的分離,也就是所謂的貧血模式,但真正的對象是數據行為在一起的,我們可能每天都在寫這樣的代碼,一種面向過程式的代碼;
- 為了代碼復用,代碼中會存在大量的 Helper 類或者 Utils、Common 類,這些類通常是靜態類,裡面有各種各樣的靜態方法,在往裡面添加方法時需要思考下,真的需要放到這裡嗎?
- 按照功能驅動,比如頁面上的一個按鈕操作,對應了一個 API 介面,不管你的代碼時如何設計和分層,一層層往下知道資料庫訪問。
所以不要以為使用了面向對象的語言就是在使用面向對象編程,重要的是抽象的思維,這種抽象需要我們去思考,去全盤考慮,相比較面向過程顯得更難,所以懶惰的程序員更容易寫出面向過程的代碼。
這些面向對象的基礎知識是學習設計模式的根基,掌握基礎知識,然後願意去思考,總結才能夠學習好設計模式,並將其應用到實際的工作中。下一篇將介紹面向對象中的常用設計原則,設計模式也都是基於這些設計原則演化而來。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/219308.html