日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

由浅入深:自己动手开发模板引擎——置换型模板引擎(四)

發(fā)布時(shí)間:2025/6/15 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 由浅入深:自己动手开发模板引擎——置换型模板引擎(四) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

受到群里兄弟們的竭力邀請(qǐng),老陳終于決定來分享一下.NET下的模板引擎開發(fā)技術(shù)。本系列文章將會(huì)帶您由淺入深的全面認(rèn)識(shí)模板引擎的概念、設(shè)計(jì)、分析和實(shí)戰(zhàn)應(yīng)用,一步一步的帶您開發(fā)出完全屬于自己的模板引擎。關(guān)于模板引擎的概念,我去年在百度百科上錄入了自己的解釋(請(qǐng)參考:模板引擎)。老陳曾經(jīng)自己開發(fā)了一套網(wǎng)鳥Asp.Net模板引擎,雖然我自己并不樂意去推廣它,但這已經(jīng)無法阻擋群友的喜愛了!

概述

置換型模板引擎系列是我們進(jìn)入模板引擎開發(fā)領(lǐng)域的基礎(chǔ)課程,這里講述的一些原理、概念和實(shí)踐方案都是后續(xù)模板引擎開發(fā)中所需要用到的,正所謂是由淺入深、循序漸進(jìn)!在編寫這些博文的時(shí)候,我遇到了很多阻力。為了能夠讓菜鳥朋友入門又不讓高手們嗤之以鼻感覺到木有干貨,這讓老陳真的是煞費(fèi)苦心!如果僅僅是開源一份代碼出去,那么完成這樣的項(xiàng)目本身可能不需要多少時(shí)間,然而要把這些組織成文字分享給大家,實(shí)在是很頭疼的一件事情。

最初,我只是想將整個(gè)置換型模板引擎分為兩節(jié)完成,但是發(fā)現(xiàn)不太可能,因此就不斷的拆分。第一課我們簡單了解了一些概念和原理,第二節(jié)我們深入探討了字符流解析為Token流的過程,而第三節(jié)我們將這種過程簡單的封裝了一些,并融入測試驅(qū)動(dòng)開發(fā)的概念進(jìn)去,借此給大家分享更多的開發(fā)技巧。而本節(jié),也是作為置換型模板引擎的最后一節(jié),將會(huì)對(duì)第三節(jié)課中我們所做的簡單封裝執(zhí)行重構(gòu)。

我們今天重構(gòu)的理念就是使用面向?qū)ο笤O(shè)計(jì)的理念來歸納整理模板引擎的業(yè)務(wù)流程、分析實(shí)體并創(chuàng)建代碼模型以及建立單元測試等。我個(gè)人不是專業(yè)的寫手,每篇博文的本意都是為大家分享一些開發(fā)經(jīng)驗(yàn)和技巧,但我不保證我的詞匯描述以及實(shí)踐方案的絕對(duì)準(zhǔn)確性。

需求分析

