時間物件
隨著時間而變化的物件

2004 年 3 月 7 日
這是 進階企業應用程式架構開發 系列文章的一部分,我於 2000 年代中期撰寫。遺憾的是,自那時起,太多其他事情吸引了我的注意力,所以我沒有時間進一步研究它們,我也看不到未來會有太多時間。因此,這份文件仍處於草稿階段,在我有時間再次研究它之前,我不會進行任何更正或更新。
有時你會認為一個物件具有時間屬性,但其他時候你會認為物件本身就是時間性的。一個很好的例子是一份經過一系列修正的合約。你可以將每個修正案視為一份具有新條款的新合約,但你也可以將它們視為同一份合約的不同版本。
運作方式
這種模式非常常見,但會以多種形式出現。為了應對這些形式,我發現使用一些角色分析非常有幫助。此模式基本上有兩個角色:一個是連續性,另一個是多個版本。
每個版本都會擷取物件在某段時間內的狀態。任何時候物件的任何屬性的值發生變化,你就會得到一個新版本。因此,你可以將版本想像成一個物件清單,其中包含一個 效期 來處理日期範圍。
連續性代表持續經歷這些版本變更的進行中物件。當人們在思考物件及其所有變更時,他們指的是這個物件。
除了版本的暫時屬性之外,連續性要不沒有資料,要不只有在連續性的整個生命週期中真正不變的資料。然而,它作為其他物件收集資料的點。目前值可以使用委派給目前版本的取得方法輕鬆存取。歷史值可以用兩種方式取得。連續性可以為版本的屬性提供暫時屬性,或者連續性可以提供快照。你可以混合兩者,也許為常見值提供暫時屬性介面,並為其餘部分提供 快照。
如果你希望使用者明確變更版本記錄,你也可以提供對版本的直接存取權限以進行編輯。然而,你通常會希望透過連續性控制所有存取權限。
使此模式如此難以處理的原因在於你有很多方法可以對其建模。
考慮我有一份信用卡合約。我在 1997 年 2 月 1 日取得這張信用卡,它附有一份難以理解的合約協議,我將其歸檔在檔案櫃中,從此不再過問。1998 年 4 月 15 日,一份經修訂的協議透過郵件寄來,並獲得相同的處理方式。因此,您有一張信用卡和兩份(版本的)合約。
一種方式是將每份合約視為與信用卡綁定的獨立合約。在許多方面,這根本不涉及時序物件。相反地,您考慮的是明確地讓信用卡具備合約的時序屬性。

圖 1:將每張信用卡視為獨立合約
第二個選項是說信用卡有一份合約,而合約本身有一組合約版本。這種方式與將每份合約視為獨立事物之間的差異,在於企業觀看其業務流程的方式。一個優點是,如果一家公司有客戶使用不同的合約,則合約物件是一個明確的地方,可以懸掛該概念。當然,一家公司可能針對此目的有其他概念,例如信用卡類型(金卡、白金卡和基本金屬卡),並且每種類型只有一個合約。在這種情況下,合約物件的價值(至少在表示方面)會降低;但行為中仍然有價值。

圖 2:具有明確版本的合約
圖 2是時序物件模式最明顯的形式,因為模式中的兩個角色都有明確的類別。然而,當一個物件同時扮演連續性和其中一個版本時,事情就會變得不那麼明確。

