[記]SAF 中快取服務的實現

farsun發表於2021-09-09

SAF 中快取服務的實現

概述

本文是《Developing Application Frameworks in .NET》的讀書筆記。SAF 是書中的一個範例框架,意為 Simple Application Framework(簡單應用程式框架),透過這篇文章,我主要想向大家說明 SAF 中快取服務的實現方式。由於新增了大量註釋,所以程式碼部分的講述相對比較少。

設計思想

我們經常需要在應用程式中快取一些常用資料供全域性使用以提升效能。如果需要快取的物件型別和數目是固定的,我們可能會直接將其宣告為static;如果我們需要快取的物件型別和數目是不定的,我們可能會藉助一個static Hashtable來實現。但是Hashtable有個缺陷:它沒有層次結構,它總是以鍵/值的形式來儲存資料,一個Key對應一個Value,如果我們想獲取相關聯的一組資料就會比較困難了。

NOTE:如果你從事Asp.Net的開發,提起快取你可能首先會想到Output Cache、資料來源快取或者是基於System.Web.Caching.Cache的物件快取。實際上快取的目的就是把物件(資料)儲存在記憶體中,不用每次需要物件服務的時候都重新建立物件(相對耗時)。將物件宣告為static,那麼物件將在其所屬的類被載入AppDomain時初始化,這樣物件的生命週期與AppDomain同樣長,從而起到快取的目的。
    感興趣的朋友可以做個測試:在站點下新建一個Default.aspx檔案,在後置程式碼中新增如下程式碼:

public class Test {
      public static DateTime a = DateTime.Now;
      public DateTime b = DateTime.Now;
}

protected void Page_Load(object sender, EventArgs e) {
      Test t = new Test();
      Label1.Text = Test.a.ToString() + "
"; //Label1為頁面上的一個Label控制元件
      Label1.Text += t.b.ToString();      
}

結果是隻要站點不重啟(程式碼也不修改),那麼a的值是恆定不變的,即使將頁面關了重新開啟也一樣,可見a只在Test類載入到AppDomain中進行了一次初始化。而b在每次重新整理時都會改變,因為每次請求頁面都會在建立Test型別例項時重新對a進行初始化。

NOTE:宣告為靜態(static)的一個特例是宣告為const,這是因為const天生就是static的。但它的侷限性是物件的型別必須為諸如int或者string的簡單型別。除此以外,宣告為const的物件將不再是變數,而是一個常量。例如const a = "abc"; 相當於給string型別的字串"abc"起了個別名叫a。因此const必須在宣告時就賦值。

XML文件結構是樹形的,具有標準的層次結構。XPath用於從Xml文件中選擇一個或多個結點。比如 "/BookStore/Book",選擇Book結點下的所有子結點。

SAF 中的快取服務透過一個在記憶體中動態構造的Xml文件樹作為橋樑,將 靜態(static)快取XPath 這兩個技術結合了起來,支援使用XPath的語法來獲取Hashtable中物件。其中靜態快取進行實際的資料快取,XPath用於獲取資料物件。從程式設計師的角度來看,即是Hashtable的Key支援了XPath的語法,可以將原本“平板式”的Hashtable想象成為一個“樹形結構”,它的結點包含了快取的資料,我們透過標準的XPath到達結點(當然這只是一個假象)並獲取資料。透過這種方式就可以使用XPath來一次獲取Hashtable中的多個相關資料物件。

而實際上是怎麼實現這一過程的呢?我們一步步來看:

  1. 首先在記憶體中動態構建一個 Xml文件,它只包含一個根結點,可以任意命名,這裡將它命名為了Cache。

  2. 提供一個Xpath路徑:獲取物件(資料)前首先要儲存物件,存物件自然要先提供一個路徑(這裡稱為“路徑”,是因為它是一個XPath,實際上也就相當於Hashtable中的鍵Key)。

  3. 根據上一步提供的路徑,以Cache為根結點,逐層深入地建立XmlNode結點。

  4. 生成一個GUID,在葉結點上新增一個Key屬性,為這個Key屬性賦值為GUID。

  5. 在Hashtable中儲存物件,其中Hashtable的Key即為上一步生成的GUID,而Value為要儲存的物件。

使用這種方式,Hashtable的實際的Key,即動態生成的GUID對程式設計師來說是透明的,程式設計師在儲存/獲取物件時,只需要提供XPath表示式就可以。下面這幅圖說明了它們之間的關係:

