由浅入深:自己动手开发模板引擎——置换型模板引擎(三)
受到群里兄弟們的竭力邀請,老陳終于決定來分享一下.NET下的模板引擎開發(fā)技術(shù)。本系列文章將會帶您由淺入深的全面認識模板引擎的概念、設(shè)計、分析和實戰(zhàn)應(yīng)用,一步一步的帶您開發(fā)出完全屬于自己的模板引擎。關(guān)于模板引擎的概念,我去年在百度百科上錄入了自己的解釋(請參考:模板引擎)。老陳曾經(jīng)自己開發(fā)了一套網(wǎng)鳥Asp.Net模板引擎,雖然我自己并不樂意去推廣它,但這已經(jīng)無法阻擋群友的喜愛了!
在上一篇我們以簡單明快的方式介紹了置換型模版引擎的關(guān)鍵技術(shù)——模板標(biāo)記的流式解析。采用流式解析可以達到相當(dāng)好的解析性能,因為它基本上只需要對字符串(模板)掃描一次就可以完成所有代碼的解析。不像String.Split()和正則表達式那樣會造成很多迭代效應(yīng)。今天我們引入一個較為復(fù)雜的示例,然后封裝一個實用級別的模板引擎。封裝就意味著使用者無需了解內(nèi)部如何實現(xiàn),只需要知道如何引用即可(為了降低門檻,本文沒有進行高級封裝和重構(gòu),這些內(nèi)容在下一篇文章中放出)。
概述
題外話:在某公司入職之后,我曾經(jīng)非常抱怨其CRM系統(tǒng)代碼架構(gòu)的糟糕程度,其中比較重要的一點是不倫不類的面向?qū)ο?過程的編碼以及各種無法重用或無意重用的代碼。一位同事便向我請教,如何編寫面向?qū)ο蟮膽?yīng)用程序呢?實際上面向?qū)ο笫紫仁且环N深度思維的結(jié)果,方法就只有一個:把一切都當(dāng)作對象!
回到我們今天的話題,想做好面向?qū)ο蟮脑O(shè)計,首先要明確一下我們要做什么——我們要做的是一個模板引擎。它應(yīng)當(dāng)能夠解析一些模板代碼,然后根據(jù)外部業(yè)務(wù)數(shù)據(jù)生成我們期望的結(jié)果。當(dāng)不關(guān)心如何實現(xiàn)這些需求的時候,可以先定義一個接口(暫時不要關(guān)心這個接口定義是否合理,否則哪里來的重構(gòu)?):
1 /// <summary> 2 /// 定義模板引擎的基本功能。 3 /// </summary> 4 public interface ITemplateEngine 5 { 6 /// <summary> 7 /// 解析模板。 8 /// </summary> 9 /// <param name="templateString">包含模板內(nèi)容的字符串。</param> 10 void Parser(string templateString); 11 12 /// <summary> 13 /// 設(shè)定變量標(biāo)記的值。 14 /// </summary> 15 /// <param name="key">鍵名。</param> 16 /// <param name="value">值。</param> 17 void SetValue(string key, object value); 18 19 /// <summary> 20 /// 處理模板并輸出結(jié)果。 21 /// </summary> 22 /// <returns>返回包含業(yè)務(wù)數(shù)據(jù)的字符串。</returns> 23 string Process(); 24 }定義了模板引擎的基本功能,我們就試著實現(xiàn)一下。為了讓大家接觸到更多的流式解析技巧,本例對上一篇文章中的標(biāo)記語法做了更改,使其更為復(fù)雜。如果您仔細觀察上面的接口定義,會發(fā)現(xiàn)SetValue()方法的value參數(shù)被定義為object。我們的目標(biāo)是滿足如下需求:
1 [TestFixture] 2 public sealed class TemplateEngineUnitTests 3 { 4 private const string _templateString = "[<time>{CreationTime:yyyy年MM月dd日 HH:mm:ss}</time>] <a href=\"{url}\">{title}</a>"; 5 private const string _html = "[<time>2012年04月03日 16:30:24</time>] <a href=\"http://www.ymind.net/\">陳彥銘的博客</a>"; 6 7 [Test] 8 public void ProcessTest() 9 { 10 var templateEngine = new TemplateEngine(); 11 templateEngine.Parser(_templateString); 12 templateEngine.SetValue("url", "http://www.ymind.net/"); 13 templateEngine.SetValue("title", "陳彥銘的博客"); 14 templateEngine.SetValue("CreationTime", new DateTime(2012, 4, 3, 16, 30, 24)); 15 16 var html = templateEngine.Process(); 17 18 Trace.WriteLine(html); 19 20 Assert.AreEqual(html, _html); 21 } 22 }有經(jīng)驗的朋友可能已經(jīng)發(fā)現(xiàn)了,這不是個單元測試么?是的,在這里老陳使用了測試驅(qū)動開發(fā)的思路(我會盡量的在我的博文中給大家分享各方面的經(jīng)驗技巧,這才是傳說中的干貨!)。測試驅(qū)動開發(fā)有什么好處?很顯然,有了單元測試代碼,我們就很明確的知道我們要做什么了,而且單元測試本身就是一個demo。你還需要文檔嗎?文檔在很多時候并不是必要的,但在某些時候又是非要不可的,要區(qū)別對待。
奔著這個單元測試代碼,我們基本可以明確今天的學(xué)習(xí)內(nèi)容:
模板解析
根據(jù)上一節(jié)課的內(nèi)容,我們首先來分析一下解析過程中所需要使用的狀態(tài):
1 /// <summary> 2 /// 表示詞法分析模式的枚舉值。 3 /// </summary> 4 /// <remarks>記得上次我們的命名是PaserMode么?今天我們換個更加專業(yè)的單詞。</remarks> 5 public enum LexerMode 6 { 7 /// <summary> 8 /// 未定義狀態(tài)。 9 /// </summary> 10 None = 0, 11 12 /// <summary> 13 /// 進入標(biāo)簽。 14 /// </summary> 15 EnterLabel, 16 17 /// <summary> 18 /// 脫離標(biāo)簽。 19 /// </summary> 20 LeaveLabel, 21 22 /// <summary> 23 /// 進入格式化字符串。 24 /// </summary> 25 EnterFormatString, 26 27 /// <summary> 28 /// 脫離格式化字符串。 29 /// </summary> 30 LeaveFormatString, 31 }請注意,每個模式都是成對出現(xiàn)的,因為流式解析總會是有始有終的!哪怕某些開始和結(jié)束在物理上是重合的。但是Enter和Leave這兩個動作總是在描述同樣一件事物,我們就可以縮減對象類型(這里是指詞法分析模式),優(yōu)化后定義如下:
1 /// <summary> 2 /// 表示詞法分析模式的枚舉值。 3 /// </summary> 4 /// <remarks>記得上次我們的命名是PaserMode么?今天我們換個更加專業(yè)的單詞。</remarks> 5 public enum LexerMode 6 { 7 /// <summary> 8 /// 未定義狀態(tài)。 9 /// </summary> 10 Text = 0, 11 12 /// <summary> 13 /// 進入標(biāo)簽。 14 /// </summary> 15 Label = 1, 16 17 /// <summary> 18 /// 進入格式化字符串。 19 /// </summary> 20 FormatString = 2, 21 }不過我們今天要強化的可不只是增加了一個格式化字符串這么簡單,我們還要能夠明確的了解到每個Token的位置信息和類型,這是我們下一節(jié)講解解釋型模版引擎時所需要用到的概念。Token在上一節(jié)中我們僅僅使用了一個string類型來表示,但這個滿足不了我們的需要了,我們需要自定義一個Token類型,如下:
1 /// <summary> 2 /// 表示一個 Token。 3 /// </summary> 4 public sealed class Token 5 { 6 /// <summary> 7 /// 初始化 <see cref="Token"/> 對象。 8 /// </summary> 9 /// <param name="kind"><see cref="TokenKind"/> 的枚舉值之一。</param> 10 /// <param name="text">Token 文本。</param> 11 /// <param name="line">Token 所在的行。</param> 12 /// <param name="column">Token 所在的列。</param> 13 public Token(TokenKind kind, string text, int line, int column) 14 { 15 this.Text = text; 16 this.Kind = kind; 17 this.Column = column; 18 this.Line = line; 19 } 20 21 /// <summary> 22 /// 獲取 Token 所在的列。 23 /// </summary> 24 public int Column { get; private set; } 25 26 /// <summary> 27 /// 獲取 Token 所在的行。 28 /// </summary> 29 public int Line { get; private set; } 30 31 /// <summary> 32 /// 獲取 Token 類型。 33 /// </summary> 34 public TokenKind Kind { get; private set; } 35 36 /// <summary> 37 /// 獲取 Token 文本。 38 /// </summary> 39 public string Text { get; private set; } 40 }我們使用行數(shù)、列數(shù)、類型和文本(內(nèi)容)來共同描述一個Token,這下可豐富多彩了!TokenKind明顯應(yīng)該是個枚舉值,根據(jù)本例,TokenKind的定義如下:
1 /// <summary> 2 /// 表示 Token 類型的枚舉值。 3 /// </summary> 4 public enum TokenKind 5 { 6 /// <summary> 7 /// 未指定類型。 8 /// </summary> 9 None = 0, 10 11 /// <summary> 12 /// 左大括號。 13 /// </summary> 14 LeftBracket = 1, 15 16 /// <summary> 17 /// 右大括號。 18 /// </summary> 19 RightBracket = 2, 20 21 /// <summary> 22 /// 普通文本。 23 /// </summary> 24 Text = 3, 25 26 /// <summary> 27 /// 標(biāo)簽。 28 /// </summary> 29 Label = 4, 30 31 /// <summary> 32 /// 格式化字符串前導(dǎo)符號。 33 /// </summary> 34 FormatStringPreamble = 5, 35 36 /// <summary> 37 /// 格式化字符串。 38 /// </summary> 39 FormatString = 6, 40 }也就是說本次我們將要面對5種Token(None純粹是為了描述一個空類型)!
在往下看之前請您按照上一課中的方法自行實現(xiàn)一下本節(jié)課的需求,1小時之后再回來。
如果您自己推敲過了,可能會發(fā)現(xiàn)一個問題,即FormatString是嵌套在Label里面的,這個貌似很難區(qū)分啊!是的,本節(jié)之所以設(shè)計了這么一個需求,就是有了這么一個嵌套Token的解析過程,掌握這個技巧是至關(guān)重要的!因此,我希望您不要偷懶,自行先摸索摸索,先不要看后面的答案……
實際上,如果您曾經(jīng)接觸過編譯原理的話,可能如上的難題根本就不是什么事,因為這是一個司空見慣的問題。這整個就是方法簽名即形式參數(shù)的實現(xiàn),比如:
- Do()
- Do("x")
- Do("x", "y")
- Do("x", y, "z")
很眼熟很常見不是?那么在解析這些代碼的時候,由于模式會嵌套,也就意味著模式會后進先出。后進先出?!你想到了什么? 對!就是它,不要懷疑!Stack!只不過在泛型稱霸天下的今天,我們當(dāng)然要選用Stack<T>了!這里我就不再帖出自己的實現(xiàn)代碼了,因為太長了。
變量賦值
變量賦值很簡單,就是使用Dictionary<string, object>:
1 private readonly Dictionary<string, object> _variables = new Dictionary<string, object>(); 2 3 /// <summary> 4 /// 設(shè)定變量標(biāo)記的值。 5 /// </summary> 6 /// <param name="key">鍵名。</param> 7 /// <param name="value">值。</param> 8 public void SetValue(string key, object value) 9 { 10 // 就這么簡單 11 this._variables[key] = value; 12 }這一小節(jié)沒有任何難度,難道說簡單一點不好么?
數(shù)據(jù)輸出
在輸出業(yè)務(wù)數(shù)據(jù)的時候,唯一的難點就是如何實現(xiàn)自定義格式化字符串,廢話不多說,直接上代碼:
1 /// <summary> 2 /// 處理模板并輸出結(jié)果。 3 /// </summary> 4 /// <returns>返回包含業(yè)務(wù)數(shù)據(jù)的字符串。</returns> 5 public string Process() 6 { 7 var result = new StringBuilder(); 8 9 for (var index = 0; index < this._tokens.Count; index++) 10 { 11 var token = this._tokens[index]; 12 13 switch (token.Kind) 14 { 15 case TokenKind.Label: 16 string value; 17 18 // 具體的Token流是: 19 ??????????????? // Label = CreationTime 20 ??????????????? // FormatStringPreamble = : 21 ??????????????? // FormatString = yyyy年MM月dd日 HH:mm:ss 22 ??????????????? // 因此這里減去2個索引值檢查操作范圍 23 if (index < this._tokens.Count - 2) 24 { 25 // 實現(xiàn)自定義格式化字符串 26 var nextToken = this._tokens[index + 2]; 27 28 if (nextToken.Kind == TokenKind.FormatString) 29 { 30 // 注意這里使用 IFormattable 來驗證目標(biāo)類型是否實現(xiàn)了格式化功能 31 var obj = this._variables[token.Text] as IFormattable; 32 33 value = obj == null ? this._variables[token.Text].ToString() : obj.ToString(nextToken.Text, null); 34 } 35 else value = this._variables[token.Text].ToString(); 36 } 37 else value = this._variables[token.Text].ToString(); 38 39 result.Append(value); 40 break; 41 42 case TokenKind.Text: 43 result.Append(token.Text); 44 break; 45 } 46 } 47 48 return result.ToString(); 49 }總結(jié)及代碼下載
與上一課相比,本課的內(nèi)容跨度較大,但學(xué)習(xí)和理解的難度尚且不是很大。我們下一節(jié)課將會對本節(jié)代碼進行重構(gòu)封裝,看看重構(gòu)能給我們帶來什么驚喜!
代碼下載:置換型模板引擎(3).zip
下集預(yù)報:本課的代碼為了讓新手容易理解所以沒有做高度封裝,下一篇博文將會對本次的代碼執(zhí)行一次高度封裝,代碼理解的難度較大,將會獨立出一個詞法分析器類、模板實體類等,充分的面向?qū)ο笤O(shè)計。
?
總結(jié)
以上是生活随笔為你收集整理的由浅入深:自己动手开发模板引擎——置换型模板引擎(三)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 由浅入深:自己动手开发模板引擎——置换型
- 下一篇: 由浅入深:自己动手开发模板引擎——置换型