圖 3:具有修正案的合約 - 類別圖。
一個很好的例子是圖 3中的模型,其中我們有一個合約,其中有一個修正案,而該修正案是一個合約,因此可以有它自己的修正案。在這種情況下,合約類別同時扮演連續性和版本的角色,通常的概念是只有鏈中的第一個合約扮演連續性的角色。這樣,任何引用原始合約的事物都有明確的參考點,但我們仍然可以使用版本來保存變更。
當合約很少修改時,圖 3 的修改樣式很有用,因為這樣只有一份合約,而 圖 2 的明確樣式即使沒有修改,也總是需要至少兩個物件。儘管如此,我現在傾向於一直使用明確形式,因為責任劃分得更清楚。此外,明確形式更適合使用時間集合來實作,而我發現這比遍歷清單容易得多。(雖然公平地說,你可以使用時間集合來修改,而不是更常使用的清單形式。)
要記住的另一個面向是,連續性通常不會由物件表示,特別是在較不物件導向的系統中。例如,在關聯式資料庫中,你可能只有一個版本表格,而沒有連續性表格。在這種關聯模式中,連續性是由欄位實作的,例如合約編號。合約表格的主鍵會結合這個合約編號和 效期 的一部分,例如開始日期。
另一個簡單的範例是原始碼控制系統。這裡的連續性是版本化檔案的檔名,每個版本都儲存在原始碼控制系統中,通常是作為 delta,作為一個獨立的項目。
何時使用
何時使用此模式的最大問題是將其與使用 時間屬性 進行比較。兩者有很大的重疊,事實上,你可以看到連續性的介面通常是一組時間屬性。確實,就客戶而言,讓每個屬性都是 時間屬性 和使用 時間物件 之間的介面幾乎相同。
一個明顯的驅動力是時間屬性的比例。如果只有幾個,則使用 時間屬性,如果大部分都是,則使用 時間物件。當然,這只是意味著我將判斷少數和多數之間的差異留給你 - 這不是令人惱火嗎?
另一個問題是企業人士如何查看資訊。如果他們想考慮一個具有明確修改的聯絡人,那麼即使只有一個屬性是時間屬性,也值得使用 時間物件。一旦企業人士需要明確地參照版本,你就需要 時間物件 來提供他們要參照的版本。
進一步閱讀
安德森的 Plop 論文 在歷史自我的名稱下討論此模式。 [Arnoldi 等人] 在版本歷史的名稱下描述此模式
範例:明確連續性和版本(Java)
對於範例程式碼,我將遵循 圖 2 的明確形式。客戶版本類別包含您預期會在客戶身上看到的資料。
類別 CustomerVersion...
private String address; private Money creditLimit; private String phone; String address() {return address;} Money creditLimit() {return creditLimit;} String phone() {return phone;} void setName(String arg) {_name = arg;} void setAddress(String arg) {address = arg;} void setCreditLimit(Money arg) {creditLimit = arg;}
客戶類別包含客戶版本的暫時性集合(請參閱 暫時性屬性 以了解其運作方式)及其簡單的取得方法委派給最新版本。
類別 Customer...
private TemporalCollection history = new SingleTemporalCollection(); public String name() {return current().name();} public String address() {return current().address();} public Money creditLimit() {return current().creditLimit();} public String phone() {return current().phone();} private CustomerVersion current() { return (CustomerVersion)history.get(); }
在更新客戶時,您必須考量您希望版本有多明確。如果您想要的只是一個簡單的目前附加更新,您可以提供一個看起來正常的設定方法。此設定方法會複製版本,更新複製版本,然後將複製版本新增至歷程記錄。
類別 Customer...
public void setAddress(String arg) { CustomerVersion workingCopy = getWorkingCopy(); workingCopy.setAddress(arg); history.put(workingCopy); } public CustomerVersion getWorkingCopy() { return current().copy(); }
類別 CustomerVersion...
CustomerVersion copy() { return new CustomerVersion(_name, address, phone, creditLimit); } public CustomerVersion (String name, String address, String phone, Money creditLimit) { super(name); this.address = address; this.phone = phone; this.creditLimit = creditLimit; }
這讓您可以使用簡單的設定方法對暫時性記錄進行變更。
類別 Tester...
public void testSimple () { MfDate.setToday(new MfDate (1998, 8, 23)); martin.setAddress(Damon15); martin.setCreditLimit(Money.dollars(100)); MfDate.setToday(new MfDate (2000, 9,30)); assertAddresses(); assertCreditLimits(); } private void assertCreditLimits() { assertEquals(Money.dollars(50), martin.creditLimit(new MfDate(1997, 12, 25))); assertEquals(Money.dollars(50), martin.creditLimit(new MfDate(1998, 8, 22))); assertEquals(Money.dollars(100), martin.creditLimit(new MfDate(1998, 8, 23))); assertEquals(Money.dollars(100), martin.creditLimit()); } private void assertAddresses() { assertEquals(Franklin963, martin.address(new MfDate(1997, 12, 25))); assertEquals(Franklin963, martin.address(new MfDate(1998, 8, 22))); assertEquals(Damon15, martin.address(new MfDate(1998, 8, 23))); assertEquals(Damon15, martin.address()); }
但是,對於追溯更新,客戶的客戶端需要知道版本。因此,追溯更新會由像這樣的客戶端執行。
類別 Tester...
public void testWorkingCopy() { MfDate.setToday(new MfDate (2000, 9,30)); CustomerVersion workingCopy = martin.getWorkingCopy(); workingCopy.setAddress(Damon15); workingCopy.setCreditLimit(Money.dollars(100)); martin.addVersion(new MfDate (1998, 8, 23), workingCopy); MfDate.setToday(new MfDate (2000, 9,30)); assertAddresses(); assertCreditLimits(); }