由淺入深:自己動手開發模板引擎——置換型模板引擎(三)


受到群里兄弟們的竭力邀請,老陳終於決定來分享一下.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. 標記格式和上一課一樣,都是“{label}”
  2. 今天加強的是允許對某些變量自定義格式化字符串,這里以日期類型為舉例。聰明的你一定想到了,就是在輸出Token流的時候需要有一段類似於dateTime.ToString("yyyy年MM月dd日 HH:mm:ss") 的代碼。
  3. 由於增加了一個格式化參數的語法,在“{label}”內部又需要將format字符串分離出來,因此解析的難度加大。

模板解析

根據上一節課的內容,我們首先來分析一下解析過程中所需要使用的狀態:

 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


下集預報:本課的代碼為了讓新手容易理解所以沒有做高度封裝,下一篇博文將會對本次的代碼執行一次高度封裝,代碼理解的難度較大,將會獨立出一個詞法分析器類、模板實體類等,充分的面向對象設計。
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM