面向對象設計的基本概念 面向對象設計的概念



文章插圖
面向對象設計的基本概念 面向對象設計的概念

文章插圖
主流的編程范式有三種:面向過程、面向對象和函數式編程 , 我們現在使用的主流編程語言 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 接口 , 不管你的代碼時如何設計和分層 , 一層層往下知道數據庫訪問 。
所以不要以為使用了面向對象的語言就是在使用面向對象編程 , 重要的是抽象的思維 , 這種抽象需要我們去思考 , 去全盤考慮 , 相比較面向過程顯得更難 , 所以懶惰的程序員更容易寫出面向過程的代碼 。
【面向對象設計的基本概念 面向對象設計的概念】這些面向對象的基礎知識是學習設計模式的根基 , 掌握基礎知識 , 然后愿意去思考 , 總結才能夠學習好設計模式 , 并將其應用到實際的工作中 。下一篇將介紹面向對象中的常用設計原則 , 設計模式也都是基于這些設計原則演化而來 。