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() + "<br />"; //Label1為頁面上的一個Label控件
Label1.Text += t.b.ToString();
結果是隻要站點不重新開機(代碼也不修改),那麼a的值是恒定不變的,即使将頁面關了重新打開也一樣,可見a隻在Test類加載到AppDomain中進行了一次初始化。而b在每次重新整理時都會改變,因為每次請求頁面都會在建立Test類型執行個體時重新對a進行初始化。
聲明為靜态(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中的多個相關資料對象。
而實際上是怎麼實作這一過程的呢?我們一步步來看:
- 首先在記憶體中動态建構一個 Xml文檔,它隻包含一個根結點,可以任意命名,這裡将它命名為了Cache。
- 提供一個Xpath路徑:擷取對象(資料)前首先要存儲對象,存對象自然要先提供一個路徑(這裡稱為“路徑”,是因為它是一個XPath,實際上也就相當于Hashtable中的鍵Key)。
- 根據上一步提供的路徑,以Cache為根結點,逐層深入地建立XmlNode結點。
- 生成一個GUID,在葉結點上添加一個Key屬性,為這個Key屬性指派為GUID。
- 在Hashtable中存儲對象,其中Hashtable的Key即為上一步生成的GUID,而Value為要存儲的對象。
使用這種方式,Hashtable的實際的Key,即動态生成的GUID對程式員來說是透明的,程式員在存儲/擷取對象時,隻需要提供XPath表達式就可以。下面這幅圖說明了它們之間的關系:

這裡還需要再說明三點:
- 我們使用Hashtable存儲對象,可以直接将Hashtable聲明為static的,也可以将Hashtable聲明為instance的,但是将Hashtable所屬的對象聲明為static的。這裡應用了Singleton模式,先将對Hashtable的操作封裝成一個類,然後在這個類上應用Singleton模式,確定了這個類隻有一個(這個類所維護的Hashtable執行個體自然也隻有一個了)。很明顯,這個類包含了主要的邏輯,我們将之命名為Cache。
- 使用Hashtable的好處是可以存儲任何類型的對象,缺點是喪失了類型安全。有時候我們可能會想使用一個泛型集合類來取代Hashtable,比如Dictionary<T key, T value>。是以這裡又引入了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類的字段以及構造函數(注意為私有)。
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 < xpathArray.Length; 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) {
xpath = "/Cache"; // 這裡的名稱需與構造函數中建立的根結點名稱對應
foreach (string s in xpathArray) {
if (s != "") {
xpath += "/" + s;
return xpath;
AddItem()方法用于向緩存中添加對象,包括了下面幾個步驟:
- 根據輸入的XPath判斷到達 葉結點 的路徑是否已經存在,如果不存在,調用上面的CreateNode()方法,逐層建立結點。
- 生成GUID,在組結點下建立 XmlNode 葉結點,為葉結點添加屬性Key,并将值設為GUID。
- 将對象儲存至實際的位置,預設實作是一個Hashtable,通過調用ICacheStrategy.AddItem()方法來完成,并将Hashtable的Key設定為GUID。
為了說明友善,這裡有一個我對一類結點的命名--“組結點”。假設有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;
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) {
XmlNode group = rootMap.SelectSingleNode(xpath);
// 擷取該結點下的所有子結點(使用[@key]確定子結點一定包含key屬性)
XmlNodeList results = group.SelectNodes(xpath + "/*[@key]");
ArrayList objects = new ArrayList();
string key;
foreach (XmlNode result in results) {
Object obj = cacheStrategy.GetItem(key);
objects.Add(obj);
return (object[])objects.ToArray(typeof(object));
至此,SAF 的緩存服務的設計和代碼實作都完成了,現在我們來看看如何使用它。
程式測試
static void Main(string[] args) {
CacheService.Cache cache = CacheService.Cache.Instance;
// 添加對象到緩存中
cache.AddItem("/WebApplication/Users/Xin", "customer xin");
cache.AddItem("/WebApplication/Users/Jimmy", "customer jimmy");
cache.AddItem("/WebApplication/Users/Steve", "customer other");
cache.AddItem("/WebApplication/GlobalData", "1/1/2008");
cache.AddItem("/Version", "v10120080401");
cache.AddItem("/Site", "TraceFact.Net");
// 擷取所有User
object[] objects = cache.GetList("/WebApplication/Users");
foreach (object obj in objects) {
Console.WriteLine("Customer in cache: {0}", obj.ToString());
// 删除所有WebApplication下所有子孫結點
cache.RemoveItem("/WebApplication");
// 擷取單個對象
string time = (string)cache.GetItem("/WebApplication/GlobalData");
string name = (string)cache.GetItem("/WebApplication/Users/Xin");
Console.WriteLine("Time: {0}", time);// 輸出為空,WebApplication下所有結點已删除
Console.WriteLine("User: {0}", name);// 輸出為空, WebApplication下所有結點已删除
// 擷取根目錄下所有葉結點
objects = cache.GetList("/");
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