圖片描述

這裡還需要再說明三點:

  • 我們使用Hashtable儲存物件,可以直接將Hashtable宣告為static的,也可以將Hashtable宣告為instance的,但是將Hashtable所屬的物件宣告為static的。這裡應用了Singleton模式,先將對Hashtable的操作封裝成一個類,然後在這個類上應用Singleton模式,確保了這個類只有一個(這個類所維護的Hashtable例項自然也只有一個了)。很明顯,這個類包含了主要的邏輯,我們將之命名為Cache。

  • 使用Hashtable的好處是可以儲存任何型別的物件,缺點是喪失了型別安全。有時候我們可能會想使用一個泛型集合類來取代Hashtable,比如Dictionary。所以這裡又引入了Strategy模式,建立了一個ICacheStrategy介面,這個介面包括三個方法,分別用於新增、獲取、刪除物件。

  • 用Xpath獲取結點時,可以是基於當前結點的相對路徑;也可以是基於根結點的絕對路徑。在本文的範例程式中,使用的是絕對路徑,顯然這樣更加方便一些。

型別介面

我們先看一下型別的組織,然後再看實現。

ICacheStrategy用於定義如何新增、獲取、刪除欲進行快取的物件。實際上,在介面的實體類中要明確使用何種型別來儲存物件,是Dictionary還是Hashtable或者其他。

public interface ICacheStrategy {  
    void AddItem(string key, object obj);// 新增物件
    object GetItem(string key);      // 獲取物件
    void RemoveItem(string key); // 刪除物件
}

接下來是Cache類,這個類包含了主要的邏輯,包括 動態構建的XML文件、將Xml文件對映到Hashtable 等。

public class Cache {
    void AddItem(string xpath, object obj);
    object GetItem(string xpath);
    object[] GetList(string xpath);
    void RemoveItem(string xpath);
}

僅從介面上看,這個類似乎和ICacheStrategy的沒有太大分別,實際上,這個類儲存了一個對於ICacheStrategy型別例項的引用,最後一步的實際工作,都委託給了ICacheStrategy去完成。而在此之前各個方法的工作主要是由 Xml結點到Hashtable的對映(這裡說是Hashtable,是因為它是作者提供的一個預設實現,當然也可以是其他)。

型別實現

我們首先看DefaultCacheStrategy,它實現了ICacheStrategy介面,並使用Hashtable儲存物件。

public class DefaultCacheStrategy : ICacheStrategy {
    private Hashtable objectStore;

    public DefaultCacheStrategy() {
       objectStore = new Hashtable();
    }

    public void AddItem(string key, object obj) {
       objectStore.Add(key, obj);
    }

    public object GetItem(string key) {
       return objectStore[key];
    }

    public void RemoveItem(string key) {
       objectStore.Remove(key);
    }
}

接下來我們一步步地看Cache類的實現,下面是Cache類的欄位以及建構函式(注意為私有)。

public class Cache {
    private XmlElement rootMap;             // 動態構建的 Xml文件 的根結點
    private ICacheStrategy cacheStrategy;   // 儲存對ICacheStrategy的引用
    public static readonly Cache Instance = new Cache();  // 實現Singleton模式
    private XmlDocument doc = new XmlDocument();   // 構建 Xml文件

    // 私有建構函式,用來實現Singleton模式
    private Cache() {
       // 這裡應用了Strategy模式。
       // 改進:可以將使用何種Strategy定義到app.config中,然後使用反射來動態建立型別
       cacheStrategy = new DefaultCacheStrategy();

       // 建立文件根結點,用於對映 實際的資料儲存(例如Hashtable) 和 Xml文件
       rootMap = doc.CreateElement("Cache");
      
       // 新增根結點
       doc.AppendChild(rootMap);
    }
    // 略...
}

Cache類還包含兩個私有方法。PreparePath()用於對輸入的Xpath進行格式化,使其以建構函式中建立的根節點("Cache")作為根結點(這樣做是可以使你在新增/獲取物件時免去寫根結點的麻煩);CreateNode() 用於根據XPath逐層深入地建立Xml結點。

