面向對象的思考

2024年2月6日 17点热度 0人点赞

What

面向對象

作為一名程序員,代碼編程我們平時伸手就來。日常用到比較多的語言也許是 Java、TypeScript、C 等,大傢都很清楚,這些都是面向對象的語言。那麼問題也隨之而來,是我們需要使用面向對象的特性才選擇了這些語言開發,還是人雲亦雲地選擇了這些語言開發?

在面向對象的理念中,萬物皆對象。面向對象的精髓在於抽象,面向對象的困難也在於抽象。簡單來說:面向對象的成功在於成功的抽象,面向對象的失敗在於失敗的抽象。

對象與對象之間都是孤立的,好比現實生活的你和我之間。隻有在特定的場景下,孤立對象之間進行了某些信息交互才表示出一個場景過程,好比基於這篇文章,你和我之間才建立起了作者和讀者的關系。

面向過程

既然說到面向對象,就還應該了解到另外一個選項:面向過程。面向過程認為我們的世界是由一個個相互關聯的消息同組成的,這是一種類似 螺旋式 的結構,每個小系統都有明確的開始和明確的結束,開始和結束之間有著嚴謹的因果關系。在面向過程的設計方法中強調將問題分解成小的、可重用的模塊,每個模塊負責執行特定的任務。

Talk is cheap. Show me the code! 我們聯想下生活中是如何購物的?瀏覽商品 → 加購 → 結算 。轉換成對應的偽代碼,應該是這樣的:

這很符合我們平時的代碼風格,但也確實是一種面向過程的標準寫法。感到一絲詫異和矛盾,難道我們平時都在用面向對象語言來寫面向過程的代碼?事實確實如此。我們平時的方法封裝調用很大一部分就是面向過程的設計。

這裡並非是說 面向過程 的寫法不正確,反而在某些場景下面向過程更加直觀。但面向對象的設計為何而來?

哪怕我們平時大部分做的都是 CRUD 的工作,重復性的代碼也會構建出一個龐大的系統。構成一個系統的因素太多,要把所有問題的因素都考慮到,再把所有因素的因果關系都分析清楚,接著把整個過程都用代碼表述出來未免太過於困難,這不僅對創作者來說是一個災難,對後繼者來說更是:talk is cheap, code is shit

我們轉換下思維,如何利用面向對象的特性設計以上代碼?利用面向對象的方法論,萬物皆對象。

那麼由此可以設計出 商品購物車 以及 付款 的對象

通過利用這種方式,不論在哪一個層次上,我們都隻需要面對有限的復雜度和有限的對象結構,從而可以專心地了解這個層次上的對象是如何工作的。比如在付款的結構上,我們可以隻專註付款的擴展,從而可以延伸出 微信付款支付寶付款銀聯付款 等眾多方式。

How

作為開發,我們工作的本質就是把一個產品需求轉化成一個可以運行的系統,中間可能會涉及產品設計、需求建模、架構設計、實現設計、代碼編寫等眾多步驟。

在調研需求時常常會陷入面向過程的誤區,我們會最先弄清楚有多少業務流程,接著畫出業務流程圖,然後順藤摸瓜,找出業務流程中每個關鍵步驟,弄清楚上下文是如何傳遞的。

事實上,架構設計、實現設計、代碼編寫都是屬於軟件開發的階段。在設計之前,我們如果有一個清晰的目標,那後面的行動無疑會變得順利很多。我們面對著成百上千的需求,每個需求可能都存在錯綜復雜的關系,復雜度並非是線性增長的。需求復雜度是否相等於技術復雜度?

面向對象編程意味著編寫針對建模對象的代碼。這是用於描述復雜系統動作的眾多技術之一。它是通過描述交互對象的數據和行為來定義。 那麼在編程之間我們就需要進行很關鍵的一步:建模

ChatGPT:建模是指對客觀事物建立一種抽象的方法用以表征事物並獲得對事物本身的理解,同時把這種理解懷念化,並將這些邏輯概念組織起來,構成一種對所觀察對象內部結構和工作原理便於理解的表達。

公式:靜態的事物(物) 特定的條件(規則) 特定的動作(參與者的驅動)=特定的場景(事件)

當我們嘗試為需求背後的場景建模時,首先要決定便是抽象的角度,這一步尤其重要也尤其不易。一旦抽象角度確定,後面的設計就變得順理成章,而不是雜亂無章。

