去model化和資料物件

發表於2016-05-11

 

簡述

去model化這個說法其實有點兒難聽,model化就是使用資料物件,去model化就是不使用資料物件。所以這篇文章主要討論的問題就是:資料傳遞時,是否要採用資料物件?這裡的資料傳遞並不是說類似RPC的場景,而是在單個工程內部,各物件之間、各元件之間、各層之間的資料傳遞。

所謂資料物件,就是把不同型別的資料對映到不同型別的物件上,這個物件僅用於表達資料,資料通過物件的property來體現。瘦Model、貧血模型就屬於這一類。

去Model化,就是不使用特定物件迎合特定資料的對映的方式,來表達資料。比如我們可以使用NSDictionary,或者其他手段例如reformer、virtual record,來避免這種資料對映物件。

關於這個問題的討論涉及以下內容:

  • 如何理解物件導向思想
  • 為什麼不使用資料物件
  • 去Model化都有哪些手段

通過以上三點,我希望能夠幫助大家建立對物件導向的正確理解,讓大家明白如何權衡是否要採用物件化的設計。以及最終當你決定不採用物件化思想而採用非物件化思想時,應該如何進行架構設計。

如何理解物件導向思想

物件導向的思想簡單總結一下就是:將一個或多個複雜功能封裝成為一個聚合體,這個聚合體經過抽象後,僅暴露少部分方法,這些方法向外部獲取實現功能所需要的條件後,就能完成對應功能。傳統的程式導向只針對功能的實現做了封裝,也就是函式。經過這層封裝後,僅暴露引數列表和函式名,用於外部呼叫者呼叫並完成功能。

我們可以推匯出:函式封裝了實現功能所需要的程式碼,因此物件實質上就是再次對函式進行了封裝。將函式集合在一起,形成一個函式集合,物件導向思想的提出者把這個函式集合稱之為物件,把物件的概念從理論對映到實際的工程領域,我們也可以叫它

然而我們很快就能發覺,只是單純地把函式集合在一起是不夠的,這些函式集有可能互相之間需要共用引數或共享狀態。因此物件導向的理論設計者讓物件自己也能夠提供屬性(property),來滿足函式集間共用引數和共享狀態的需求。這個函式集現在有了更貼切的說法:領域。因此當這個領域中的個別函式不需要共用引數或共享狀態,僅僅是提供功能時,這些相關函式就可以體現為類方法。當領域裡的函式需要共用引數或共享狀態時,這些函式的體現就是例項方法。

這裡補充一下,領域的概念我們更多會把它理解得比較大,比如多個相關物件形成一個領域。但一個物件自身所包含的所有函式也是一個領域,是大領域裡的一個子領域。

以上就是一個對物件導向思想的樸素理解。在這個理解的基礎上,還衍生出了非常多的概念,不過這並不在本文的討論範圍中。

總之,一個物件事實上是對一個較小領域的一種封裝。對應到本文要討論的問題來看,如果拿一個物件去表達一套資料而非一個領域,這在一定程度上是違背物件導向的設計初衷的。你看著好像是把資料物件化了,就是物件導向程式設計了,然而事實上並非如此。Martin Fowler早年也在他的《Anemic Domain Model》中提出了一樣的看法:

The fundamental horror of this anti-pattern is that it’s so contrary to the basic idea of object-oriented design; which is to combine data and process together. The anemic domain model is really just a procedural style design, exactly the kind of thing that object bigots like me (and Eric) have been fighting since our early days in Smalltalk. What’s worse, many people think that anemic objects are real objects, and thus completely miss the point of what object-oriented design is all about.

為什麼不使用資料物件

根據上一小節的內容,我們可以把物件抽象地理解為一個函式集,一個領域。在這一節裡,我們再進一步推導:如果這些函式集裡的所有函式,並不都是處在同一個問題領域內,那麼物件導向的這種實踐是否依舊成立?

答案是成立的,但顯然我們並不建議這麼做。不同領域的函式集如果被封裝在了一起,在實際工程中,這種做法至少會帶來以下問題:

  1. 當需要維護某個問題領域內的函式時,如果修改到某個需要被共用的引數或者需要被共享的物件,那麼其他問題領域也存在被影響的可能。牽一髮而動全身,這是我們不希望看到的。
  2. 當需要解決某個問題時,如果引入了某個充滿了各種不同問題領域的函式集,這實質就是引入了對不同問題領域解決方案的依賴。當需要做程式碼遷移或複用時,就也要把不相關的解決方案一併引入。拔出蘿蔔帶出泥,這也是我們不希望看到的。