有了前面幾節(jié)課,我們對(duì)模板引擎的原理已經(jīng)有了非常清楚的認(rèn)識(shí),它本身的實(shí)現(xiàn)就是某種替換機(jī)制。為了追求高效、嚴(yán)謹(jǐn),最后我們提到了按流替代式模板引擎并作出深入探討。經(jīng)歷了三節(jié)課的認(rèn)知和學(xué)習(xí),我們知道按流替代式模板引擎的工作過程會(huì)經(jīng)歷如下階段:

  • 解析模板:
  • 以字符為單位解析模板代碼,并將代碼整理為Token流。在沒有復(fù)雜需求的前提下,每一個(gè)Token都是有著直接意義的。要么它表示普通的Text對(duì)象,會(huì)原原本本的輸出;要么?表示一種Label對(duì)象,在輸出的時(shí)候會(huì)被替換為真實(shí)的業(yè)務(wù)數(shù)據(jù);
  • 有了Token流,按照順序就可以將Text對(duì)象和Label對(duì)象按照實(shí)際的業(yè)務(wù)需求進(jìn)行輸出。實(shí)際上我們之前的舉例并沒有真正的深入到流的概念,使用的都是集合。集合與流的最大區(qū)別就是流只能向前,其中的每個(gè)元素基本上就只有一次訪問機(jī)會(huì),而集合是任意的。
  • 設(shè)定業(yè)務(wù)數(shù)據(jù);
  • 處置并得到輸出結(jié)果。輸出結(jié)果可以保存到臨時(shí)變量,也可以直接輸出展示,此后變脫離模板引擎的業(yè)務(wù)范圍了。
  • 在第三節(jié)課中,我們引入了一個(gè)Label中的Label的概念,即上篇文章中的“{CreationTime:yyyy年MM月dd日 HH:mm:ss}”標(biāo)簽。 這個(gè)標(biāo)記使得我們不是死板的去替換Label,而是可以在模板中直接指定某些數(shù)據(jù)的輸出格式。那么把這種標(biāo)簽還理解為Label的話是不是不太合適了呢?如果未來我們?cè)黾痈訌?fù)雜的語法呢?

    是的,為了使得流程更加清晰,我們?cè)僖胍粋€(gè)概念——Element。對(duì)!元素!就是模板元素!現(xiàn)在我們的解析流程變更為:

  • 將字符流轉(zhuǎn)換為Token流;
  • 將Token流轉(zhuǎn)換為Element流;
  • 如果有可能,還需要把Element整理為Tag語句(這是解釋型引擎內(nèi)必備的東西)
  • 在這里留下一個(gè)作業(yè):請(qǐng)您結(jié)合這幾節(jié)講述的內(nèi)容整理出一個(gè)完整的模板引擎工作流程圖。

    實(shí)體建模

    在面向?qū)ο蟪绦蛟O(shè)計(jì)里,幾乎每一件事物都可以使用結(jié)構(gòu)等來描述,因?yàn)榫幊陶Z言里面之所以支持命名空間、類、結(jié)構(gòu)、接口等概念,就是為了描述面向?qū)ο缶幊獭=裉煳以囍鴱囊粋€(gè)菜鳥的角度來分析和考慮如何實(shí)現(xiàn)實(shí)體建模,思路可能不太符合您的習(xí)慣,但我相信這樣的過程菜鳥們一定會(huì)喜歡!

    整個(gè)模板引擎分為兩個(gè)體系,一個(gè)是對(duì)外公開的業(yè)務(wù)引擎和實(shí)體,一個(gè)是對(duì)內(nèi)的代碼解析器和實(shí)體

    模板引擎的定義

    模板引擎自身不是現(xiàn)實(shí)中的一種實(shí)體,它是一種業(yè)務(wù),也可以理解為幫助類——即某種封裝。以下是思路:

  • 模板引擎就是用來處置模板的,因此它需要有個(gè)模板的屬性,而這個(gè)模板是在模板引擎初始化時(shí)就存在的,模板引擎無權(quán)修改它;
  • 處置模板本身就是做事的過程,這個(gè)需要定義為方法,通過這個(gè)方法我們應(yīng)該能捕獲處置結(jié)果;
  • 要處置模板標(biāo)簽,需要一個(gè)預(yù)定義變量的容器,要提供一套添加變量、刪除變量等的方法;
  • 整理后我們使用接口描述,如下:

    1 /// <summary> 2 /// 定義模板引擎的基本功能。 3 /// </summary> 4 public interface ITemplateEngine 5 { 6 /// <summary> 7 /// 獲取模板。 8 /// </summary> 9 Template Template { get; } 10 11 /// <summary> 12 /// 設(shè)定變量標(biāo)記的置換值。 13 /// </summary> 14 /// <param name="key">鍵名。</param> 15 /// <param name="value">值。</param> 16 void SetVariable(string key, object value); 17 18 /// <summary> 19 /// 刪除變量標(biāo)記的置換值。 20 /// </summary> 21 /// <param name="key">鍵名。</param> 22 void RemoveVariable(string key); 23 24 /// <summary> 25 /// 清空變量標(biāo)記的置換值。 26 /// </summary> 27 void ClearVariables(); 28 29 /// <summary> 30 /// 處理模板。將處理結(jié)果保存到字符編寫器中。 31 /// </summary> 32 /// <param name="writer">指定一個(gè)字符編寫器。</param> 33 void Process(TextWriter writer); 34 35 /// <summary> 36 /// 處理模板。并將結(jié)果作為字符串返回。 37 /// </summary> 38 /// <returns>返回 <see cref="System.String"/></returns> 39 string Process(); 40 }

    模板的定義

    在上文中我們提到,今天增加了一個(gè)Element的概念,那么模板的直接構(gòu)成者就是Element,就如HTML代碼是由各種Element和Text組成的一樣,Text是一種特殊的Element。那么,模板的描述就非常簡單了,它就是Element的集合:

    1 /// <summary> 2 /// 定義一個(gè)模板。 3 /// </summary> 4 public interface ITemplate 5 { 6 /// <summary> 7 /// 獲取模板的標(biāo)簽庫。 8 /// </summary> 9 List<Element> Elements { get; } 10 }

    Element的定義

    Element是構(gòu)成模板的基本單位,然而Element并不是只有一種,前面我們提到最起碼會(huì)分為Label和Text兩種。既然是面向?qū)ο蟮脑O(shè)計(jì),我們就使用多態(tài)性來描述Element。多態(tài)是指同一(種)事物的多種形態(tài),而不是指狀態(tài)。先來看看我們的模板代碼:

    [<time><strong>{CreationTime:<span style="color: #888888;">yyyy年MM月dd日 HH:mm:ss</span>}</strong></time>]\r\n<a href=\"<strong>{url}</strong>\"><strong>{title}</strong></a>

    歸納一下:

    • 非{xxx}格式的都理解為普通Text,會(huì)原原本本的輸出;
    • {xxx}是Label
    • {xxx:xxx}是帶有格式化字符串的Label

    OK,那么就可以形成如下關(guān)系圖:

    圖中的VariableLabel和TextElement共同派生自Element,體現(xiàn)出了Element的多態(tài)性。FormattableVariableLabel派生自VariableLabel又提現(xiàn)了VariableLabel的多態(tài)性。這里,我們將Element定義為抽象類,就不需要定義接口了。如果要定義,那么這個(gè)接口就只需要兩個(gè)屬性:Line和Column。因?yàn)镋lement的共同特點(diǎn)就是有特定的位置,至于是否有數(shù)據(jù)在里面這個(gè)是說不定的事情!

    仔細(xì)觀察VariableLabel還獨(dú)自聲明了一個(gè)Process(Dictionary<string, object> variables)方法,這個(gè)將數(shù)據(jù)置換的過程移動(dòng)到了Element自身。降低了整個(gè)代碼架構(gòu)的耦合性。

    另外,我們這里的Element定義實(shí)際上還缺少了對(duì)“{”、“}”和“:”等特殊字符的描述,他們也是模板代碼的基本元素之一。只不過,在解析過程中我們要忽略它們,這里即使定義了,也可能用不到。

    代碼解析器的定義

    代碼解析器就只有一個(gè)作用——將Token流轉(zhuǎn)換為Element集合,它應(yīng)該從詞法分析器初始化,也僅需要一個(gè)公開方法:

    1 /// <summary> 2 /// 定義模板代碼解析器。 3 /// </summary> 4 internal interface ITemplateParser 5 { 6 /// <summary> 7 /// 解析模板代碼。 8 /// </summary> 9 /// <returns>返回 <see cref="Element"/> 對(duì)象的集合。</returns> 10 List<Element> Parse(); 11 }

    詞法分析器的定義

    詞法分析器的作用是將字符流轉(zhuǎn)換為Token流:

    1 /// <summary> 2 /// 定義模板詞法分析器。 3 /// </summary> 4 internal interface ITemplateLexer 5 { 6 /// <summary> 7 /// 繼續(xù)分析下一條詞匯,并返回分析結(jié)果。 8 /// </summary> 9 /// <returns>Token</returns> 10 Token Next(); 11 }

    這里我們僅僅使用了一個(gè)唯一的Next()方法,它的返回值是Token。也就是說,詞法分析是一個(gè)只能向前的過程,現(xiàn)在您是否能夠領(lǐng)略到為什么我一直在強(qiáng)調(diào)Token流的概念么?作業(yè):請(qǐng)認(rèn)真思考流和集合的區(qū)別。

    Token的定義

    實(shí)際上,Token與Element一樣,都有位置屬性。然而為了便于后期處理,我們還需要保存Token代表的數(shù)據(jù)(這里的Text,實(shí)際上應(yīng)該定義為Data更加合適,為了直觀,這里就Text吧!),還要指明當(dāng)前Token的類型(TokenKind):

    1 /// <summary> 2 /// 定義一個(gè) Token。 3 /// </summary> 4 internal interface IToken 5 { 6 /// <summary> 7 /// 獲取 Token 所在的列。 8 /// </summary> 9 int Column { get; } 10 11 /// <summary> 12 /// 獲取 Token 所在的行。 13 /// </summary> 14 int Line { get; } 15 16 /// <summary> 17 /// 獲取 Token 類型。 18 /// </summary> 19 TokenKind Kind { get; } 20 21 /// <summary> 22 /// 獲取 Token 文本。 23 /// </summary> 24 string Text { get; } 25 }

    其他定義

    Token需要TokenKind來描述其類型,這是一個(gè)有限的狀態(tài)集合,那么就定義為枚舉值。詞法分析器這個(gè)東東,實(shí)際上在第二課第三課已經(jīng)見識(shí)過了,我們不斷的在不同的狀態(tài)中穿梭,那么就需要一個(gè)詞法分析狀態(tài)的枚舉值,這兩個(gè)枚舉值的定義分別如下:

    1 /// <summary> 2 /// 表示 Token 類型的枚舉值。 3 /// </summary> 4 internal enum TokenKind 5 { 6 /// <summary> 7 /// 未指定類型。 8 /// </summary> 9 None = 0, 10 11 /// <summary> 12 /// 左大括號(hào)。 13 /// </summary> 14 LeftBracket = 1, 15 16 /// <summary> 17 /// 右大括號(hào)。 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)符號(hào)。 33 /// </summary> 34 FormatStringPreamble = 5, 35 36 /// <summary> 37 /// 格式化字符串。 38 /// </summary> 39 FormatString = 6, 40 41 /// <summary> 42 /// 表示字符流末尾。 43 /// </summary> 44 EOF = 7 45 } 46 47 /// <summary> 48 /// 表示詞法分析模式的枚舉值。 49 /// </summary> 50 /// <remarks>記得上次我們的命名是PaserMode么?今天我們換個(gè)更加專業(yè)的單詞。</remarks> 51 internal enum LexerMode 52 { 53 /// <summary> 54 /// 未定義狀態(tài)。 55 /// </summary> 56 Text = 0, 57 58 /// <summary> 59 /// 進(jìn)入標(biāo)簽。 60 /// </summary> 61 Label = 1, 62 63 /// <summary> 64 /// 進(jìn)入格式化字符串。 65 /// </summary> 66 FormatString = 2, 67 }

    單元測試

    完成了基本的實(shí)體接口定義,我們不著急編寫功能實(shí)現(xiàn)的代碼,而是先創(chuàng)建個(gè)單元測試,實(shí)現(xiàn)以測試為目的來驅(qū)動(dòng)我們的開發(fā)過程。測試驅(qū)動(dòng)開發(fā)的好處,是我們?cè)陂_發(fā)之前就已經(jīng)知道了我們的編碼目標(biāo)!而平常我們經(jīng)常是需求驅(qū)動(dòng)開發(fā)的,這個(gè)不算科學(xué),當(dāng)遇到多個(gè)團(tuán)隊(duì)配合的時(shí)候,就顯得難以交流。較好的方案是:需求驅(qū)動(dòng)測試、測試驅(qū)動(dòng)開發(fā)、開發(fā)驅(qū)動(dòng)猴子

    實(shí)際上,我們的單元測試代碼在上一課中就編寫過了,稍加修改如下:

    1 [TestFixture] 2 public sealed class TemplateEngineUnitTests 3 { 4 private const string _templateString = "[<time>{CreationTime:yyyy年MM月dd日 HH:mm:ss}</time>]\r\n<a href=\"{url}\">{title}</a>"; 5 private const string _html = "[<time>2012年04月03日 16:30:24</time>]\r\n<a href=\"http://www.ymind.net/\">陳彥銘的博客</a>"; 6 7 [Test] 8 public void ProcessTest() 9 { 10 var templateEngine = TemplateEngine.FromString(_templateString); 11 12 templateEngine.SetVariable("url", "http://www.ymind.net/"); 13 templateEngine.SetVariable("title", "陳彥銘的博客"); 14 templateEngine.SetVariable("CreationTime", new DateTime(2012, 4, 3, 16, 30, 24)); 15 16 var html = templateEngine.Process(); 17 18 Trace.WriteLine(html); 19 20 // 還記得第一節(jié)課我就說,我為了簡化代碼架構(gòu)使用了單元測試的方法來做的demo代碼,那個(gè)不是真正的單元測試 21 // 因?yàn)樵谀莻€(gè)時(shí)候,我們的代碼中沒有包含結(jié)果驗(yàn)證的過程 22 23 // 對(duì)輸出結(jié)果進(jìn)行測試驗(yàn)證,首先不能是null 24 Assert.NotNull(html); 25 26 // 輸出結(jié)果必須與預(yù)期結(jié)果完全一致 27 Assert.AreEqual(_html, html); 28 29 // 如果以上兩個(gè)驗(yàn)證無法通過,那么執(zhí)行的時(shí)候必定會(huì)報(bào)錯(cuò)! 30 } 31 }

    做單元測試的方法有很多,我自己喜歡使用NUnit.Framework + ReSharper,效果如下圖:

    編碼實(shí)現(xiàn)

    編碼實(shí)現(xiàn)這一步主要講一下幾個(gè)難點(diǎn),剩下的請(qǐng)仔細(xì)琢磨代碼。

    難點(diǎn)一:如何實(shí)現(xiàn)FormattableVariableLabel的Process()方法

    在.NET中,凡是支持自定義格式化字符串的對(duì)象必定都會(huì)實(shí)現(xiàn)IFormattable接口,利用這一點(diǎn)我們可以通過以下代碼實(shí)現(xiàn)這個(gè)需求,說難也不難:

    1 /// <summary> 2 /// 處置當(dāng)前元素。 3 /// </summary> 4 /// <param name="variables">與當(dāng)前元素關(guān)聯(lián)的對(duì)象。</param> 5 /// <returns>返回 <see cref="System.String"/></returns> 6 public override string Process(Dictionary<string, object> variables) 7 { 8 if (variables == null || variables.Count == 0) return String.Empty; 9 if (variables.ContainsKey(this.Name) == false) return String.Empty; 10 11 var obj = variables[this.Name] as IFormattable; 12 13 return obj == null ? variables[this.Name].ToString() : obj.ToString(this.Format, null); 14 }

    難點(diǎn)二:如何將Token流轉(zhuǎn)換為Element集合

    我們?yōu)槊總€(gè)Token標(biāo)記了位置和類型信息,依照這些信息進(jìn)行歸納整理即可。在處理的時(shí)候只需要理會(huì)Text和Label兩種類型即可,當(dāng)遇到Label類型時(shí),還有可能要讀取FormatString,而在FormatString之前則必定是FormatStringPreamble!

    詳情請(qǐng)參考TemplateParser.Parse()方法的實(shí)現(xiàn)。

    難點(diǎn)三:詞法解析的過程只能向前會(huì)不會(huì)有問題?

    實(shí)際上,流是一種很普通的概念,水管里面的水只能是一個(gè)方向;電流只會(huì)從一端到另外一端;網(wǎng)絡(luò)數(shù)據(jù)流的發(fā)送和接受都是一次性的(如果您涉足過),如此等等。只能向前,這意味著更好的性能,因?yàn)檫@注定了某些事情我們只能做一次!

    詞法解析過程中可能要判斷前后依賴的字符和字符串(這里要理解為字符數(shù)組),這里就需要定位了,記得FileStream里面有個(gè)Position屬性么?呵呵,為什么我們就不能有呢?但是不要濫用它!

    限于篇幅,通過文字已經(jīng)無法準(zhǔn)確去描述這個(gè)過程了,希望大家能夠認(rèn)真的研究TemplateLexer類!如果您搞不懂它,那么在制作解釋型模板引擎的時(shí)候?qū)?huì)遇到很大的阻力!加油!不懂的地方跟帖發(fā)問!!!

    總結(jié)及代碼下載

    置換型模板引擎系列分了4課才講述完畢,實(shí)際上還不夠完美,但時(shí)間倉促,也不想把篇幅拉的太長,希望大家能夠多多研究代碼。如果有問題請(qǐng)跟帖提出即可。

    本系列教程并沒有將話題集中在模板引擎自身,期間提到了狀態(tài)機(jī)、有限狀態(tài)機(jī)、編譯原理、詞法分析、單元測試、測試驅(qū)動(dòng)開發(fā)、面向?qū)ο笤O(shè)計(jì)等等概念,希望大家能夠有所收獲!誠然,如果您發(fā)現(xiàn)了錯(cuò)誤之處還請(qǐng)指出,歡迎挑刺!

    代碼下載:置換型模板引擎(4).zip


    4月9日之后我們將開始討論解釋型模板引擎,敬請(qǐng)關(guān)注!

    總結(jié)

    以上是生活随笔為你收集整理的由浅入深:自己动手开发模板引擎——置换型模板引擎(四)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。