關於面向對象“封裝”的理解


 

"封裝"是面向對象思想中最基礎的概念,實質上是將相關的函數和對象放一起,對外有函數作為操作通道,對內則以變量作為操作原料。

問題1 將數據結構和函數放在一起是否真的合理

函數是做事情的,其有輸入、執行邏輯、輸出;而數據結構是用來表達數據的,可作為輸入或輸出。

兩者本質上是不同的東西,面向對象思想將他們放到一起,使得函數的作用被限制在某一個區域里,這樣做雖然能夠很好地將操作歸類,但是這種歸類方法是根據"作用領域"來歸類的,在現實世界中可以,但在程序的世界中,有可能有些不妥。

不妥的理由可以用如下兩個情形是試着說明:

情形1:

並行計算時,由於執行部分和數據部分被綁定在一起,這就使得這種方案制約了並行程度。在為了更好地實現並行的時候,業界的工程師們發現了一個新的思路:函數式編程將函數作為數據來使用,這樣就能保證執行的功能在時序上的正確性了。但你不覺得,只要把數據表達和執行部分分開,形成流水線,這不就能夠非常方便地將並行數提高了么?

舉例:

 在數據和函數沒有分開時,程序的執行流程是這樣:

  A.function1() -> A.function2() -> A.function3() 最后得到經過處理的A 

當處於並發環境時,假設有這么多任務同時到達

  A.f1() -> A.f2() -> A.f3()     最后得到經過處理的A
  B.f1() -> B.f2() -> B.f3()     最后得到經過處理的B
  C.f1() -> C.f2() -> C.f3()     最后得到經過處理的C
  D.f1() -> D.f2() -> D.f3()     最后得到經過處理的D
  E.f1() -> E.f2() -> E.f3()     最后得到經過處理的E
  F.f1() -> F.f2() -> F.f3()     最后得到經過處理的F
  ...

假設並發數是3,那么完成上面類似的很多個任務,時序就是這樣

| time | 1   | 2   | 3   | 4   | 5   | 6   | 7   | 8   | 9   | 10  | 11  | 12  |
|------|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
| A    | A.1 | A.2 | A.3 |     |     |     |     |     |     |     |     |     |
| B    | B.1 | B.2 | B.3 |     |     |     |     |     |     |     |     |     |
| C    | C.1 | C.2 | C.3 |     |     |     |     |     |     |     |     |     |
| D    |     |     |     | D.1 | D.2 | D.3 |     |     |     |     |     |     |
| E    |     |     |     | E.1 | E.2 | E.3 |     |     |     |     |     |     |
| F    |     |     |     | F.1 | F.2 | F.3 |     |     |     |     |     |     |
| G    |     |     |     |     |     |     | G.1 | G.2 | G.3 |     |     |     |
| H    |     |     |     |     |     |     | H.1 | H.2 | H.3 |     |     |     |
| I    |     |     |     |     |     |     | I.2 | I.2 | I.3 |     |     |     |
| J    |     |     |     |     |     |     |     |     |     | J.1 | J.2 | J.3 |
| K    |     |     |     |     |     |     |     |     |     | K.1 | K.2 | K.3 |
| L    |     |     |     |     |     |     |     |     |     | L.1 | L.2 | L.3 |

當數據和函數分開時,並發數同樣是3,就能形成流水線了,有沒有發現吞吐量一下子上來了?

| time | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11| 12|
|------|---|---|---|---|---|---|---|---|---|---|---|---|
| f1() | A | B | C | D | E | F | G | H | I | J | K | L |
| f2() | Z | A | B | C | D | E | F | G | H | I | J | K |
| f3() | Y | Z | A | B | C | D | E | F | G | H | I | J |

你要是粗看一下,誒?怎么到了第13個周期K才剛剛結束?上面一種方案在第12個周期的時候就結束了?不能這么看的哦,其實在12個周期里面,Y、Z也已經交付了。因為流水線吞吐量的提升是有過程的,我截取的片段應該是機器在持續運算過程中的一個片段。

我們不能單純地去看ABCD,要看交付的任務數量。在12個周期里面,大家都能夠完成12個任務,在11個周期里面,流水線完成了11個任務,前面一種只完成了9個任務,流水線的優勢在這里就體現出來了:每個時間段都能穩定地交付任務,吞吐量很大。而且並發數越多,跟第一種方案比起來的優勢就越大,具體的大家也可以通過畫圖來驗證。

