软件设计之道(三)---语言的三步设计

642 阅读6分钟

语言的三步设计

Andrew Hunt和David Thomas在《程序员修炼之道》(The Pragmatic Programmer)中给程序员们提了一项重要的建议:每年至少学习一门新语言。

其实,程序设计语言本身也是一个软件,它也包含模型、接口和实现。而我们学习程序设计语言主要是为了学习程序设计语言提供的编程模型,比如:不同的程序组织方式,不同的控制结构等等。因为不同的编程模型会带给你不同的思考方式。

模型

语言的模型.png

接口

一说起程序设计语言的接口,你的直观印象肯定是程序设计语言的语法,那是一个你已经很熟悉的话题了,但程序设计语言还有一个你可能都不曾留意的接口:程序库。

程序库最初只是为了消除重复。后来,逐渐有了标准库,然后有了大量的第三方库,进而发展出包管理器。

如果通用性足够好,一些经过大量实践验证过的程序库甚至会变成语言的语法,而一些语法解决得不够好的地方,又会出现新的程序库去探索新的解决方案。所以,语言设计就是程序库设计,程序库设计就是语言设计。二者相互促进,不断发展。

当你开始学习如何编写程序库,你对软件设计的理解就会踏上一个新的台阶。

语言的接口.png

实现

程序设计语言的实现就是支撑程序运行的部分:运行时,也有人称之为运行时系统,或运行时环境,它主要是为了实现程序设计语言的执行模型。

相比于语法和程序库,我们在学习语言的过程中,对运行时的关注较少。因为不理解语言的实现依然不影响我们写程序,那我们为什么还要学习运行时呢?因为运行时,是我们做软件设计的地基。 你可能会问,软件设计的地基不应该是程序设计语言本身吗?并不是,一些比较基础的设计,仅仅了解到语言这个层面是不够的。

我用个例子来进行说明,我曾经参与过一个开源项目:在JVM上运行Ruby。这种行为肯定不是 Java语言支持的,为了让Ruby能够运行在JVM上,我们将Ruby的代码编译成了Java的字节码,而字节码就属于运行时的一部分。

你看,做设计真正的地基,并不是程序设计语言,而是运行时,有了对于运行时的理解,我们甚至可以做出语言本身不支持的设计。而且理解了运行时,我们可以成为一个更好的程序员,真正做到对自己编写的代码了如指掌。

理解运行时,可以将 “程序如何运行” 作为主线,将相关的知识贯穿起来。我们从了解可执行文件的结构开始,然后了解程序加载机制,知道加载器的运作和内存布局。然后,程序开始运行,我们要知道程序的运行机制,了解字节码,形成一个整体认识。最后,还可以根据需要展开各种细节,做深入的了解。

有一些语言的运行时还提供了一些语言层面的编程接口,程序员们可以与运行时进行交互,甚至拥有超过语言本身的能力。这些接口有的是以程序库的方式提供,有的则是以规范的方式提供。如果你是一个程序库的开发者,这些接口可以帮助你写出更优雅的程序。比如AOP,ASM的程序库,具有了操作字节码的能力。

语言的实现.png

DSL

程序设计语言的发展趋势,就是离计算机本身越来越远,而离要解决的问题越来越近。但通用程序设计语言无论怎样逼近要解决的问题,它都不可能走得离问题特别近,因为通用程序设计语言不可能知道具体的问题是什么。

这给具体的问题留下了一个空间,如果能把设计做到极致,它就能成为一门语言,填补这个空间。注意,这里用的并不是比喻,而是真的成为一门语言,一门解决一个特定问题的语言。这种语言就是领域特定语言(Domain Specific Language,简称 DSL),它是一种用于某个特定领域的程序设计语言。这种特定于某个领域是相对于通用语言而言的,通用语言可以横跨各个领域,我们熟悉的大多数程序设计语言都是通用语言。DSL只要做到满足特定领域的业务需求,就足以缩短问题和解决方案之间的距离,降低理解的门槛。比如正则表达式,就是一种用于文本处理这个特定领域的DSL。

想要实现一个DSL,可以这么说,DSL的语法本身都是次要的,模型才是第一位的。当你有了模型之后,所谓的构建DSL,就相当于设计一个接口,将模型的能力暴露出来。

既然是接口,形式就可以有很多种,我们经常能接触到的DSL主要有两种:外部DSL和内部 DSL。外部DSL和内部DSL的区别就在于,DSL采用的是不是宿主语言(Host Language)。你可以这么理解,假设你的模型主要是用Java写的,如果DSL用的就是Java语言,它就是内部DSL,如果DSL用的不是Java,比如,你自己设计了一种语法,那它就是外部DSL。

代码的表达性

创建一个Computer的实例,如果用普通风格的代码写出来,应该是这个样子:

 Processor p = new Processor(2, 2500, Processor.Type.i386); 
 Disk d1 = new Disk(150, Disk.UNKNOWN_SPEED, null);
 Disk d2 = new Disk(75, 7200, Disk.Interface.SATA);
 return new Computer(p, d1, d2);

而用内部 DSL 写出来,则是这种风格:

 computer() 
   .processor()
     .cores(2) 
     .speed(2500) 
     .i386()
   .disk()
     .size(150)
   .disk()
    .size(75)
    .speed(7200) 
    .sata()
 .end();

之所以会觉得这种一连串的方法调用可以接受,一个重要的原因是,这段代码并不是在做动作,而是在进行声明。做动作是在说明怎么做(How),而声明的代码则是在说做什么(What)。二者的抽象级别是不同的,“怎么做”是一种实现,而“做什么”则体现着意图。将意图与实现分离开来,是内部DSL与普通的程序代码一个重要的区别,同样,这也是一个好设计的考虑因素。在设计DSL时,重点是要体现出意图。抛开是否要实现一个DSL不说,的确,程序员在写代码时应该关注代码的表达能力,而这也恰恰是很多程序员忽略的,同时也是优秀程序员与普通程序员拉开差距的地方。

分离意图和实现其实也是一个重要的设计原则,是的,想写好代码,一定要懂得设计

DSL.png