谈谈 DSL 以及 DSL 的应用(以 CocoaPods 为例)

最近在公司作了一次有关 DSL 在 iOS 开发中的应用的分享,这篇文章会简单介绍此次分享的内容。css

由于 DSL 以及 DSL 的界定自己就是一个比较模糊的概念,因此不免有与他人观点意见相左的地方,若是有不一样的意见,咱们能够具体讨论。html

此次文章的题目虽然是谈谈 DSL 以及 DSL 的应用,不过文章中主要侧重点仍然是 DSL,会简单介绍 DSL 在 iOS 开发中(CocoaPods)是如何应用的。前端

没有银弹?

1987 年,IBM 大型电脑之父 Fred Brooks 发表了一篇关于软件工程中的论文 No Silver Bullet—Essence and Accidents of Software Engineering 文中主要围绕这么一个观点:没有任何一种技术或者方法能使软件工程的生产力在十年以内提升十倍。ios

There is no single development, in either technology or management technique, which by itself promises even one order-of-magnitude improvement within a decade in productivity, in reliability, in simplicity.git

时至今日,咱们暂且不谈银弹在软件工程中是否存在(这句话在老板或者项目经理要求加快项目进度时,仍是十分好用的),做为一个开发者也不是很关心这种抽象的理论,咱们更关心的是开发效率可否有实质的提高。github

silver-bullet

而今天要介绍的 DSL 就能够真正的提高生产力,减小没必要要的工做,在一些领域帮助咱们更快的实现需求。正则表达式

DSL 是什么?

笔者是在两年之前,在大一的一次分享上听到 DSL 这个词的,可是当时并无对这个名词有多深的理解与认识,听过也就忘记了,可是最近作的一些开源项目让我从新想起了 DSL,也是此次分享题目的由来。数据库

DSL 实际上是 Domain Specific Language 的缩写,中文翻译为领域特定语言(下简称 DSL);而与 DSL 相对的就是 GPL,这里的 GPL 并非咱们知道的开源许可证,而是 General Purpose Language 的简称,即通用编程语言,也就是咱们很是熟悉的 Objective-C、Java、Python 以及 C 语言等等。express

Wikipedia 对于 DSL 的定义仍是比较简单的:编程

A specialized computer language designed for a specific task.

为了解决某一类任务而专门设计的计算机语言。

与 GPL 相对,DSL 与传统意义上的通用编程语言 C、Python 以及 Haskell 彻底不一样。通用的计算机编程语言是能够用来编写任意计算机程序的,而且能表达任何的可被计算的逻辑,同时也是 图灵完备 的。

这一小节中的 DSL 指外部 DSL,下一节中会介绍 内部 DSL/嵌入式 DSL

可是在里所说的 DSL 并非图灵完备的,它们的表达能力有限,只是在特定领域解决特定任务的。

A computer programming language of limited expressiveness focused on a particular domain.

另外一个世界级软件开发大师 Martin Fowler 对于领域特定语言的定义在笔者看来就更加具体了,DSL 经过在表达能力上作的妥协换取在某一领域内的高效

而有限的表达能力就成为了 GPL 和 DSL 之间的一条界限。

几个栗子

最多见的 DSL 包括 Regex 以及 HTML & CSS,在这里会对这几个例子进行简单介绍

  • Regex
    • 正则表达式仅仅指定了字符串的 pattern,其引擎就会根据 pattern 判断当前字符串跟正则表达式是否匹配。 regex
  • SQL
    • SQL 语句在使用时也并无真正的执行,咱们输入的 SQL 语句最终还要交给数据库来进行处理,数据库会从 SQL 语句中读取有用的信息,而后从数据库中返回使用者指望的结果。
  • HTML & CSS
    • HTML 和 CSS 只是对 Web 界面的结构语义和样式进行描述,虽然它们在构建网站时很是重要,可是它们并不是是一种编程语言,正相反,咱们能够认为 HTML 和 CSS 是在 Web 中的领域特定语言。

Features

上面的几个🌰明显的缩小了通用编程语言的概念,可是它们确实在本身领域表现地很是出色,由于这些 DSL 就是根据某一个特定领域的特色塑造的;而通用编程语言相比领域特定语言,在设计时是为了解决更加抽象的问题,而关注点并不仅是在某一个领域。

上面的几个例子有着一些共同的特色:

  • 没有计算和执行的概念;
  • 其自己并不须要直接表示计算;
  • 使用时只须要声明规则、事实以及某些元素之间的层级和关系;

虽然了解了 DSL 以及 DSL 的一些特性,可是,到目前为止,咱们对于如何构建一个 DSL 仍然不是很清楚。

构建 DSL