情形2:

  函數就是一個執行黑盒,只要滿足函數調用的充要條件(給夠參數),就是能夠確定輸出結果的。面向對象思想將函數和數據綁在一起,這樣的封裝擴大了代碼重用時的粒度。如果將函數和數據拆開,代碼重用的基本元素就由對象變為了函數,這樣才能更靈活更方便地進行代碼重用。

  誰都經歷過重用對象時,要把這個對象所依賴的所有東西都要移過來,哪怕你想用的只是這個對象里的一個方法,然而很有可能你的這些依賴是跟你所需要的方法無關的。

  但如果是函數的話,由於函數自身已經是天然完美封裝的了,所以如果你要用到這個函數,那么這個函數所有的依賴你都需要,這才是合理的。

可見:數據部分就是數據部分,執行部分就是執行部分,不同類的東西放在一起是不合適的!

 

問題2 是否所有的東西都需要對象化

  面向對象語言一直以自己做到"一切皆對象"為榮,但事實是:是否所有的東西都需要對象化?

  iOS開發中,有一個類叫做NSNumber,它封裝了所有數值:double,float,unsigned int, int...等等類型,在使用的時候它弱化了數值的類型,使得非常方便。但問題也來了,計算的時候是不能直接對這個對象做運算的,你得把它們拆成數值,然后進行運算,然后再把結果變成NSNumber對象,然后返回。這是第一點不合理。第二點不合理的地方在於,運算的時候你不知道原始數據的類型是什么,拆箱裝箱過程中難免會導致內存的浪費(比如原來uint8_t的數據變成unsigned int),這也十分沒有必要。

  還有就是file descriptor,它本身是一個資源的標識號,如果將資源抽象成對象,那么不可避免的就會使得這個對象變得非常龐大,資源有非常多的用法,你需要將這些函數都放到對象里去。在真正傳遞資源的時候,其實我們也只是關心資源標識而已,其它的真的無需關心。

  我們已經有函數作為黑盒了,拿着數據塞到黑盒里就夠了。

 

問題3 類型爆炸

  由於數據和函數綁定到了一起,在邏輯上有派生關系的兩種對象往往可以當作一種,以派生鏈最上端的那個對象為准。單純地看這個現象直覺上會覺得非常棒,父親有的兒子都有。但在實際工程中,派生是非常不好控制的,它導致同一類類型在工程中泛濫:ViewController、AViewController、BViewController、ThisViewController、ThatViewController...

  你會發現,一旦把執行和數據拆解開,就不需要這么多ViewController了,派生只是給對象添加屬性和方法。但事實上是這樣:

struct A {              Class A extends B
    struct B b;         {
    int number;             int number;
}                       {

  前者和后者的相同點是:在內存中,它們的數值部分的布局是一模一樣的。不同點是:前者更強烈地表達了組合,后者更強烈地表達的是繼承。而一個准則是:組合高於繼承。

  上兩者的表達在內存中沒有任何不同,但在實際開發階段中,后者會更容易把項目引入一個壞方向。


 

 

總結

為什么面向對象會如此流行?業界關於這個談論的最多的是以下幾點:

  1. 它能夠非常好地進行代碼復用
  2. 它能夠非常方便地應對復雜代碼
  3. 在進行程序設計時,面向對象更加符合程序員的直覺

第一點在理論上確實成立,但實際往往卻是在面向對象的大背景下,寫一段便於復用的代碼比面向過程背景下難多了。

關於第二點,你不覺得正是面向對象,才把工程變復雜的么?如果層次清晰,調用規范,無論面向對象還是面向過程,處理復雜業務都是一樣好,等真的到了非常復雜的時候,對象間錯綜復雜的關系只會讓你處理起來更加頭疼,不如面向過程來得簡潔。

關於第三點,這其實是一個障眼法,因為無論面向什么的設計,最終落實下來,還是要面向過程的,面向對象只是在處理調用關系時符合直覺,在架構設計時,理清需求是第一步,理清調用關系是第二步,理清實現過程是第三步。面向對象讓你在第二步時就產生了設計完成的錯覺,只有再往下落地到實現過程的時候,你才會發現第二步中都有哪些錯誤。

  綜上所述,觀點是:面向對象是在架構設計時非常好的思想,但如果只是簡單映射到程序實現上來,引入的缺點會讓我們得不償失。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM