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