DSL 的构建与编程语言其实比较相似,想一想咱们在从新实现编程语言时,须要作那些事情;实现编程语言的过程能够简化为定义语法与语义,而后实现编译器或者解释器的过程,而 DSL 的实现与它也很是相似,咱们也须要对 DSL 进行语法与语义上的设计。

compile

总结下来,实现 DSL 总共有这么两个须要完成的工做:

  1. 设计语法和语义,定义 DSL 中的元素是什么样的,元素表明什么意思
  2. 实现 parser,对 DSL 解析,最终经过解释器来执行

以 HTML 为例,HTML 中全部的元素都是包含在尖括号 <> 中的,尖括号中不一样的元素表明了不一样的标签,而这些标签会被浏览器解析成 DOM 树,再通过一系列的过程调用 Native 的图形 API 进行绘制。

dom-tree

再好比,咱们使用下面这种方式对一个模型进行定义,实现一个 ORM 领域的 DSL:

define :article do
  attr :name
  attr :content
  attr :upvotes, :int

  has_many :comments
end复制代码

在上面的 DSL 中,使用 define 来定义一个新的模型,使用 attr 来为模型添加属性,使用 has_many 创建数据模型中的一对多关系;咱们可使用 DSL 对这段“字符串”进行解析,而后交给代码生成器来生成代码。

public struct Article {
    public var title: String
    public var content: String
    public var createdAt: Date

    public init(title: String, content: String, createdAt: Date)

    static public func new(title: String, content: String, createdAt: Date) -> Article
    static public func create(title: String, content: String, createdAt: Date) -> Article?
    ...
}复制代码

这里建立的 DSL 中的元素数量很是少,只有 define attr 以及 has_many 等几个关键字,可是经过这几个关键字就能够完成在模型层须要表达的绝大部分语义。

设计原则和妥协

DSL 最大的设计原则就是简单,经过简化语言中的元素,下降使用者的负担;不管是 Regex、SQL 仍是 HTML 以及 CSS,其说明文档每每只有几页,很是易于学习和掌握。可是,由此带来的问题就是,DSL 中缺少抽象的概念,好比:模块化、变量以及方法等。

抽象的概念并非某个领域所关注的问题,就像 Regex 并不须要有模块、变量以及方法等概念。

因为抽象能力的缺少,在咱们的项目规模变得愈来愈大时,DSL 每每知足不了开发者的需求;咱们仍然须要编程语言中的模块化等概念对 DSL 进行补充,以此解决 DSL 并非真正编程语言的问题。

css-sass

在当今的 Web 前端项目中,咱们在开发大规模项目时每每不会直接手写 CSS 文件,而是会使用 Sass 或者 Less 为 CSS 带来更强大的抽象能力,好比嵌套规则,变量,混合以及继承等特性。

