1.6.0.0 出來一陣子了,不過到過年才有空升級… 主要的原因只有一個,就是最近 spam comments 實在太多了 =_=,新版對於這類問題的處理比較像樣一點..
其它改進還有 nested comments 跟其它一堆改進,就不一一列出來了,有興趣的人可到官方網站去看看。
試了一下,升級後沒啥大問題,除了 CSS 有點走樣之外… 如果各位有發現什麼地方漏掉了,請再通知我 :D
祝大家新年快樂 :D
上一篇廢話了這麼多,其實重點只有一個,我這次打算利用 CacheDependency 的機制,只要一聲令下,我想移除的 cache item 就會因為 CacheDependency 的關係自動失效,而不用很辛苦的拿著 cache key 一個一個移除。
我的想法是用 tags 的概念,建立起一套靠某個 tag 就能對應到一組 cache item,然後將它移除。開始之前先來想像一下 code 寫好長什麼樣子:
透過 tags 來控制 cache items 的範例程式
static void Main(string[] args)
{
string[] urls = new string[] {
"http://columns.chicken-house.net/",
// 共 50 組網址... 略
};
foreach (string url in urls)
{
DownloadData(new Uri(url));
}
Console.ReadLine();
TaggingCacheDependency.DependencyDispose("funp.com");
Console.ReadLine();
}
private static void Info(string key, object value, CacheItemRemovedReason reason)
{
Console.WriteLine("Remove: {0}", key);
}
private static byte[] DownloadData(Uri sourceURL)
{
byte[] buffer = (byte[])HttpRuntime.Cache[sourceURL.ToString()];
if (buffer == null)
{
// 直接到指定網址下載。略...
buffer = null;
HttpRuntime.Cache.Add(
sourceURL.ToString(),
buffer,
new TaggingCacheDependency(sourceURL.Host, sourceURL.Scheme),
Cache.NoAbsoluteExpiration,
TimeSpan.FromSeconds(600),
CacheItemPriority.NotRemovable,
Info);
}
return buffer;
}
}
這段 sample code 做的事很簡單,程式準備了 50 個網址清單,用 for-loop 一個一個下載。下載的 method: DownloadData(Uri sourceURL) 會先檢查 cache 是否已經有資料,沒有才真正下載 (不過下載的細節不是本篇要講的,所以就直接略過了…)。
而主程式的最後一行,則是想要把指定網站 ( funp.com ) 下載的所有資料,都從 cache 移除。為了方便觀看程式結果,我特地加上了 callback method, 當 cache item 被移除時, 會在畫面顯示資訊:
由執行結果來看,果然被移出 cache 的都是來在 funp.com 的網址… 接著來看看程式碼中出現的 TaggingCacheDependecny 是怎麼實作的。相關的 code 如下:
TaggingCacheDependency 的實作
public class TaggingCacheDependency : CacheDependency
{
private static Dictionary<string, List<TaggingCacheDependency>> _lists = new Dictionary<string, List<TaggingCacheDependency>>();
public TaggingCacheDependency(params string[] tags)
{
foreach (string tag in tags)
{
if (_lists.ContainsKey(tag) == false)
{
_lists.Add(tag, new List<TaggingCacheDependency>());
}
_lists[tag].Add(this);
}
this.SetUtcLastModified(DateTime.MinValue);
this.FinishInit();
}
public static void DependencyDispose(string tag)
{
if (_lists.ContainsKey(tag) == true)
{
foreach (TaggingCacheDependency tcd in _lists[tag])
{
tcd.NotifyDependencyChanged(null, EventArgs.Empty);
}
_lists[tag].Clear();
_lists.Remove(tag);
}
}
}
30 行不到… 其實程式很簡單,TaggingCacheDependency 繼承自 CacheDependency, 額外宣告一個靜態的 Dictionary<string, List
用法很簡單,當你要把任何物件放進 cache 時,只要用 TaggingCacheDependency 物件來標示它的 tag:
把物件加進 Cache, 配上 TaggingCacheDependency …
HttpRuntime.Cache.Add(
sourceURL.ToString(),
buffer,
new TaggingCacheDependency(sourceURL.Host, sourceURL.Scheme),
Cache.NoAbsoluteExpiration,
TimeSpan.FromSeconds(600),
CacheItemPriority.NotRemovable,
Info);
在這個例子裡 (line 4), 直接在 TaggingCacheDependency 物件的 constructor 上直接標上 tags, 在此例是直接把網址的 hostname, scheme 兩個部份當作 tag, 未來就可以依照這兩種資訊直接讓 cache 裡的相關物件失效。
而要下令讓 Cache 內有標上某個 tag 的 cache item 失效,只要這行:
將標為 “funp.com” 的 cache item 設為失效的 cache item
TaggingCacheDependency.DependencyDispose("funp.com");
結果就會如同上面的程式範例一樣,還留在 cache 的該網址下載資料,在這一瞬間通通都會被清掉…
用這種方式,是不是比拿到 key 再去呼叫 Cache.Remove( key ) 的方式簡單多了呢? 同時也能夠更快速的處理複雜的移除機制。其實運用 tagging 的方式只是一例,需要的話你也可以設計合適的 CacheDependency 類別。
以下是本篇文章的兩個附加參考檔案:
Download File - URL清單
Download File - Visual Studio 2008 Project
每次心裡有什麼好點子想寫出來時,第一關就卡在想不出個好標題… 想來想去的標題,怎麼看就是既不顯眼又不聳動… 果然是個老實的工程師性格 =_= … 這次要講的,是 .NET HttpRuntime 裡提供的 Cache 物件的操作心得。這個東西我想不用我多作介紹,大家都用到爛掉了吧? 不過好用歸好用,有個老問題其實一直困擾著我很久了…
“我該怎麼手動的把某個物件從 cache 裡移除?”
老實說,這問題蠻沒水準的… 老叫別人要翻 MSDN,我自己怎麼沒翻? 不不… 容我花點篇幅先說明一下問題。Cache物件,是個典型的 Dictionary 型態的應用 (雖然它沒有 implement interface: IDictionary… ), 透過 key 就可以拿到 cached item. 要從 cache 裡移除某個 item, 簡單的很,只要用 Remove 這個 method, 一行就搞定了:
從 key 移除指定的 cache item
HttpRuntime.Cache.Remove("cache-key");
別小看這一行,實作起來障礙還不少。首先,你得額外去記著 cache key 的值。當你要移除的 cache item 有多個的時後,或是移除的 items 之間的關係有點複雜時,這些 code 就不怎麼漂亮了。下一個問題是:
“我該如何得知所有存在 Cache 內的 keys 有那些?”
這個問題單純的多,那些把 intelligent sense 當購物網站的人 (平常不看文件,只會按下 . 然後挑個順眼 method 來用的人),可能這次就碰壁了… Cache 物件不像一般的 Dictionary 一樣,有提供 Keys 這樣的 property … 它藏在 GetEnumerator 這 method 內,它會把所有的 keys 給巡一遍,你需要所有的 keys 的話,可以這樣用:
跑過 cache 裡每一個 key
foreach (string key in HttpRuntime.Cache) {
// …
}
不過這樣的風險也是蠻高的,誰曉得你拿到 key 後的下一秒,這個 cache item 還在不在 cache 內?
本文正式開始! 哈哈,前面那一段只是廢話 + 碎碎唸,現在才是正題。前面想表達的只是,因為 cache 的不確定性 (資料隨時都會被 remove), 操作起來變的要格外小心, 即使它用起來像一般的 Dictionary 一樣。
我舉個案例,來說明我應用 cache 的情況。假如我想實作一個簡單的 web browser, 透過網路下載資源是很慢的動作,每種 browser 都會有某種程度的 cache 機制。我們就拿 Cache 物件替代 IE 的 “temporary internet files” 目錄吧。這時很簡單,只要用 URL 當作 KEY,下載的 content 就當物件塞進去就好…
不過事情沒那麼簡單。如果程式運作了一陣子,我想提供使用者手動清除 “部份” cache 的功能的話,那該怎麼辦? 我舉幾種情況:
這樣的要求應該不算過份吧? 用前面提到的兩種作法,你會想哭吧 XD .. 用這些基礎,你大概只能選這幾種作法 (各位網友有好作法也記得提供一下):
自己另外管理所有下載過的 URL, 用盡各種適合的資料結構,讓你可以順利的挑出這些 match 的 key, 然後移除它。
缺點: 都作這麼多,你乾脆自己重寫個 cache 機制好了… 何況時間一久,你管理的 key, 那些對應的資料搞不好老早就通通從 cache 裡清掉了…
聰明一點,用 regular expression … 從 GetEnumerator( ) 一筆一筆過濾出要移除的 URL, 然後清掉它…
缺點: 這作法只會檢查還留在 cache 內的 URL,不過這樣的 cache 隨便也有成千上萬個,每次都要 looping 掃一次實在不怎麼好看… 有違處女座有潔癖的個性…
這些方法 code 寫起來實在不怎麼漂亮,我就不寫 sample code 了,請各位自行想像一下寫起來的樣子。抱歉,如果你用的正好是上面的作法… 那請多包含… :D 這些都是 workable 的作法,但是看起來就是沒什麼設計感;程式可以動,不過就效能、簡潔、可讀性、美感來看,就是覺的不夠精緻 @@。跟朋友討論到這個問題時,我想到一個爛主意…
“用蠢方法,這些 cache item 先分好類,每一類去關聯一個檔案,設 CacheDependency … 要清掉時去 touch 一下這個檔案,一整組的物件就會自動被清出 cache 了…。”
老實說,我覺的這是個既聰明又愚蠢的作法。聰明的是它很漂亮的解決我要如何移除某一群 item 的問題…,愚蠢的是這種單純程式內可以解決的事,竟然要繞到外面不必要的 file system I/O 動作… 而這通常是最慢的…
–
咳,寫太晚,實際的程式碼明天待續…
有嘗試過的朋友就知道,單日腳踏車破百,雖然不算太難,不過也不輕鬆。騎到後來會有那種撞牆期的感覺.. XD
今天就拼了,再度挑戰這個里程碑,騎到大溪然後再繞回來…
起點:從家裡出發 (關渡),終點:大溪。來回預估應該有 90 ~ 95km 左右,不過看起來是不夠,那就繞點路唄 :P
[5.6km] 先到關渡水岸公園,然後接腳踏車道…
[7.0 km] 經過關渡大橋
[11km] 五股溼地… 上回來的時候沒這些大片的水,那時都是泥土
[19.3km] 大漢溪左岸,快到城林橋了。
(上回的舊照片) 剛過城林橋..
[27km] 轉眼間已經到鶯歌了..
[28km] (上回舊照片) 鶯歌陶瓷博物館,經過了好幾次,可是都沒進去過 @_@
[28.3 km] 三鶯大橋下,鶯歌到大溪的自行車道 (2009/07 才通車)
過了三鶯大橋後,腳踏車道的風景就完全不一樣了 (Y)
過了個閘門,原來水都被關在這裡,難怪一路上大漢溪都沒什麼水…
真棒的風景,我喜歡這種有有山有水的 (Y)
離開溪邊,到大溪的這段路變成鄉間小逕,兩旁都是韭菜田及稻田…
[41.2km] 到大溪橋了 (這次沒照照片,拿上次的照片充數… :P)
看看碼錶,才 41.2km, 騎回家大概連 90 都不到,更別提破百了,真是失算 @@ 看看時間還夠,回程就繞去三峽老街逛逛好了。
雖然公司有好幾個人住三峽,不過每個都不知道那家有名的牛角麵包是那一家… 只會跟我講我上次買錯家了 XD…
看來還是 GOOGLE + 路人比較可靠,這次就找到了… 是在條不起眼的巷子裡,一家叫 [福美軒] 的麵包店.. 一堆人等著麵包出爐,排隊排到店外面… 足足排了一小時才買到 =_=,一個 20 元,每人限購 30 個…
在回程的路上拿了兩個出來吃,果然好吃 (Y)
柑園橋旁的XX公園 (抱歉,名字忘記了),有一片草地,前面的是蓮花池… 不過季節不對,看不到蓮花 @@ 那堆綠綠的是布袋蓮,可不是草地… 踩下去是會掉下去的
[85km] 華江橋上照的..
最後回到家,最後一個巷子口看了一下碼錶,98.5km…. @@
單日破百的行程怎麼可以敗在這最後這區區 1.5km ? 於是就繞了點路,去附近的腳踏車店打個氣,然後再回家… 正好 100.29 km! 哇哈哈,單日破百的成就達成!
原定 #4 就提到的 “抽像化”,竟然被我連拖兩期,拖到 #6 才提到它… 人老了果然比較囉唆… 在前面的幾篇,重點都在如何 “具體” 的描述 “生命遊戲” 裡的細胞。不過現在要把這程式擴大到能容納各種不同的生物,先作好抽像化的工作是必要的…。
一般物件導向所指的 “抽像化”,是指你對某些事物的一般概念。比如有人問你:
“你會開車嗎?”
你腦袋裡想的應該是一般印像中的車子,有方向盤,排檔打下去,油門踩了就會前進,煞車踩了就會停下來…,這就是你對 “車子” 的抽像化。你不會去管車子是什麼牌的,什麼顏色,是二門跑車,或是休旅車之類的細節… 而你 “會” 開的車,也不會因為這些細節,有太大的不同。
這樣的抽像化概念,套用到考駕照這件事來說,你只要知道方向盤,油門等等的用法,同時也練習過,能正確的控制教練車,通過測驗,監理所就會發張駕照給你,證明你會開一般的車子。就算是在你學會開車後十年才上市發表的新車也是一樣。
看起來沒什麼了不起的描述,在電腦的世界裡可不是這麼一回事。Microsoft Word 1.0 想要順利開啟 Microsoft Word 6.0 的檔案,大概想都不用想,因為 1.0 版設計之初,有太多 6.0 版的變化是無法事先預料的,自然無法設計出能正確操作的程式,這現像在電腦的世界很正常。不過如果你兩年前考到的駕照,碰到兩年後的新車你就不會開了,甚至監理所還要求你重考張新的架照… 那這駕照等於一點用都沒有。中間的差別,就在於駕駛者對於開車的認知,跟實際的車子,中間是隔著一層 “抽像化” 的概念,而只要能掌握這抽像化的定義,就能順利操作未來的車種。
因此物件技術不斷的想要模擬這樣的關係,就發展出繼承這樣的方式,來表達這個概念。先用一個類別 (base class) 或是介面 (interface) 來表達這個 “抽像化” 的概念,而不表達細節。其它要跟它互動的程式,只能透過這個抽像化型別來溝通,而其它的細節或實作,則被藏在裡面,或是衍生類別。中間的故事我就不再多說了,再說我就直接去寫 OOP 的書好了 =_=,有興趣可以參考這本經典 [世紀末軟體革命],有復刻版喔。套用到我們的 “生命遊戲” 裡,要定義的就是 “世界” 如何跟 “生命” 互動? 之間的關係是什麼? 另外就是 “生命” 有各種不同的型態,所有的 “生命” 型態是否都能順利的在同一個 “世界” 裡生存?
先試著用簡單文字來描述吧。在我們的定義裡,世界是個 M * N 的棋盤,每一格都能放一個生物。每個生物有自己的狀態 (生/死),也會隨著時間與環境的不同,讓生物的狀態產生變化。畫成 UML 的 class diagram, 大概就像這樣 (手邊沒工具,用 power point 大概畫一下… Orz):
我們在撰寫程式時,就必需思考題目中講到的生物各種特性,那些是所有的生命共有的特色? 這部份要把它定義在 Life … 另一部份是某種細胞特有的,則要放在衍生類別 Cell 裡。而世界必需要能跟生命作適當的互動,讓生命的進行能繼續下去。這樣的架構好處是,未來如果有第二種 Cell 或是其它的生物,只要是從 Life 繼承下來,都能很順利的在 World 裡活著,因為物件導向技術的 “抽像化” 概念,保證這樣程式的可行性。
好,我們就以需要跟 World 接觸跟互動的部份為主,把原程式的 Cell 抽離出來,放到它的上層類別 Life 裡。這也是物件技術裡常提到的 “generalization” (一般化),越一般的特性要越往上層類別移動,而越往下就是 “specialization” (特殊化),底層的類別要去實作特殊的部份,或是特有的細節。
先把原程式作好調整吧。原 Cell 的程式碼,部份被搬移到 Life, 同時這兩個類別有了繼承關係,如下:
Life 的部份,定義了所有 Life 都該表達出來的特性,也就是我們對於 Life 的認知,都應該描述在裡面,像是 Life 活在 World (CurrentWorld) 裡,會有它的座標 (PosX, PosY), 也會有它在這個棋盤內顯示的方式 (DisplayText) 等。而跟 World 互動的方面,Life 則透過 GetNextWorldTask( ) 來讓 World 來讓 Life 驅動它生命的進行。
在 World 的這邊,不管是那種 Life 衍生類別的物件,一律都當成 Life 的 “抽像概念” 來操作。這樣的優點,在還不曉得未來這世界到底還有多少種不同的 Life 會在裡面生活時,主要程式就能開發了。未來 Life 可以一直擴充,衍生出多種不同的 Life 子類別,而 Life / World 之間的互動及規範,則可以完全不用修正。
接下來就要讓這遊戲的規則,變的更真實一點了。實際的情況下,應該是我們已經知道會有那些不同的生物,經過歸納 (一般化及特殊化) 之後,可以設計出我們需要的類別架構。不過實際寫起程式來可沒這麼好命 (就像 USER 永遠不會一次給你完整確定的需求一樣),很多時後你得去 “猜” 或是 “假設”,因此跟本沒有 “一般化” 這回事,你得預先去猜測未來要應付什麼問題,而在細節都還不清楚時就先定義出上層類別。
我們開始來試看看,我們定義的夠不夠抽像吧! 如果助教看你這麼快就把生命遊戲的作業交出來,覺的很沒面子,想把題目變難一點,加上有病毒感染的情況。於是原題目的四條規則追加一條,變成這樣:
我們的程式該怎麼配合它改變? (對,機車的 USER 就都是這樣臨時修改規格…) 先來看看執行的結果,畫面上已經分的出來活著的 Cell 跟受感染的 Cell … 除了看到 Cell 活著與死亡的變化之外,也看的到病毒擴散的狀況是怎麼樣。執行的畫面如下:
圖例: ◎受感染的細胞,●活著的正常細胞,○死亡的細胞
接著,來看看改版後的程式碼:
public bool IsInfected
{
get
{
return this.InfectedCount > 0;
}
}
private int InfectedCount = 0;
public override string DisplayText
{
get
{
if (this.IsAlive == true) return "●";
else if (this.IsInfected == true) return "◎";
else return "○";
}
}
protected override IEnumerable<TimeSpan> WholeLife()
{
yield return TimeSpan.FromMilliseconds(_rnd.Next(800, 1200));
for (int index = 0; index < int.MaxValue; index++)
{
int livesCount = 0;
int infectsCount = 0;
foreach (Cell item in this.FindNeighbors())
{
if (item.IsAlive == true) livesCount++;
if (item.IsInfected == true) infectsCount++;
}
bool? value = _table[this.IsAlive ? 1 : 0, livesCount];
if (value.HasValue == true)
{
this.IsAlive = value.Value;
}
if (this.IsInfected == true)
{
this.InfectedCount--;
if (this.InProbability(10) == true) this.IsAlive = false;
}
else
{
if (this.InProbability(1 + infectsCount * 5) == true) this.InfectedCount = 3;
}
yield return TimeSpan.FromMilliseconds(_rnd.Next(800, 1200));
}
this.Dispose();
yield break;
}
細節我就不多介紹了。這裡的重點是經過抽像化的動作後,把 Life / Cell 之間的邏輯做明確的劃分。World 的類別程式碼完全沒有出現任何有關 Cell 的 Code, 只有出現 Life 而已。除了在主程式 GameHost 有這麼一段,明確的把 Cell 建立起來,把它放進 World:
static void Main(string[] args)
{
int worldSizeX = 30;
int worldSizeY = 30;
World realworld = new World(worldSizeX, worldSizeY);
Random _rnd = new Random();
for (int x = 0; x < worldSizeX; x++) {
for (int y = 0; y < worldSizeY; y++) {
Cell item = new Cell();
realworld.PutOn(item, x, y);
}
}
// ...
}
這樣的作法,其實已經引含了 “動態聯結” 的特性了。在開發主程式的階段 (指 World / Life 這兩個主要的 class), 都還沒有 Cell 的相關細節,而事後執行的程式碼卻可以依照 Cell 裡的邏輯來執行。這代表了我們不需要改主程式的設計,就能不斷的加入新的規責,甚至是新的生物進來一起運作。
如何? 物件技術的 “抽像化” 能力,的確很有效的解決了這樣的變化需求。下一篇會沿用一樣的架構,但是執行的範例會完全不一樣 (這次不用細胞了,直接用草原上的生態: 草、羊、虎) 的生命及規則,來套進這個框架,看看它能怎麼模擬出一個新的生態系統。
這樣的架構可以應付未來未知的變化,只要你的抽像化概念不變的前題下都沒問題。這種保留彈性,卻又不用在 design time 去多做不必要的實作,才是物件技術強大的地方。我舉個反例,很多剛入行的軟體工程師,你給他一個需求,他會想太多… 一個簡單的輸入 1 + 1 要顯示 2 的結果,他會這麼想:
USER 需不需要列印啊? 我先把這需求放進去好了,然後加個 config 預設關掉它,以免以後需要我還得大改程式…
只能算 1 + 1? 如果以後 USER 要算 3 * 5 怎麼辦? 好吧,我把 + 用一個 mode 來代表好了,以後 USER 需要括充為支援 +-/ 就不用大改程式了…*
…
碰到這些狀況,我只能誇獎這位年青有為的程式設計師一句話:
“你很認真… 辛苦了…”
不過我心裡會苦笑… 只不過要你寫個 1 + 1 = 2,搞這麼一大包? 多作考慮,預留未來可能需要的功能,不是件壞事。不過既然是未知的需求,你又如何保證你能夠正確的 “預知” ,然後進一步 “預留” ? 何況這些多做的需求,未來真正會用上的有多少? 用不到的話,只是開發成本的浪費,及讓你的架構複雜性提高,維護的困難增加而已。物件技術真的解決的了這種問題嗎? 下一篇的目標,我們會定在不修改 World / Life 的設計為前題,把生命遊戲的模擬內容換成草原的生態模擬。敬請期待續集 :D