// 根據 XPath 建立一個結點
private XmlNode CreateNode(string xpath) {

    string[] xpathArray = xpath.Split('/');
    string nodePath = "";

    // 父節點初始化
    XmlNode parentNode = (XmlNode)rootMap; 

    // 逐層深入 XPath 各層級,如果結點不存在則建立
    // 比如 /DvdStore/Dvd/NoOneLivesForever
    for (int i = 1; i       XmlNode node = rootMap.SelectSingleNode(nodePath + "/" + xpathArray[i]);

       if (node == null) {
           XmlElement newElement = rootMap.OwnerDocument.CreateElement(xpathArray[i]);   // 建立結點
           parentNode.AppendChild(newElement);
       }

       // 建立新路徑,更新父節點,進入下一級
       nodePath = nodePath + "/" + xpathArray[i];
       parentNode = rootMap.SelectSingleNode(nodePath);
    }

    return parentNode;
}

// 構建 XPath,使其以 /Cache 為根結點,並清除多於的"/"字元
private string PrepareXPath(string xpath) {
    string[] xpathArray = xpath.Split('/');
    xpath = "/Cache";     // 這裡的名稱需與建構函式中建立的根結點名稱對應
    foreach (string s in xpathArray) {
       if (s != "") {
           xpath += "/" + s;
       }
    }
    return xpath;
}

AddItem()方法用於向快取中新增物件,包括了下面幾個步驟:

  1. 根據輸入的XPath判斷到達 葉結點 的路徑是否已經存在,如果不存在,呼叫上面的CreateNode()方法,逐層建立結點。

  2. 生成GUID,在組結點下建立 XmlNode 葉結點,為葉結點新增屬性Key,並將值設為GUID。

  3. 將物件儲存至實際的位置,預設實現是一個Hashtable,透過呼叫ICacheStrategy.AddItem()方法來完成,並將Hashtable的Key設定為GUID。

NOTE:為了說明方便,這裡有一個我對一類結點的命名--“組結點”。假設有XPath路徑:/Cache/BookStore/Book/Title,那麼/Cache/BookStore/Book即為“組結點”,稱其為“組結點”,是因為其下可包含多個葉結點,比如 /Cache/BookStore/Book/Author 包含了葉結點 Author;而/Cache/BookStore/Book/Title 中的Title為葉結點,GUID儲存在葉結點的屬性中。需要注意 組結點 和 葉結點是相對的,對於路徑 /Cache/BookStore/Book 來說,它的組結點就是“/Cache/BookStore”,而 Book是它的葉結點。

下面是AddItem()方法的完整程式碼:

// 新增物件,物件實際上還是新增到ICacheStrategy指定的儲存位置,
// 動態建立的 Xml 結點僅儲存了物件的Id(key),用於對映兩者間的關係
public virtual void AddItem(string xpath, object obj) {

    // 獲取 Xpath,例如 /Cache/BookStore/Book/Title
    string newXpath = PrepareXPath(xpath);

    int separator = newXpath.LastIndexOf("/");

    // 獲取組結點的層疊順序 ,例如 /Cache/BookStore/Book
    string group = newXpath.Substring(0, separator);

    // 獲取葉結點名稱,例如 Title
    string element = newXpath.Substring(separator + 1);

    // 獲取組結點
    XmlNode groupNode = rootMap.SelectSingleNode(group);

    // 如果組結點不存在,建立之
    if (groupNode == null) {
       lock (this) {
           groupNode = CreateNode(group);
       }
    }

    // 建立一個唯一的 key ,用來對映 Xml 和物件的主鍵
    string key = Guid.NewGuid().ToString();

    // 建立一個新結點
    XmlElement objectElement = rootMap.OwnerDocument.CreateElement(element);
   
    // 建立結點屬性 key
    XmlAttribute objectAttribute = rootMap.OwnerDocument.CreateAttribute("key");

    // 設定屬性值為 剛才生成的 Guid
    objectAttribute.Value = key;

    // 將屬性新增到結點
    objectElement.Attributes.Append(objectAttribute);

    // 將結點新增到 groupNode 下面(groupNode為Xpath的層次部分)
    groupNode.AppendChild(objectElement);

    // 將 key 和 物件新增到實際的儲存位置,比如Hashtable
    cacheStrategy.AddItem(key, obj);
}