這一步也是與 面向過程 的主要區別所在:

  • 面向過程:希望能夠通盤考慮,把所有問題的因素都考慮到,再將這些因果關系理清,這就會將結構變得很復雜。(把事情復雜化)
  • 面向對象:希望能夠把復雜的事物通過合理的抽象角度分解成小塊,每個小塊之間單獨思考,最後再基於特定的場景將塊與塊之間串聯。(把事情簡單化) 因此面向對象的關鍵就在於剛開始面對問題領域的時候不要決定去全盤考慮,而是找出問題領域裡包含的抽象角度,隻要抽象角度找對找全,那麼通盤的問題也就層層解決。當然,在抽象的過程中,每個角度之間可能是互不關聯的。

做需求的時候,首要目標不是要弄清楚業務是如何一步一步完成的,而是要弄清楚有多少業務的參與者,以及每個參與者的目標是什麼。而這其中參與者的目標便是你需要抽象的角度

抽象角度

抽象的層次越高,具體信息越少,對應的概括能力越強。

抽象有兩種方式:

  • 自頂向下: 自頂向下的方法適用於讓人們從頭開始認識一個事物。先宏觀後微觀
  • 自底向上:自底向上的方法適用於在實踐中改進和提高認識。先微觀後宏觀

映射到我們的軟件開發過程中,我們往往會采用自頂向下的方式,先搭建一個框架,用少量粗粒度的概念來覆蓋系統的需求,再逐步細化,降低抽象的層次。同時在細化的過程中,我們也能夠通過細節間的相同之處,來改進較高層次的抽象范圍。因此這兩種方式應當是相輔相成,而不是兩者擇一的關系。

根據抽象而成的對象理應具備以下特征:

  • 對象都具有原子性 無論在什麼時候,在同一抽象層次上,在分析過程中都應當將對象視為一個不可分割的原子,哪怕這個對象的規模很大。
  • 對象都是可抽象的 對象所具有的方面,或者說對象所參與的場景越多,對象越有抽象價值,反之則越沒有抽象價值
  • 對象都有層次性 對象是有著抽象層次的。層次越高,其描述越粗略但適應能力越廣;層次越低則描述越精確但適應能力越下降

參與者

參與者的角色在建模過程中是處於核心地位的。他們是處於系統范圍之外,居於業務范圍之內與系統進行交互的。

參與者和系統之間有一個明確的邊界,參與者隻可能存在於邊界之外,邊界之內(系統之內)的所有人或事都不是參與者。如果參與者涉及到了系統邊界之內,那麼此時參與者的角色就是可疑的,需要重新定義。因此在業務設計階段,一定要堅守好這道邊界,參與者過早侵入系統就可能會對系統造成損害。

什麼是參與者?簡單定義如下:

  • 誰將使用此功能
  • 誰對某個特定功能感興趣
  • 誰負責支持和維護系統 參與者一定是直接並且主動地向系統發出動作並獲得反饋的,否則就不是參與者

在實際業務分析階段,我們需要區分 業務范圍系統范圍

  • 業務范圍:是指項目所涉及的全部客戶業務領域,不論是否涉及開發系統的參與,這些業務都是客觀存在的
  • 系統范圍:是指軟件系統將要實現的那一部分功能,這些功能是從業務范圍中提取,是業務范圍的一個子集

如果在業務分析階段就預設了計算機系統的存在,那麼將會混淆現有業務和將來實際開發的業務,將用戶代入到計算機系統中,可能會造成現有業務屬性的削弱,而忽略了實際業務背後的含義。

當然,這也是開發人員對接業務需求的一大誤區之一。喜歡從計算機系統的角度來思考問題,在向客戶收集需求的時候總是在第一時間想到計算機將如何實現它,常常津津樂道於跟客戶討論背後的系統將如何實現客戶的需求,並且指望客戶能夠用這種方式來確認需求。帶來的危害:

  • 客戶不能理解將來的落地實現是什麼樣子的,但是出於信任開發人員,就會將信將疑地做出肯定
  • 開發人員在一開始就加入了自己的主觀判斷,假設了業務在計算機系統裡的實現方式,而沒有真正地去理解客戶的實際業務

用例

用例是一種描述系統如何與外部用戶或其他系統進行交互的技術工具。它描述了系統的功能和行為,並以用戶的角度來描述系統的需求和使用場景。簡言之:用例是用來捕捉功能性需求