nav {
  ul {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  li { display: inline-block; }

  a {
    display: block;
    padding: 6px 12px;
    text-decoration: none;
  }
}复制代码

也就是说,在使用 DSL 的项目规模逐渐变大时,开发者会经过增长抽象能力的方式,对已有的 DSL 进行拓展;可是这种扩展每每须要从新实现通用编程语言中的特性,因此通常状况下都是比较复杂的。

Embedded DSL(嵌入式 DSL)

那么,是否有一种其它的方法为 DSL 快速添加抽象能力呢?而这也就是这一小节的主题,嵌入式 DSL。

在上一节讲到的 DSL 其实能够被称为外部 DSL;而这里即将谈到的嵌入式 DSL 也有一个别名,内部 DSL。

这二者最大的区别就是,内部 DSL 的实现每每是嵌入一些编程语言的,好比 iOS 的依赖管理组件 CocoaPods 和 Android 的主流编译工具 Gradle,前者的实现是基于 Ruby 语言的一些特性,然后者基于 Groovy。

cocoapods

CocoaPods 以及其它的嵌入式 DSL 使用了宿主语言(host language)的抽象能力,而且省去了实现复杂语法分析器(Parser)的过程,并不须要从新实现模块、变量等特性。

嵌入式 DSL 的产生其实模糊了框架和 DSL 的边界,不过这二者看起来也没有什么比较明显的区别;不过,DSL 通常会使用宿主语言的特性进行创造,在设计 DSL 时,也不会考虑宿主语言中有哪些 API 以及方法,而框架通常都是对语言中的 API 进行组合和再包装。

咱们没有必要争论哪些是框架,哪些是 DSL,由于这些争论并无什么意义。

Rails 和 Embedded DSL

最出名也最成功的嵌入式 DSL 应该就是 Ruby on Rails 了,虽然对于 Rails 是不是 DSL 有争议,不过 Rails 为 Web 应用的建立提供大量的内置的支撑,使咱们在开发 Web 应用时变得很是容易。

rails

Ruby、 DSL 和 iOS

为了保证这篇文章的完整性,这一小节中有的一些内容都出自上一篇文章 CocoaPods 都作了什么?

笔者同时做为 iOS 和 Rails 开发者接触了很是多的 DSL,而在 iOS 开发中最多见的 DSL 就是 CocoaPods 了,而这里咱们以 CocoaPods 为例,介绍如何使用 Ruby 创造一个嵌入式 DSL。

Why Ruby?

看到这里有人可能会问了,为何使用 Ruby 创造嵌入式 DSL,而不是使用 C、Java、Python 等等语言呢,这里大概有四个缘由:

  • 一切皆对象的特性减小了语言中的元素,不存在基本类型、操做符;
  • 向 Ruby 方法中传入代码块很是方便;
  • 做为解释执行的语言,eval 模糊了数据和代码的边界;
  • 不对代码的格式进行约束,同时一些约定减小了代码中的噪音。

一切皆对象

在许多语言,好比 Java 中,数字与其余的基本类型都不是对象,而在 Ruby 中全部的元素,包括基本类型都是对象,同时也不存在运算符的概念,所谓的 1 + 1,其实只是 1.+(1) 的语法糖而已。

得益于一切皆对象的概念,在 Ruby 中,你能够向任意的对象发送 methods 消息,在运行时自省,因此笔者在每次忘记方法时,都会直接用 methods 来“查阅文档”:

2.3.1 :003 > 1.methods
 => [:%, :&, :*, :+, :-, :/, :<, :>, :^, :|, :~, :-@, :**, :<=>, :<<, :>>, :<=, :>=, :==, :===, :[], :inspect, :size, :succ, :to_s, :to_f, :div, :divmod, :fdiv, :modulo, ...]复制代码

好比在这里向对象 1 调用 methods 就会返回它能响应的全部方法。

一切皆对象不只减小了语言中类型的数量,消灭了基本数据类型与对象之间的边界;这一律念同时也简化了组成语言的元素,这样 Ruby 中只有对象和方法,这两个概念,极大下降了这门语言的复杂度:

  • 使用对象存储状态
  • 对象之间经过方法通讯

block

Ruby 对函数式编程范式的支持是经过 block,这里的 block 和 Objective-C 中的 block 有些不一样。

首先 Ruby 中的 block 也是一种对象,即 Proc 类的实例,也就是全部的 block 都是 first-class 的,能够做为参数传递,返回。

下面的代码演示了两种向 Ruby 方法中传入代码块的方式:

def twice(&proc)
    2.times { proc.call() } if proc
end

def twice
    2.times { yield } if block_given?
end复制代码

yield 会调用外部传入的 block,block_given? 用于判断当前方法是否传入了 block

twice do 
    puts "Hello"
end

twice { puts "hello" }复制代码

twice 方法传入 block 也很是简单,使用 doend 或者 {} 就能够向任何的 Ruby 方法中传入代码块。

eval

早在几十年前的 Lisp 语言就有了 eval 这个方法,这个方法会将字符串当作代码来执行,也就是说 eval 模糊了代码与数据之间的边界。

> eval "1 + 2 * 3"
 => 7复制代码

有了 eval 方法,咱们就得到了更增强大的动态能力,在运行时,使用字符串来改变控制流程,执行代码并能够直接利用当前语言的解释器;而不须要去手动解析字符串而后执行代码。

格式和约定

编写 Ruby 脚本时并不须要像 Python 同样对代码的格式有着严格的规定,没有对空行、Tab 的要求,彻底能够想怎么写就怎么写,这样极大的增长了 DSL 设计的可能性。

同时,在通常状况下,Ruby 在方法调用时并不须要添加括号:

puts "Wello World!"
puts("Hello World!")复制代码

这样减小了 DSL 中的噪音,可以帮助咱们更加关心语法以及语义上的设计,下降了使用者出错的可能性。

最后,Ruby 中存在一种特殊的数据格式 Symbol

> :symbol.to_s
 => "symbol"
> "symbol".to_sym
 => :symbol复制代码

Symbol 能够经过 Ruby 中内置的方法与字符串之间无缝转换。那么做为一种字符串的替代品,它的使用也可以下降使用者出错的成本并提高使用体验,咱们并不须要去写两边加上引号的字符串,只须要以 : 开头就能建立一个 Symbol 对象。

Podfile 是什么

对 Ruby 有了一些了解以后,咱们就能够再看一下使用 CocoaPods 的工程中的 Podfile 究竟是什么了:

source 'https://github.com/CocoaPods/Specs.git'

target 'Demo' do
    pod 'Mantle', '~> 1.5.1'
    ...
end复制代码

若是不了解 iOS 开发后者没有使用过 CocoaPods,笔者在这里简单介绍一下这个文件中的一些信息。

source 能够看做是存储依赖元信息(包括依赖的对应的 GitHub 地址)的源地址;

target 表示须要添加依赖的工程的名字;

pod 表示依赖,Mantle 为依赖的框架,后面是版本号。

上面是一个使用 Podfile 定义依赖的一个例子,不过 Podfile 对约束的描述实际上是这样的:

source('https://github.com/CocoaPods/Specs.git')

target('Demo') do
    pod('Mantle', '~> 1.5.1')
    ...
end复制代码

Podfile 中对于约束的描述,其实均可以看做是代码的简写,在解析时会当作 Ruby 代码来执行。

简单搞个 Embedded DSL

使用 Ruby 实现嵌入式 DSL 通常须要三个步骤,这里以 CocoaPods 为例进行简单介绍:

  • 建立一个 Podfile 中“代码”执行的上下文,也就是一些方法;
  • 读取 Podfile 中的内容到脚本中;
  • 使用 eval 在上下文中执行 Podfile 中的“代码”;

原理

CocoaPods 对于 DSL 的实现基本上就是咱们建立一个 DSL 的过程,定义一系列必要的方法,好比 sourcepod 等等,创造一个执行的上下文;而后去读存储 DSL 的文件,而且使用 eval 执行。

信息的传递通常都是经过参数来进行的,好比:

source 'https://github.com/CocoaPods/Specs.git'复制代码

source 方法的参数就是依赖元信息 Specs 的 Git 地址,在 eval 执行时就会被读取到 CocoaPods 中,而后进行分析。

实现

下面是一个很是常见的 Podfile 内容:

source 'http://source.git'
platform :ios, '8.0'

target 'Demo' do
    pod 'AFNetworking'
    pod 'SDWebImage'
    pod 'Masonry'
    pod "Typeset"
    pod 'BlocksKit'
    pod 'Mantle'
    pod 'IQKeyboardManager'
    pod 'IQDropDownTextField'
end复制代码

由于这里的 sourceplatformtarget 以及 pod 都是方法,因此在这里咱们须要构建一个包含上述方法的上下文:

# eval_pod.rb
$hash_value = {}

def source(url)
end

def target(target)
end

def platform(platform, version)
end

def pod(pod)
end复制代码

使用一个全局变量 hash_value 存储 Podfile 中指定的依赖,而且构建了一个 Podfile 解析脚本的骨架;咱们先不去完善这些方法的实现细节,先尝试一下读取 Podfile 中的内容并执行 eval 看看会不会有问题。

eval_pod.rb 文件的最下面加入这几行代码:

content = File.read './Podfile'
eval content
p $hash_value复制代码

这里读取了 Podfile 文件中的内容,并把其中的内容当作字符串执行,最后打印 hash_value 的值。

$ ruby eval_pod.rb复制代码

运行这段 Ruby 代码虽然并无什么输出,可是并无报出任何的错误,接下来咱们就能够完善这些方法了:

def source(url)
    $hash_value['source'] = url
end

def target(target)
    targets = $hash_value['targets']
    targets = [] if targets == nil
    targets << target
    $hash_value['targets'] = targets
    yield if block_given?
end

def platform(platform, version)
end

def pod(pod)
    pods = $hash_value['pods']
    pods = [] if pods == nil
    pods << pod
    $hash_value['pods'] = pods
end复制代码

在添加了这些方法的实现以后,再次运行脚本就会获得 Podfile 中的依赖信息了,不过这里的实现很是简单的,不少状况都没有处理:

$ ruby eval_pod.rb
{"source"=>"http://source.git", "targets"=>["Demo"], "pods"=>["AFNetworking", "SDWebImage", "Masonry", "Typeset", "BlocksKit", "Mantle", "IQKeyboardManager", "IQDropDownTextField"]}复制代码

不过使用 Ruby 构建一个嵌入式 DSL 的过程大概就是这样,使用语言内建的特性来进行创做,创造出一个在使用时看起来并不像代码的 DSL。

写在后面

在最后,笔者想说的是,当咱们在某一个领域常常须要解决重复性问题时,能够考虑实现一个 DSL 专门用来解决这些相似的问题。

而使用嵌入式 DSL 来解决这些问题是一个很是好的办法,咱们并不须要从新实现解释器,也能够利用宿主语言的抽象能力。

同时,在嵌入式 DSL 扩展了 DSL 的范畴以后,不要纠结于某些东西究竟是框架仍是领域特定语言,这些都不重要,重要的是,在遇到了某些问题时,咱们可否跳出来,使用文中介绍的方法减轻咱们的工做量。

Reference

其它

GitHub Repo:iOS-Source-Code-Analyze

Follow: Draveness · GitHub

Source: draveness.me/dsl

相关文章
相关标签/搜索