時間屬性

隨著時間而改變的屬性

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 LogAudit 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 點再次修改記錄。使用目前的實作,適當的值雖然不會完全遺失,但不容易取得。另一個方法(商業上經常使用)是僅在營業日結束時執行計費等工作,且在計費完成後不再允許在當天進行任何更新。計費後更新會取得計費完成後一天的記錄日期。同樣地,這種功能可以輕鬆新增到適當的時間集合類別。