浅谈领域特定语言

编程语言及工具

105人已加入

描述

  领域特定语言(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中的对象,而是对应的领域模型。对于领域驱动设计我会在后面的领域驱动设计的文章中介绍。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分