值對象(value object)===================== 什麼是值對象 維基百科的定義 In computer science, a value object is a small object that represents a simple entity whose equ....
值對象(value object)
什麼是值對象
維基百科的定義
In computer science, a value object is a small object that represents a simple entity whose equality is not based on identity
在電腦科學中,值對象是一種小型對象,它表示一個簡單的實體,該實體的相等性不取決於標識。
我們身邊就有一堆“值對象”,比如你現在從錢包掏一張紙幣,它就是典型的值對象,紙幣存在的意義在於表示一個價值。我們區分紙幣時並不是根據紙幣上那個唯一的編號,而是根據它表示的面值,具有相同面值的紙幣就是等價的,是可互換的。
這裡紙幣的編號就對應程式中對象的標識,紙幣的面值就對應程式中對象表示的值。
其它值對象的例子:ip地址、rgb顏色、地理坐標。
我的解理就是:值對象是用來表示一個量、一個值、一個信息的對象,它的狀態是靜態的;我們使用值對象時,關心的是它所表示的信息,而不是這個對象本身
值對象與不可變性(immutability)
值對象通常都會被設計成不可變的(immutable),比如.NET基類庫中的Int32、DateTime這些類型都是不可變的。
可能有人會對下麵的C#代碼提出疑問:
Int32 i = 1;
i = 2;
咦!這不是可變的嗎?註意這裡i是一個l-value(左值)
,是一個存放Int32類型值的變數,是線程棧上一個記憶體塊,是一個容器,相當於是錢包;下一句中只是把該錢包中的紙幣換了,並沒有修改紙幣的面值。像某日期.月份 = 12
, 某整數.符號 = 負
才叫修改了對象。
為什麼說值對象需要是不可變的
主要有以下原因:
- 從理論上講值對象存在的意義就在於表示一個值,如果狀態修改了,那就是一個新的值,應該用新的對象表示。
簡化編程、避免bug
如果值對象可變,當它被共用時,一處修改可能會影響另一處,修改操作就會產生“副作用(side effect)”,如java中的java.util.Date是引用類型並且是可變的,下麵的代碼演示了使用它時可能犯的錯誤://表示一個定時任務 class Task{ //設置執行時間 public Date setStartDate(Date date){ this.startDate = date; } //獲取執行時間 public Date getStartDate(){ return this.startDate; } //FIXME: 改成 return this.startDate.clone(); //延遲執行時間 void delay(int delayDays) { this.startDate.setDate(startDate.getDate() + delayDays); } private Date startDate;//執行時間 } task1.setStartDate(new Date("2016/01/01"); task2.setStartDate(task1.getTaskDate()); task2.delay(5); //這裡本想修改task2的日期,但無意中影響到了task1
這裡的問題在於我們關心的其實只是一個“日期值”而不是一個
java.util.Date
對象,所以在傳遞時應該是傳遞表示的日期值,而不是直接傳遞對象。註釋中的FIXME標註是一種解決辦法。
值對象必須要實現為不可變嗎?
但是如果把一個類實現為不可變的話,意味著修改一個成員就要創建一個新的對象,如果成員非常多的話,代碼寫起來會比較繁瑣。
有些語言支持值語義(value semantics),所謂“值語義”是指對象傳遞時是傳遞的它所表示的值(傳值),而不是傳遞對象本身(傳引用),這就意味著傳遞過程就是“複製”,比如C++預設就是值語義、C#中的struct等,因為是複製,所以值對象不會被共用,也就沒有了上面提到的那個問題,這種情況下,值對象可變的話也是完全可以的,但是值對象仍可以被“按引用”傳遞,所以把值對象實現為不可變是最保險的手段。
值對象的實現
- C++
C++預設就是值語義,如果類狀態較簡單,則可以不做任何特殊處理。如果比較複雜,比如成員是指針或引用,則就要實現生命周期管理函數 - C#
- 如果類狀態較簡單,可直接用struct,因為struct正好支持值語義
- 如果狀態較複雜則用class,並且從介面上把它設計成不可變,比如:public readonly的屬性,提供一個能夠初始化所有成員的構造方法 或 提供修改狀態的方法,但實際不修改本對象的狀態,而是構建並返回一個具有的新狀態的新對象。
- java
同C#中的class
其它
struct與class
不管是C++和C#中的struct關鍵字,還是ruby中的Struct::new都是趨向用於定義簡單的複合類型,所以struct適合用來定義沒有複雜行為和狀態的值對象。而class更趨向用於定義具有豐富邏輯、複雜狀態的對象類型,struct、class和值對象、引用對象並不是一一對應的關係。
特殊的值對象 - 字元串
在我看來字元串是值對象,理由很簡單:它的相等性取決於它的狀態
雖然它本質是一個字元容器,但我們更多的是用它來進行比較,輸出等等,而不是對裡面的字元進行“增刪改查”,所以從它的用途來看,它是一個靜態的字元序列。
那麼問題來了,為什麼在C#和java中字元串都是引用類型呢?
現階段我是這麼認為的:
- “值類型”,“引用類型”這些只是語言/平臺實現中的術語,“值對象”、“引用對象”是語言無關的,是對象設計思想,所以不是說被實現為“引用類型”它就不是值對象了
- 字元串本質上是字元容器,大小不定,不像Int32固定占4位元組,且無法在編譯時確定(當然字面量除外),如
String str = new String('a', 10)
,因此無法分配於棧上
雖然在C#中也完全可以把字元串實現為struct,在內部動態分配字元數組,但由於這樣內部實現會比較複雜,而struct更傾向於用來定義簡單的複合類型。
DTO(Data transfer object)
它和值對象的區別在於:值對象可以是一個domain model,可以擁有邏輯;而DTO被用於打包其它對象的狀態,在layer間傳遞,是一種貧血模型,它沒有邏輯。
引用對象(reference object)
什麼是引用對象?
引用對象是相等性取決於它的標識的一種對象。
為什麼要“相等性取決於標識”?因為在使用引用對象時,我們關心的是這個對象本身,在傳遞過程中,需要傳遞同一個對象,因此就把對象的“引用(即標識、通常是記憶體地址)”傳遞過去,這也就是“引用語義(reference semantics)”。
我們實際寫程式中,使用對象的目的在於映射問題域中的事物,因此基本關心的是對象本身,所以使用引用對象會相對比較多。
實現
- 在C#和java中用class定義的類型叫“引用類型”,具有引用語義,創建的對象天生就是引用對象
- 在C/C++中通常通過動態分配記憶體和傳遞指針實現
指針 與 引用類型
- C/C++中的指針其實是一個unsigned int,表示一個記憶體地址。具體類型的指針類型如
int *
,類型名的用途在於解引用時知道數據的大小和數據表示的含義,我們可以用“強制類型轉換”以不同的大小和類型解讀一塊記憶體,非常靈活,但也容易出錯。 - C#和java中的引用實際上是一個受限的、托管的、安全的指針,因為記憶體管理被GC接管了,它會釋放記憶體、整理記憶體碎片(就可能會移動分配在堆上的對象),所以就禁止開發人員通過這個指針獲取記憶體地址、計算偏移、釋放記憶體等,以防出錯,只能用它來“引用”托管堆上的對象。