RemoveItem()則用於從快取中刪除物件,它也包含了兩個步驟:1、先從Xml文件樹中刪除結點;2、再從實際的儲存位置(Hashtable)中刪除物件。這裡需要注意的是:如果XPath指定的是一個葉結點,那麼直接刪除該結點;如果XPath指定的是組結點,那麼需要刪除組結點下的所有結點。程式碼如下:

// 根據 XPath 刪除物件
public virtual void RemoveItem(string xpath) {

    xpath = PrepareXPath(xpath);
    XmlNode result = rootMap.SelectSingleNode(xpath);

    string key;           // 物件的Id

    // 如果 result 是一個組結點(含有子結點)
    if (result.HasChildNodes) {

       // 選擇所有包含有key屬性的的結點
       XmlNodeList nodeList = result.SelectNodes("descendant::*[@key]");
      
       foreach (XmlNode node in nodeList) {

           key = node.Attributes["key"].Value;

           // 從 Xml 文件中刪除結點
           node.ParentNode.RemoveChild(node);

           // 從實際儲存中刪除結點
           cacheStrategy.RemoveItem(key);
       }
    } else {      // 如果 result 是一個葉結點(不含子結點)

       key = result.Attributes["key"].Value;
       result.ParentNode.RemoveChild(result);
       cacheStrategy.RemoveItem(key);
    }
}

最後的兩個方法,GetItem()和GetList()分別用於從快取中獲取單個或者多個物件。值得注意的是當使用GetList()方法時,Xpath應該為到達一個組結點的路徑。

// 根據 XPath 獲取物件
// 先根據Xpath獲得物件的Key,然後再根據Key獲取實際物件
public virtual object GetItem(string xpath) {

    object obj = null;
    xpath = PrepareXPath(xpath);
    XmlNode node = rootMap.SelectSingleNode(xpath);

    if (node != null) {
       // 獲取物件的Key
       string key = node.Attributes["key"].Value;

       // 獲取實際物件
       obj = cacheStrategy.GetItem(key);
    }
    return obj;
}

// 獲取一組物件,此時xpath為一個組結點
public virtual object[] GetList(string xpath) {
    xpath = PrepareXPath(xpath);

    XmlNode group = rootMap.SelectSingleNode(xpath);

    // 獲取該結點下的所有子結點(使用[@key]確保子結點一定包含key屬性)
    XmlNodeList results = group.SelectNodes(xpath + "/*[@key]");

    ArrayList objects = new ArrayList();

    string key;

    foreach (XmlNode result in results) {
       key = result.Attributes["key"].Value;
       Object obj = cacheStrategy.GetItem(key);
       objects.Add(obj);
    }

    return (object[])objects.ToArray(typeof(object));
}

至此,SAF 的快取服務的設計和程式碼實現都完成了,現在我們來看看如何使用它。

程式測試

static voidMain(string[] args) {

    CacheService.Cache cache = CacheService.Cache.Instance;

    // 新增物件到快取中
    cache.AddItem("/lication/Users/Xin", "customer xin");
    cache.AddItem("/lication/Users/Jimmy", "customer jimmy");
    cache.AddItem("/lication/Users/Steve", "customer other");
    cache.AddItem("/lication/GlobalData", "1/1/2008");
    cache.AddItem("/Version", "v10120080401");
    cache.AddItem("/Site", "TraceFact.Net");

    // 獲取所有User
    object[] objects = cache.GetList("/lication/Users");
    foreach (object obj in objects) {
       Console.WriteLine("Customer in cache: {0}", obj.ToString());
    }

    // 刪除所有lication下所有子孫結點
    cache.RemoveItem("/lication");

    // 獲取單個物件
    string time = (string)cache.GetItem("/lication/GlobalData");
    string name = (string)cache.GetItem("/lication/Users/Xin");

    Console.WriteLine("Time: {0}", time);// 輸出為空,lication下所有結點已刪除
    Console.WriteLine("User: {0}", name);// 輸出為空, lication下所有結點已刪除
   

    // 獲取根目錄下所有葉結點
    objects = cache.GetList("/");
    foreach (object obj in objects) {
       Console.WriteLine("Object: {0}", obj.ToString());
    }

    Console.ReadLine();
}

輸出的結果為:

Customer in cache: customer xin
Customer in cache: customer jimmy
Customer in cache: customer other
Time:
User:
Object: v10120080401
Object: Trace

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4289/viewspace-2811700/,如需轉載,請註明出處,否則將追究法律責任。

相關文章