領域特定語言(domain-specific languages,簡稱DSL)
在定義DSL是什么的問題上,Flowler認為目前經常使用的一些特征,例如“關注于領域”、“有限的表現”和“語言本質”是非常模糊的。因此,唯一能夠確定DSL邊界的方法是考慮“一門語言的一種特定用法”和“該語言的設計者或使用者的意圖”:
如果XSLT的設計者將其設計為XML的轉換工具,那么我認為XSLT是一個DSL。如果一個用戶使用DSL的目的是該DSL所要達到的目的,那么它一個DSL,但是如果有人以通用的方式來使用一個DSL,那么它(在這種用法下)就不再是一個DSL了。
以Fowler的觀點,DSL首先是一種幫助用戶從一個系統中抽象出某些部分的工具。所以“當你意識到你需要一個組件,或者當你已經有了一個組件而你希望簡化操作它的方式的時候”,DSL是有用的。使用DSL確實提供了某些益處。DSL不僅提高了代碼的易讀性,讓開發者可以和領域專家更好的交流,而且是改變執行上下文的一種手段,例如:把邏輯從編譯時切換到運行時,或者當命令式編程不是很合適的時候轉用聲明式計算模型。
DSL包含哪些部分,有哪些分類
DSL主要分為三類:外部DSL、內部DSL,以及語言工作臺。
外部DSL是一種“不同于應用系統主要使用語言”的語言。外部DSL通常采用自定義語法,不過選擇其他語言的語法也很常見(XML就是一個常見選擇)。宿主應用的代碼會采用文本解析技術對使用外部DSL編寫的腳本進行解析。一些小語言的傳統UNIX就符合這種風格。可能經常會遇到的外部DSL的例子包括:正則表達式、SQL、Awk,以及像Struts和Hibernate這樣的系統所使用的XML配置文件。
內部DSL是一種通用語言的特定用法。用內部DSL寫成的腳本是一段合法的程序,但是它具有特定的風格,而且只用到了語言的一部分特性,用于處理整個系統一個小方面的問題。用這種DSL寫出的程序有一種自定義語言的風格,與其所使用的宿主語言有所區別。這方面最經典的例子是Lisp。Lisp程序員寫程序就是創建和使用DSL。Ruby社區也形成了顯著的DSL文化:許多Ruby庫都呈現出DSL的風格。特別是,Ruby最著名的框架Rails,經常被認為是一套DSL。
語言工作臺是一個專用的IDE,用于定義和構建DSL。具體來說,語言工作臺不僅用來確定DSL的語言結構,而且是人們編寫DSL腳本的編輯環境。最終的腳本將編輯環境和語言本身緊密結合在一起。
多年來,這三種風格分別發展了自己的社區。你會發現,那些非常擅長使用內部DSL的人,完全不了解如何構造外部DSL。我擔心這可能會導致人們不能采用最適合的工具來解決問題。我曾與一個團隊討論過,他們采用了非常巧妙的內部DSL處理技巧來支持自定義語法,但我相信,如果他們使用外部DSL的話,問題會變得簡單許多。但由于對如何構造外部DSL一無所知,他們別無選擇。因此,在本書中,把內部DSL和外部DSL講清楚對我來說格外重要,這樣你就可以了解這些信息,做出適當的選擇。(語言工作臺稍顯粗略,因為它們很新,尚在演化之中。)
另一種看待DSL的方式是:把它看做一種處理抽象的方式。在軟件開發中,我們經常會在不同的層面上建立抽象,并處理它們。建立抽象最常見的方式是實現一個程序庫或框架。操縱框架最常見的方式是通過命令/查詢式API調用。從這種角度來看,DSL就是這個程序庫的前端,它提供了一種不同于命令/查詢式API風格的操作方式。在這樣的上下文中,程序庫成了DSL的“語義模型”,因此,DSL經常伴隨著程序庫出現。事實上,我認為,對于構建良好的DSL 而言,語義模型是一個不可或缺的附屬物。
談及DSL,人們很容易覺得構造DSL很難。實際上,通常是難在構造模型上,DSL只是位于其上的一層而已。雖然讓DSL 工作良好需要花費一定的精力,但相對于構建底層模型,這一部分的付出要少多了。
1. 我們常常會看到這樣一種劃分:一方面是程序庫/框架或者組件的實現代碼;另一方面是配置代碼或組件組裝代碼。從本質上說,這種做法分開了公共代碼和可變代碼。用公共代碼構建一套組件,然后根據不同的目的進行配置。
2. “聲明式”是一個非常模糊的術語,但是它通常適應于所有遠離了命令式編程的方式。。。。遠離變量倒換,用xml的子元素表示狀態的動作和轉換。
3. DSL扮演領域專家和業務分析人員之間的交流媒介。。。
4. 文本DSL有兩種,稱為外部DSL和內部DSL。外部DSL是指,在主程序設計語言之外,用一種單獨的語言表示領域專用語言。內部DSL是指,用通用語言的語法表示的DSL;
5. 是什么讓內部DSL不同于通常的api呢?。。。連貫接口,這個術語強調這樣一個事實:內部DSL實際只是某種形式的api,只不過其設計考慮了連貫性難以琢磨的質量。
xx:后續在4.1節還會論述這個問題,DSL與api調用的區別,從這里看,兩者都提供一種抽象,但DSL在抽象的設計上考慮了連貫性。
在講完以上內容后,提出DSL由三部分組成,即語言,語義模型和代碼生成。語言只是用一種可讀的方式來組裝語義模型。
6. 我強烈建議,幾乎始終應該使用語義模型。。。。語義模型,清晰的將語言解析和結果語義的關注點切分開,。。。可以單獨推究狀態機的運作機制,增強和調試,無須估計語言。
DSL只是模型一個薄薄的門面
7.許多人用了代碼生成之后,就舍棄了語義模型,他們在解析 輸入文本之后,就直接產生生成的代碼。。。不推薦任何人這么做。語義模型的存在,可以將解析,執行語義以及代碼生成分開。
最后推薦一個開發DSL的語言工作臺,MetaEdit
為何需要DSL
DSL只是一種工具,關注點有限,無法像面向對象編程或敏捷方法論那樣,引發軟件開發思考方式的深刻變革。相反,它是在特定條件下有專門用途的一種工具。一個普通的項目可能在多個地方采用了多種DSL——事實上很多項目這么做了。
DSL有其自身的價值。當考慮采用DSL時,要仔細衡量它的哪些價值適合于我們的情況。
1)提高開發效率
DSL的核心價值在于,它提供了一種手段,可以更加清晰地就系統某部分的意圖進行溝通。拿格蘭特小姐控制器的定義來說,相比于采用命令–查詢API,DSL形式對我們而言更容易理解。
這種清晰并非只是審美追求。一段代碼越容易看懂,就越容易發現錯誤,也就越容易對系統進行修改。因此,我們鼓勵變量名要有意義,文檔要寫清楚,代碼結構要寫清晰。基于同樣的理由,我們應該也鼓勵采用DSL。
人們經常低估缺陷對生產率的影響。缺陷不僅損害軟件的外部質量,還浪費開發人員的時間去調查以及修復,降低開發效率,并使系統的行為異常,播下混亂的種子。DSL的受限表達性,使其難于犯錯,縱然犯錯,也易于發現。
模型本身可以極大地提升生產率。通過把公共代碼放在一起,它可以避免重復。首先,它提供了一種“用于思考問題”的抽象,這樣,更容易用一種可理解的方式指定系統行為。DSL提供了一種“對閱讀和操作抽象”更具表達性的形式,從而增強了這種抽象。DSL還可以幫助人們更好地學習使用API,因為它將人們的關注點轉移到怎樣將API方法整合在一起。
我還遇到過一個有趣的例子,使用DSL封裝一個棘手的第三方程序庫。當命令–查詢接口設計得很糟糕時,DSL 慣常的連貫性就得以凸現。此外,DSL只須支持客戶真正用到的部分,這大大降低了客戶開發人員學習的成本。
2)與領域專家的溝通
我相信,軟件項目中最困難的部分,也是項目失敗最常見的原因,就是開發團隊與客戶以及軟件用戶之間的溝通。DSL提供了一種清晰而準確的語言,可以有效地改善這種溝通。
相比于關于生產率的簡單爭論,改善溝通所帶來的好處顯得更加微妙。首先,很多DSL并不適用于溝通領域問題,比如,用于正則表達式或構建依賴關系的DSL,在這些情況下就不合適。只有一部分獨立DSL確實應用這種溝通手段。
當在這樣的場景下討論DSL時,經常會有人說:“好吧,現在我們不需要程序員了,領域專家可以自己指定業務規則。”我把這種論調稱為“COBOL謬論”——因為COBOL曾被人寄予這樣的厚望。這種爭論很常見,不過,我覺得這種爭論不值得在此重復。
雖然存在“COBOL謬論”,我仍然覺得DSL可以改善溝通。不是讓領域專家自己去寫DSL,但他們可以讀懂,從而理解系統做了什么。能夠閱讀DSL代碼,領域專家就可以指出問題所在。他們還可以同編寫業務規則的程序員更好地交流,也許,他們還可以編寫一些草稿,程序員們可以將其細化成適當的DSL規則。
但我不是說領域專家永遠不能編寫DSL。我遇見過很多團隊,他們成功地讓領域專家用DSL編寫了大量系統功能。但我仍然認為,使用DSL的最大價值在于,領域專家能夠讀懂。所以編寫DSL的第一步,應該專注于易讀性,這樣即便后續的目標達不到,我們也不會失去什么。
使用DSL是為了讓領域專家能夠看懂,這就引出了一個值得爭議的問題。如果希望領域專家理解一個“語義模型”的內容,可以將模型可視化。這時就要考慮一下,相比于支持一種DSL,是不是只使用可視化會是一種更有效的辦法。可視化對于DSL而言,是一種有益的補充。
讓領域專家參與構建DSL,與讓他們參與構建模型是同樣的道理。我發現,與領域專家一起構建模型能夠帶來很大的好處,在構建一種Ubiquitous Language [Evans DDD] 的過程中,程序員與領域專家之間可以深入溝通。DSL提供了另一種增進溝通的手段。隨著項目的不同,我們可能發現,領域專家可能會參與模型和DSL,也可能只參與 DSL。
實際上,有些人發現,即便不實現DSL,有一種描述領域知識的DSL,也能帶來很大的好處。即使只把它當做溝通平臺也可以獲益。
總的來說,讓領域專家參與構建DSL比較難,但回報很高。即使最終不能讓領域專家參與,但是開發人員在生產率方面的提升,也足以讓我們大受裨益,因此,DSL值得投入。
3)執行環境的改變
當談及將狀態機表述為XML的理由時,一個重要的原因是,狀態機定義可以在運行時解析,而非編譯時。在這種情況下,我們希望將代碼運行于不同的環境,這類理由也是使用DSL一個常見的驅動力。對于XML配置文件而言,將邏輯從編譯時移到運行時就是一個這樣的理由。
還有一些需要遷移執行環境的情況。我曾見過一個項目,它要從數據庫里找出所有滿足某種條件的合同,給它們打上標簽。他們編寫了一種DSL,以指定這些條件,并用它以Ruby語言組裝“語義模型”。如果用Ruby將所有合同讀入內存,再運行查詢邏輯,那會非常慢,但是團隊可以用語義模型的表示生成SQL,在數據庫里做處理。直接用SQL編寫規則,對開發人員都很困難,遑論業務人員。然而,業務人員可以讀懂(在這種情況下,甚至編寫)DSL里有關的表達式。
這樣用DSL常常可以彌補宿主語言的局限性,將事物以適宜的DSL形式表現出來,然后,生成可用于實際執行環境的代碼。
模型的存在有助于這種遷移。一旦有了一個模型,或者直接執行它,或者根據它產生代碼都很容易。模型可以由表單風格的界面創建,也可以由DSL創建。DSL相對于表單有一些優勢。在表述復雜邏輯方面,DSL比表單做得更好。而且,可以用相同的代碼管理工具,比如版本控制系統,管理這些規則。當規則經由表單輸入,存入數據庫中,版本控制就無能為力了。
下面會談及DSL的一個偽優點。我聽說,有人宣稱DSL的一個好處是,它能夠在不同的語言環境下執行相同的行為。一個人編寫了業務規則,然后生成C#或Java代碼,或者,描述校驗邏輯之后,在服務器端以C#形式運行,在客戶端則是JavaScript。這是一個偽優勢,因為僅僅使用模型就可以做到這一點,根本無需DSL。當然,DSL有助于理解這些規則,但那是另外一個問題。
4)其他計算模型
幾乎所有主流的編程語言都采用命令式的計算模型。這意味著,我們要告訴計算機做什么事情,按照怎樣的順序來做。通過條件和循環處理控制流,還要使用變量——確實,還有很多我們以為理所當然的東西。命令式計算模型之所以流行,是因為它們相對容易理解,也容易應用到許多問題上。然而,它并不總是最佳選擇。
狀態機是這方面的一個良好例子。可以采用命令式代碼和條件處理這種行為,也確實可以很好地構建出這種行為。但如果直接把它當做“狀態機”來思考,效果會更好。另外一個常見的例子是,定義軟件構建方式。我們固然可以用命令式邏輯實現它,但后來,人們發現用“依賴網絡”(比如,運行測試必須依賴于最新的編譯結果)解決會更容易。結果,人們設計出了專用于描述構建的語言(比如Make和Ant),其中將任務間的依賴關系作為主要的結構化機制。
你可能經常聽到,人們把非命令式方式稱為聲明式編程。之所以叫做聲明式,是因為這種風格讓人定義做什么,而不是用一堆命令語句來描述怎么做。
采用其他計算模型,并不一定非要有DSL。其他編程模型的核心行為也源自“語義模型”,正如前面所講的狀態機。然而,DSL還是能夠帶來很大的轉變,因為操作聲明式程序,組裝語義模型會容易一些。
總感覺項目組寫的代碼不規范,有時一個類能有幾千行代碼。那么應該如何規范代碼那?
領域特定語言的目標是主要關注我們應該做什么,而不是怎樣去實現某種特定的業務邏輯。
Linq簡化了代碼
如果使用c#開發則對應的領域特定語言就是Linq技術了。
這是個表示分組的對象,用于保存分類的名稱和產品數量。然后我們就會寫一些十分丑陋的代碼
DSL是對模型的一個有益的補充。
Dictionary《string, Grouping》 groups = new Dictionary《string, Grouping》();
foreach (Product p in products)
{
if (p.UnitPrice 》= 20)
{
if (!groups.ContainsKey(p.CategoryName))
{
Grouping r = new Grouping();
r.CategoryName = p.CategoryName;
r.ProductCount = 0;
groups[p.CategoryName] = r;
}
groups[p.CategoryName].ProductCount++;
}
}
List《Grouping》 result = new List《Grouping》(groups.Values);
result.Sort(delegate(Grouping x, Grouping y)
{
return
x.ProductCount 》 y.ProductCount ? -1 :
x.ProductCount 《 y.ProductCount ? 1 :
0;
});
不過如果這里我們使用DSL,也就是LINQ,就像這樣:
var result = products
.Where(p =》 p.UnitPrice 》= 20)
.GroupBy(p =》 p.CategoryName)
.OrderByDescending(g =》 g.Count())
.Select(g =》 new { CategoryName = g.Key, ProductCount = g.Count() });
方法鏈
其實我們在C#代碼中除了Linq,其他地方也可以做類似的設計,學名叫方法鏈(Method chaining),類型如下實現:
Object.DoSomething().DoSomethingElse().DoAnotherThing();
class Person
{
private string _name;
private byte _age;
public Person SetName(string name)
{
_name = name;
return this;
}
public Person SetAge(byte age)
{
_age = age;
return this;
}
public Person Introduce()
{
Console.WriteLine(“Hello my name is {0} and I am {1} years old.”, _name, _age);
return this;
}
}
//Usage:
static void Main()
{
Person user = new Person();
// Output of this sequence will be: Hello my name is Peter and I am 21 years old.
user.SetName(“Peter”).SetAge(21).Introduce();
}
學習Linq之前需要學習的一些必備知識
var 關鍵字
C#擴展方法和擴展庫
namespace MyExtensionsLibrary
{
public static class Class1
{
public static void DispAssembly(this object obj)
{
Console.WriteLine(“該對象所在的程序集位置是:{0}/n-》{1}‘/t”, obj.GetType().Name, System.Reflection.Assembly.GetAssembly(obj.GetType()));
}
public static int ReverseInt(this int i)
{
char []digits=i.ToString ().ToCharArray ();
Array .Reverse(digits );
string newDigits=new string (digits );
return int.Parse (newDigits );
}
}
}
對象初始化器
var point3=new Point{x=7,y=4};
匿名類型
匿名對象
var Apeople=new{Sex=”male”,Name=”Linc”,Age=”26”};
匿名方法
public partial class Form1 : Form
{
delegate void Printer(string s);
public Form1()
{
InitializeComponent();
// 匿名方法
Printer p = delegate(string j)
{
textBox1.Text += j+“/r/n”;
};
// 匿名委托調用后返回的結果
p(“The delegate using the anonymous method is called.”);
//命名方法
p = new Printer(DoWork);
// 傳統的調用方式
p(“The delegate using the named method is called.”);
}
void DoWork(string k)
{
textBox1.Text += k + “/r/n”;
}
}
Lambda表達式
Lambda表達式語法看上去真怪異,說白了是更好的匿名方法。廢話不多說,先來看看使用匿名方法:
//首先使用集合初始化語法建立一個整型列表
List《int》 list = new List《int》() { 1, 2, 3, 4, 5, 6, 7 };
//匿名方法粉墨登場
List《int》 oddNumbers = list.FindAll(
delegate(int i)
{
return (i % 2) != 0;
}
);//匿名方法結束
foreach (var oddNumber in oddNumbers)
{
//輸出奇數
Console.WriteLine(oddNumber);
}
觀察上面的匿名方法,我們使用delegate,而且還要保證輸入參數的類型匹配,這種語法確實還是讓人覺得冗長。下面看看Lambda表達式是如何簡化FindAll()方法的:
//通過Lambda表達式就一句就搞定了!多神奇啊!傳統的委托語法通通消失了
List《int》 oddNumbers = list.FindAll(i =》 (i % 2) != 0);
解剖Lambda表達式
i =》 (i % 2) != 0
你一定注意到了Lambda表達式的 =》 標記(讀作 goes to),它的前面是一個參數列表,后面是一個表達式。
很明顯,前面的參數列表并沒有定義參數的類型(由編譯器根據上下文推斷出i是一個整型),所以它是隱式的。當然,我們也可以顯示定義: (int i)=》(i%2)!=0);
我們這里參數列表只有一個參數,所以那個括號可以被省略。
Linq
自己也沒有把所有資料都看完,個人對于Linq的用法大致是這樣設想的,我們在做項目時會建立許多聚合根,如果我們的查詢會跨多個聚合根則使用存儲過程,否則使用Linq進行查詢。Linq有個LinqPad的小工具用于測試,
聚合與組合
組合的話是類似共生共滅的關系,例如大雁和大雁的翅膀。對于我們營收系統來說,就好比營業賬和營業賬子表
而聚合則沒有這么強的從屬關系,比如用戶和冊本信息,冊本信息作為一個用戶的只讀屬性存在,但也可以單獨操作冊本信息。
賬戶可以獨立存在,但用戶又有一個賬戶信息的只讀屬性
系統架構
這里說的系統架構主要是邏輯上的分層,不是物理上的。主要是想利用Entity Framework在系統架構里面,替換以前的codesmith生成entity和dal,同時想引入領域模型的概念,不直接操作entity framework中的對象,而是對應的領域模型。對于領域驅動設計我會在后面的領域驅動設計的文章中介紹。
評論
查看更多