在軟件開發階段,我們會以用例作為最小指導單元進行設計,標準的用例應當具備以下特征:

  • 用例是相對獨立的
  • 用例的執行結果對參與者來說是可觀測的和有意義的 用例的粒度大小不是從用例包含的步驟的多少來判斷的,而是每一個用例能盡量能夠說明一件完整的事情

用例的獲取並非來源於開發人員,換言之,開發人員是用例的翻譯者。用例的定義是參與者驅動的,這也對應了用例的特征之一:用例的執行結果對參與者來說是可觀測的和有意義的。因此用例的來源就是參與者對系統的期望。所以發現用例的前提條件就是發現參與者,確定參與者的同時就確定了系統邊界。

Code without understanding the business is like playing the rogue 結合業務,與參與者聊需求的時候經常感覺需求飄渺不定,每個點都感覺是需求的核心部分。那麼是否有認真想過:參與者想做和要做的事情不一定是他真實的目標,也許隻是他做事情的一個步驟

  • 一個明確有效的目標才是一個用例的來源
  • 一個真實的目標應當完備地表達參與者的期望
  • 一個有效的目標應當在系統邊界內,由參與者發動,並具有明確的後果

在軟件開發需要耗時最久是哪個階段?應該是需求分析和設計階段。為了縮短開發周期,很容易想到便是縮減需求分析和設計階段的時間,當你真的這麼做了,你可能就會離真正的用例愈偏愈遠!要平衡時間和質量,就需要充分理解用戶需求,當訪談不順利的時候應當重新調整策略,調整系統邊界的規模或者更換訪談的參與者都是不錯的選擇。

用例不是功能點 在大多數開發者看來 用例是一個功能點。然而實際情況並非如此。功能的生命周期:輸入->計算->輸出,功能是脫離使用者的願望而存在的,本身是孤立的。

類似一個判斷用戶是否有操作權限的方法,這個方法本身是脫離業務的,它的用例場景可能是用戶在提交某個模塊下的變更記錄,才需要校驗權限,而校驗權限隻是其中的一個功能。

在實際情況下,更應當從使用者的觀點去描述,一個用例是一個參與者如何使用系統,獲得什麼結果的一個集合,那麼此時用例可以解釋為一系列完成一個特定目標的功能的組合,針對不同的應用場景,這些功能可以以不同的組合方式構建出新的用例。

步驟不是目標 一個用例是參與者對目標的一個期望。在完成這個目標之前需要經由很多步驟,但每個步驟並不能完整地反映出參與者的目標,因此作為一個用例是有所缺陷的。錯誤地使用步驟作為用例,將無法準確地描述參與者如何使用系統,整體系統的操作流程以及交互細節會發生偏差,脫離預期。

目標和步驟很容易造成混淆,在弄清楚目標與步驟之前,我們就需要設置一些實際場景來解釋,通過場景中涉及到的問題來測試出什麼才是參與者真正的目的。

可能在此之前參與者也不清楚自己想要的到底是什麼!


至此,回想《領域驅動設計-軟件核心復雜性應對之道》自問世以來,帶來了一個全新的思想:領域驅動設計。重點傳述的也是通過領域設計來分離業務復雜度和技術復雜度。復雜度也許永遠不能消除,但我們可以分析復雜度,進而管理復雜度。書中有提到:

每個軟件程序是為了執行用戶的某項活動,或是滿足用戶的某種需求。這些用戶應用軟件的問題區域就是軟件的領域。

回過頭來看上述所講到的面向對象,不乏有相同之處。某種程度上,把“領域驅動設計”改為“模型驅動設計”也相當貼切。

回收開頭的問題,我認為:需求復雜度 != 技術復雜度,建模理應成為我們管理復雜度的工具!

本篇文章的思維路線:

最後以 ChatGPT 來個結尾 面向對象編程在與業務需求結合時展現出不凡的優勢,通過將業務需求映射為對象和類的組織結構,我們能夠更好地理解和管理復雜的業務邏輯。

通過面向對象的方法,我們能夠將業務需求轉化為具體的對象和類,從而更好地理解和模擬真實世界中的業務流程和實體。通過封裝數據和行為,我們能夠將復雜的業務邏輯劃分為獨立的對象,每個對象負責特定的功能和責任。這種模塊化的設計使得我們能夠更好地理解和管理業務需求,同時也為將來的擴展和修改提供了便利。