當需要維護某個問題領域內的函式時,如果修改到某個需要被共用的引數或者需要被共享的物件,那麼其他問題領域也存在被影響的可能。牽一髮而動全身,這是我們不希望看到的。

我們在進行物件化設計時,必須要分割好問題域,才能保證設計出良好的架構。

業界所謂的各種XX建模、XX驅動設計、XXP,大部分其實都是在強調合理分割這一點,他們提供不同的方法論去告訴你應該如何去做分割的事情,以及如何把分割出來的部分再進一步做封裝。然而這些XX概念要成立的話,就都一定需要具備這樣一個前提條件:一個被封裝出來的物件的活動領域,必須要小於等於當前被分割出來的子問題領域。如果不符合這個前提條件的話,一個大的問題領域即使被強行分割成各種小的問題領域,這些小的問題領域還是依舊難以被封裝成為物件,因為物件的跨領域活動勢必就要引入其它領域的問題解決方案,這就使得分割名不副實。

然而,一個被封裝出來的物件的活動領域,必須要小於等於當前被分割出來的子問題領域這個前提在實際業務場景實踐中,是否一定成立呢?如果一定成立的話,那麼這種做法和這些方法論就是沒問題的。如果在某些場景中不成立,物件化設計在這些場景就有問題了。

事實上,這個前提在實際業務場景中,是不一定成立的。在實際業務場景中,一個資料物件被多個業務領域使用是非常常見的。一個資料物件在不同層、不同模組中被使用也是非常常見的。所以,如果兩個業務物件之間需要傳遞的僅是資料,在這個場景下就不適合傳遞物件化的資料。

當需要解決某個問題時,如果引入了某個充滿了各種不同問題領域的函式集,這實質就是引入了對不同問題領域解決方案的依賴。當需要做程式碼遷移或複用時,就也要把不相關的解決方案一併引入。拔出蘿蔔帶出泥,這也是我們不希望看到的。

這種場景其實就很好理解了。實際工程中,物件化資料往往不是一個獨立存在的物件,而是依附於某一個領域。例如持久層提供的物件化資料,往往依附於持久層。網路層提供的物件化資料往往依附於網路層。當你的業務層某個模組使用來自這些層的物件化資料時,將來要遷移這個模組,就必須不得不把持久層或者網路層也跟著遷移過去。遷移發生的場景之一就是大型工程的元件化拆分,實施元件化時遇到這種問題是一件非常傷腦筋的事情。

小結

所以,在資料傳遞時,我不建議採用物件化設計,尤其是資料傳遞的兩個實體是跨層實體或者跨模組實體時,物件化設計對架構的傷害非常大。

從實際而非理論的角度上講,資料物件的使用主要存在這些問題:

  1. 資料物件並不符合物件導向的設計初衷
  2. 資料物件有變為支援多領域物件的可能
  3. 資料物件使得領域間依賴性變強

去Model化都有哪些手段

字典流

這種做法是最原始最簡單的做法,我就不多說了。

 

 

reformer

reformer是這樣的工作原理:

APIManager提供了來自網路層的資料。Reformer_A,Reformer_B,Reformer_C,分別代表不同的領域。View_A,View_B,View_C,分別就是各領域對不同的資料應用之後產生的結果。在講網路層的文章中,我設計了reformer的方式來實現非物件化。更詳細的講述和實際的Demo文章裡都有,我在這裡就不多說了。

Virtual Record

Virtual Record事實上把reformer和某個領域相關物件集合在了一起。Virtual Record和reformer的區別在於:reformer更加有利於單資料對應多物件的場景,Virtual Record更加有利於多資料對單物件的場景

 

事實上這幅圖有個地方畫的不太貼切,Virtual Record其實只是View_B的一個protocol,它並不是一個例項,所以才Virtual。關於Virtual Record的詳細解釋和案例,在講持久層的文章裡有。

總結

將資料物件化事實上是一個不符合物件導向思想的做法。

這種說法看起來很反直覺,但事實上如果你對物件導向有深入的理解,就能夠明白其中的原因。這種不符合物件導向思想的做法,也導致了工程實踐上程式碼的高耦合和元件難以複用的情況,這都是我們不希望看到的。我在這篇文章裡提供了幾種去Model化的做法,但看起來這應該不是所有的手段,很有可能還有其它方法。未來如果我遇到了其他場景想到了其它方法的話,會對它進行補充。如果各位讀者還有不同的方法或其它的問題,也歡迎在評論區一起交流。

 

 

相關文章