時間屬性
隨著時間而改變的屬性

2004 年 3 月 7 日
這是 進一步的企業應用程式架構開發寫作的一部分,我在 2000 年代中期進行。遺憾的是,自那以後,太多其他事情吸引了我的注意力,所以我沒有時間進一步研究它們,我也看不到未來可預見的時間。因此,這份材料很大程度上是草稿形式,在我有時間再次研究它之前,我不會做任何更正或更新。
通常,當我們看到類別上的屬性時,它們表示我們現在可以對物件提出的問題。然而,有時我們不只是想詢問物件屬性的問題,我們還想詢問過去某個時間點的問題,當時情況可能已經改變。
它的運作方式
此模式的關鍵是提供一個規律且可預測的介面,用於處理物件中隨著時間而改變的屬性。其中最重要的部分在於存取器函式。你總是會看到一個將 時間點 作為引數的存取器函式:這允許你詢問「1998 年 2 月 2 日福勒先生的地址是什麼?」。此外,你通常會看到一個不帶引數的存取器,這是根據預設(通常是今天)詢問地址的問題。
除了存取資訊之外,您通常需要更新資訊。最簡單的修改形式是增量更新。您可以將增量更新視為在時間軸末端新增另一則資訊。增量更新可以使用單一 put 方法處理,該方法採用時間點和新值。因此,這表示「自 1998 年 8 月 23 日起,將 Fowler 先生的地址變更為 Damon 大道 15 號」。提供的日期可以是過去,表示追溯變更;現在,表示當前變更;或未來,反映預定的變更。同樣地,您可能會發現使用 put 方法很有用,而無需時間點,因為它使用今天(或類似的預設值)作為變更的生效日期。這使得進行當前變更變得容易。
另一種更新是插入更新。這是一種形式,您想要說「我們讓 Fowler 先生在 1998 年 1 月搬進富蘭克林街 963 號,但我們需要將其變更為 1997 年 12 月」。重點在於這會修改我們目前擁有的時間資訊,而不是在末端新增資訊。為此,您需要一個參考,以存取您目前擁有的時間值,然後將其調整到新的範圍。僅提供值是不夠的,因為該值可能在不同的範圍內有效:我可以搬出,然後再回到同一個地址。
舉例來說,我住在 Norwood 路 154 號從 85 年 9 月到 87 年 1 月,搬到 Brighton 六個月,然後再回來住了一段時間。如果我需要將第一次住宿的離開時間從 1 月調整到 2 月,重要的是要得到第一次住宿,而不是第二次住宿。因此,我需要地址和可以識別第一次住宿的東西 - 實際上是第一次住宿範圍內的任何日期。
如果您所擁有的值是 值物件,您實際上可以使用增量更新,儘管插入更新可能更容易使用。
實作 Temporal Property 有兩種方法。其一是使用 Effectivity 有一個物件集合,然後操作這個集合。但是,一旦你發現自己做了這件事超過一次,你就會發現最好建立一個提供這種行為的特殊集合類別:一個時間集合。這種類別很容易寫,而且可以在需要 Temporal Property 時使用。
何時使用
當你有一個類別有幾個屬性顯示時間行為,而且你想輕鬆存取那些時間值時,你應該使用 Temporal Property。
這一點的第一個重點是輕鬆存取。記錄時間變化的最簡單方法是使用 Audit Log。Audit Log 的缺點是你需要額外的工作來處理記錄檔。因此,你需要知道的第一件事是在什麼情況下人們需要該屬性的歷史記錄。請記住,將常規屬性重構為時間屬性並不困難。(你只要用時間集合取代目標欄位,就可以輕鬆維護現有的介面。)
第二個重點是要考慮有多少屬性是時間屬性。如果類別的大部分屬性都是時間屬性,那麼你將需要使用 Temporal Object。
進一步閱讀
我第一次在 [fowler-ap] 中以歷史對應的名稱描述 Temporal Property。然後,我在準備 [PLoPD 4] 中的時間模式論文時,與 Andy Carlson 和 Sharon Estepp 合作改進了我的想法。Francis Anderson 的 plop 論文 也在關聯歷史的名稱下描述了這個模式。
範例:使用時間集合 (Java)
時間收集是實作時間屬性的簡單方法。時間收集的基本表示法和介面類似於地圖:提供使用日期作為索引的取得和放入操作。事實上,地圖是其良好的後援收集。
類別 TemporalCollection...
private Map contents = new HashMap(); public Object get(MfDate when) { /** returns the value that was effective on the given date */ Iterator it = milestones().iterator(); while (it.hasNext()) { MfDate thisDate = (MfDate) it.next(); if (thisDate.before(when) || thisDate.equals(when)) return contents.get(thisDate); } throw new IllegalArgumentException("no records that early"); } public void put(MfDate at, Object item) { /** the item is valid from the supplied date onwards */ contents.put(at,item); clearMilestoneCache(); }
地圖包含依據其生效的開始日期索引的值。里程碑方法會以反向順序傳回這些金鑰。然後,取得方法會透過這些里程碑找出正確的金鑰。當您較可能要求最近的值時,此演算法最有效。
如果您存取時間收集的頻率高於更新頻率,快取里程碑收集可能是值得的。
類別 TemporalCollection...
private List _milestoneCache; private List milestones() { /** a list of all the dates where the value changed, returned in order latest first */ if (_milestoneCache == null) calculateMilestones(); return _milestoneCache; } private void calculateMilestones() { _milestoneCache = new ArrayList(contents.size()); _milestoneCache.addAll(contents.keySet()); Collections.sort(_milestoneCache, Collections.reverseOrder()); } private void clearMilestoneCache() { _milestoneCache = null; }
當您編寫時間收集時,很容易為客戶建立地址時間收集。
類別 Customer...
private TemporalCollection addresses = new SingleTemporalCollection(); public Address getAddress(MfDate date) { return (Address) addresses.get(date); } public Address getAddress() { return getAddress(MfDate.today()); } public void putAddress(MfDate date, Address value) { addresses.put(date, value); }
使用時間收集最大的問題之一是,如果您必須將收集持續存在於關聯式資料庫中,則對應到表格並非完全直接。基本上,關聯式資料庫需要使用效能。這通常表示您需要建立交集表格作為日期範圍的位置。部分此程式碼可以概括為時間收集類別,但需要一些明確的對應程式碼。
範例:實作雙時間屬性 (Java)
在思考雙時間屬性的運作方式之前,值得思考它必須做什麼。基本上,雙時間屬性允許我們儲存歷史資訊,並在兩個面向中保留完整歷程。因此,我們建立像這樣的歷程。
類別 Tester...
private Customer martin; private Address franklin = new Address ("961 Franklin St"); private Address worcester = new Address ("88 Worcester St"); public void setUp () { MfDate.setToday(new MfDate(1996,1,1)); martin = new Customer ("Martin"); martin.putAddress(new MfDate(1994, 3, 1), worcester); MfDate.setToday(new MfDate(1996,8,10)); martin.putAddress(new MfDate(1996, 7, 4), franklin); MfDate.setToday(new MfDate(2000,9,11)); }
請注意更新的節奏。當我們儲存雙時間歷程時,記錄日期永遠是今天。因此,在我們的測試中,我們會先變更目前日期,然後將資訊儲存在歷程中。當我們將資訊放入歷程中時,我們會提供實際日期。
產生的歷程如下所示。
類別 Tester...
private MfDate jul1 = new MfDate(1996, 7, 1); private MfDate jul15 = new MfDate(1996, 7, 15); private MfDate aug1 = new MfDate(1996, 8, 1); private MfDate aug10 = new MfDate(1996, 8, 10); public void testSimpleBitemporal () { assertEquals("jul1 as at aug 1", worcester, martin.getAddress(jul1, aug1)); assertEquals("jul1 as at aug 10",worcester, martin.getAddress(jul1, aug10)); assertEquals("jul1 as at now",worcester, martin.getAddress(jul1)); assertEquals("jul15 as at aug 1", worcester, martin.getAddress(jul15, aug1)); assertEquals("jul15 as at aug 10",franklin, martin.getAddress(jul15, aug10)); assertEquals("jul15 as at now",franklin, martin.getAddress(jul15)); }
正如您可以使用時間收集實作時間屬性的許多複雜性一樣,您也可以定義雙時間收集來處理雙時間屬性的許多複雜性。
基本上,雙時間收集是時間收集,其元素是時間收集。每個時間收集都是記錄歷程的圖片。
我們將首先探討如何從集合中取得資訊。最新的時間集合代表實際歷程記錄日期為現在。因此,僅有實際日期的取得方法會使用此目前的實際歷程記錄。
類別 BitemporalCollection...
private SingleTemporalCollection contents = new SingleTemporalCollection(); public BitemporalCollection() { contents.put(MfDate.today(), new SingleTemporalCollection()); } public Object get(MfDate when) { return currentValidHistory().get(when); } private SingleTemporalCollection currentValidHistory() { return (SingleTemporalCollection) contents.get(); }
(類別 SingleTemporalCollection
僅是我上面討論的香草時間集合,我稍後會說明兩者之間的關係。)
若要取得真實的雙時間值,我們會使用同時具有實際日期和記錄日期的取得器。
類別 BitemporalCollection...
public Object get(MfDate validDate, MfDate transactionDate) { return validHistoryAt(transactionDate).get(validDate); } private TemporalCollection validHistoryAt(MfDate transactionDate) { return (TemporalCollection) contents.get(transactionDate); }
我們接著可以在網域類別中使用雙時間集合。
類別 Customer...
BitemporalCollection addresses = new BitemporalCollection(); public Address getAddress(MfDate actualDate) { return (Address) addresses.get(actualDate); } public Address getAddress(MfDate actualDate, MfDate recordDate) { return (Address) addresses.get(actualDate, recordDate); } public Address getAddress() { return (Address) addresses.get(); }
每次更新集合時,我們都需要保留實際歷程記錄的舊副本。
類別 BitemporalCollection...
public void put(MfDate validDate, Object item) { contents.put(MfDate.today(), currentValidHistory().copy()); currentValidHistory().put(validDate,item); } public void put(Object item) { put(MfDate.today(),item); }
雙時間集合支援與一維時間集合非常類似的介面。因此,我們可以為時間集合建立介面,並為一維和二維集合提供個別的實作。這讓雙時間集合可以替換一維時間集合,讓將一維時間屬性重構為雙時間屬性變得容易。
在這個案例中,我使用 時間點,實際日期和記錄日期都具有日期粒度。雖然這讓範例寫起來更簡單,但最好為記錄時間使用更精細的粒度。問題在於,如果你在下午 2 點修改記錄,下午 3 點執行計費程序,然後在下午 4 點再次修改記錄。使用目前的實作,適當的值雖然不會完全遺失,但不容易取得。另一個方法(商業上經常使用)是僅在營業日結束時執行計費等工作,且在計費完成後不再允許在當天進行任何更新。計費後更新會取得計費完成後一天的記錄日期。同樣地,這種功能可以輕鬆新增到適當的時間集合類別。