白话 Ruby 与 DSL 以及在 iOS 开发中的运用

阅读本文不须要预先掌握 Ruby 与 DSL 相关的知识html

何为 DSL

DSL(Domain Specific Language) 翻译成中文就是:“领域特定语言”。首先,从定义就能够看出,DSL 也是一种编程语言,只不过它主要是用来处理某个特定领域的问题。ios

广为人知的编程语言有 C、Java、PHP 等,他们被称为 GPL(General Purpose Language),即通用目的语言。与这些语言相比,DSL 相对显得比较神秘,他们中的大多数甚至连一个名字都没有。这主要是由于 DSL 一般用来处理某个特定的、很小的领域中的问题,所以起名字这事没有太大的必要和意义。git

说了这么多废话, 必定有读者在想:“能不能举个例子讲解一下,什么是 DSL”。实际上,DSL 只是对一类语言的描述,它能够很是简单:github

UIView (0, 0, 100, 100) black
UILabel (50, 50, 200, 200) yellow
……复制代码

好比这就是我本身随便编的一个语言。它的语法看上去很奇怪,不过这不是重点。语言的根本目的是传递信息。数据库

为何要用 DSL

其实从上面的代码中已经能够比较出 DSL 和 GPL 的特色了。DSL 语法更加简洁,好比能够没有括号(这取决于你如何设计),所以开发、阅读的效率更高。但做为代价,DSL 调试很麻烦,很难作类型检查,所以几乎不可思议能够用 DSL 开发一个大型的程序。编程

若是同时接触过编译型语言和脚本语言,你能够把 DSL 理解为一种比脚本语言更加轻量、灵活的语言。swift

DSL 的执行过程

了解过 C 语言的开发者应该知道,从 C 语言源码到最后的可执行文件,须要通过预编译、编译(词法分析、语法分析、语义分析)、汇编、连接等步骤,最终生成 CPU 相关的机器码,也就是一堆 0 和 1。ruby

脚本语言不须要编译(有些也能够编译),他们在运行时被解释,固然也须要作词法分析和语法分析,最终生成机器码。闭包

因而问题来了,自定义的 DSL 如何被执行呢?app

对于词法分析和语法分析,因为语言简单,一般只是少数关键字,即便使用最简单的字符串解析,工做量和复杂度也在可接受的范围内。然而最后生成汇编代码就显得不是颇有必要了,DSL 的特色不是追求执行效率,而是高效,对开发者友好。

所以一种常见的作法是,用别的语言(能够理解为宿主语言)来解析 DSL,并执行宿主语言。继续以上面的 DSL 为例,咱们能够用 OC 读取这个文本文件,了解到咱们要建立一个 UIView 对象,所以会执行如下代码:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
view.backgroundColor = [UIColor blackColor];复制代码

如何实现 DSL

能够看到,DSL 的定义与实现毫无技术难度可言,与其说是一门语言,不如说是一种信息的标记格式,一种存储信息的协议。从这个角度来讲,JSON、XML 等数据格式也能够被称为 DSL。

然而,随着关键字数量的增多,对 DSL 的解析难度迅速提升。举个简单的例子,Cocoa 框架下的控件类型有不少,所以在解析上述 DSL 时就须要考虑不少状况。这显然与 DSL 的初衷不符。

有没有一种快速实现 DSL 的方法呢?选择 ruby 必定程度上能够解决上述问题。在解释为何恰恰选择 ruby 以前,首先介绍一些基础知识。

Ruby

这篇文章不是用来介绍 Ruby 语法的,感兴趣的读者能够阅读 《七周七语言》 或者 《松本行弘的程序世界》这两本书的前面几个章节来入门 Ruby,进阶教程推荐 《Ruby 元编程》。

本文主要介绍为什么 Ruby 常常做为宿主语言,被用来实现 DSL,用一句话归纳就是:

DSL 其实就是 Ruby 代码

上文说过,实现 DSL 的主要难度在于利用宿主语言解析 DSL 的语法,而借助 Ruby 实现的 DSL,其自己就是 Ruby 代码,只是看起来比较像 DSL。这样在执行的时候,咱们彻底借助了 Ruby 解释器的力量,而不须要手动分析其中的语法结构。

借用 Creating a Ruby DSL 这篇文章中的例子,假设咱们想写一段 HTML 代码:

<html>
  <body>
    <div id="container">
      <ul class="pretty">
        <li class="active">Item 1</li>
        <li>Item 2</li>
      </ul>
    </div>
  </body>
</html>复制代码

但又感受手写代码太麻烦,但愿简化它,因此使用一个自创的 DSL:

html = HTMLMaker.new.document do
  body do
    div id: "container" do
      ul class: "pretty" do
        li "Item 1", class: :active
        li "Item 2"
      end
    end
  end
end
# 这个 html 变量是一个字符串,值就是上面的 HTML 文档复制代码

不熟悉 Ruby 语法的读者可能没法一眼看出这段看上去像是文本的内容,实际上是 Ruby 代码。为何恰恰是 Ruby,而不是 Objective-C 或者 C++ 这些语言呢?我总结为如下两点:

  1. Ruby 自身的语法特性
  2. Ruby 具有元编程的能力

语法简介

首先,Ruby 调用函数能够不用把参数放在括号中:

def say(word)
  puts word
end

say "Hello"
# 调用 say 函数会输出 "Hello"复制代码

这就保证了语法的简洁,看上去像是一门 DSL。

另外要提到的一点是 Ruby 中的闭包。与 Objective-C 和 Swift 不一样的是,Ruby 的闭包能够写在 do … end 代码块,而不是必须放在大括号中:

(1..10).each do |i|
  puts "Number = #{i}"
end

# 输出十行,每行的格式都是 "Number = i"复制代码

大括号看上去就像是一门比较复杂的语言,而 do … end 会更容易阅读一些。

Ruby 元编程

元编程是 Ruby 的精髓之一。咱们见过不少以“元”开头的单词,好比 “元数据”、“元类”、“元信息”。这些词汇看上去很难理解,其实只要把 “元xx” 当作 “关于xx的xx”,就很容易理解了。

以元数据为例,它表示“关于数据的数据”。好比个人 ID 是 bestswifter,它是一个数据。我还能够说这个单词中有两个字母 s,一共有 11 个字母等等。这些也是数据,而且是关于数据(bestswifter)的数据,所以能够被称为元数据。

在 runtime 中常常提到的元类,也就是关于类的类。因此存储了类的方法和属性。

而所谓的元编程,天然指的就是“关于编程的编程”。编程是指用一段代码输出某个结果,而关于编程的编程则能够理解为经过编程的方式来产生这段代码。

在实际开发时,元编程一般以两种形式体现出他的威力:

  1. 提供反射的功能,经过 API 来提供对运行时环境的访问和修改能力
  2. 提供执行字符串格式代码的能力

在 Ruby 中,咱们能够随意为任何一个类添加、修改甚至删除方法。调用不存在方法时,能够统一进行转发:

class TestMissing
  def method_missing(m, *args, &block)
    puts "方法名:#{m},参数:#{args},闭包:#{block}"
  end
end

TestMissing.new.say "Hello", "World" do
  puts "Hello, world"
end
# 方法名:say,参数:["Hello", "World"],闭包:#<Proc:0x007feeea03cb00@t.ruby:7>复制代码

可见,当调用不存在的方法 say 时,会被转发到类的 method_missing 方法中,而且能够很容易的获取到方法名称和参数。

有必定 iOS 开发经验的读者会马上想到,这哪是元编程,明明就是 runtime。确实,相比于静态语言好比 Java、Swift 的反射机制而言,Objective-C 的 runtime 提供了更强大的功能,它不只能够自省,还能动态的进行修改。固然这也是由语言特性决定的,对于静态语言来讲,早在编译时期就生成了机器码,而且随后进行连接,能提供一个反射机制就很不错了,至于修改仍是不要奢望。

实际上,若是咱们广义的把元编程理解为:“关于编程的编程”,那么 runtime 能够理解为一种元编程的实现方式。若是狭义的把元编程理解为用代码生成代码,而且动态执行,那 runtime 就不算了。

利用 Ruby 实现 DSL

分别介绍了 DSL 和 Ruby 的基础概念后,咱们就能够着手利用 Ruby 来实现本身的 DSL 了。

以上文生成 HTML 的 DSL 为例进行分析,为了说明问题,我把代码再次简化一下:

html = HTMLMaker.new.document do
  body do
    div id: "container"
  end
end复制代码

首先咱们要定义一个 HTMLMaker 类,而且把 document 方法做为入口。这个方法接收一个闭包,闭包中调用 body 函数,这个函数也提供了闭包,闭包中调用了 div 方法,而且有一个参数 id: "container"……

可见这实际上是一个递归调用,不管是 body 仍是 div,他们对应着 HTML 标签,其实都是一些并列的方法,方法能够接受若干个键值对,也就是 HTML 中标签的属性,最后再跟上一个闭包用来建立隶属于本身的子标签。

若是不用 Ruby,咱们须要事先知道全部的 HTML 标签名,而后进行匹配,可想工做量有多大。而在 Ruby 中,他们都是并列关系,能够统一转发到 method_missing 方法中,获取方法名、参数和闭包。

咱们首先解析参数,配合方法名拼凑出当前标签的字符串,而后递归调用闭包便可,核心代码以下:

def method_missing(m, *args, &block)
    tag(m, args, &block)
end

def tag(html_tag, args, &block)
  # indent 用来记录行首的空格缩进
  # options 表示解析后的 HTML 属性,好比 id="container", content 则是标签中的内容
  html << "\n#{indent}<#{html_tag}#{options}>#{content}"
  if block_given? # 若是传递了闭包,递归执行
    instance_eval(&block) # 递归执行闭包
    html << "\n#{indent}"
  end
  html << "</#{html_tag}>"
end复制代码

这里的 instance_eval 也是一种元编程,表示以当前实例为上下文,执行闭包,具体用途能够参考这篇文章: Eval, module_eval, and instance_eval

完整的代码意义不大,主要是细节的处理,若是感兴趣,或者没有彻底理解上面这段代码的意思,能够去原文中查看代码,而且自行调试。

Ruby 在 iOS 开发中的运用

Ruby 主要用来实现一些自动化脚本,并且因为 iOS 系统上没有 Ruby 解释器,因此它一般是在 Mac 系统上使用,在编译前(绝非 app 的运行时)进行一些自动化工做。

你们最熟悉的 Cocoapods 的 podfile 其实就是一份 Ruby 代码:

target 'target_name' do
    pod 'pod_name', '~> version'
end复制代码

熟悉的 do end 代码块告诉咱们,这段声明式的 pod 依赖关系,其实就是能够执行的 ruby 代码。Cocoapods 的具体实现原理能够参考 @draveness 的这篇文章: CocoaPods 都作了什么?

在 Ruby On Rails 中有一个著名的模块: ActiveRecord,它提供了对象关系映射的功能(ORM)。

在面向对象的语言中,咱们用对象来存储数据,对象是类的实例,而在关系型数据库中,数据的抽象模型叫实体(Entity)。类和实体在必定程度上有类似性,好比均可以拥有多个属性,类对属性的增删改查操做由类对外暴露的方法实现,在关系型数据库中则是由 SQL 语句实现。

ORM 提供了对象和实体之间的对应关系,咱们再也不须要手写 SQL 语句,而是直接调用对象的相关方法, 这些方法的内部会生成相应的 SQL 语句并执行。能够说 ORM 框架屏蔽了数据库的具体细节, 容许咱们以面向对象的方式对数据进行持久化操做。

MetaModel

MetaModel 这个框架借鉴了 ActiveRecord 的功能,致力于打造一个 iOS 开发中的 ORM 框架。

在没有 ORM 时,假设有一个 Person 类,它有若干个属性。即便咱们利用继承等面向对象的特性封装好了大量模板方法,每当增长或删除属性时,代码改动量依然不算小。考虑到实体之间还有一对1、一对多、多对多等关系,一旦关系发生变化,相关代码的变化会更大。

MetaModel 的原理就是利用 ruby 实现了一个 DSL,在 DSL 中规定了每一个实体的属性和关系,这也是开发者惟一须要关心的内容。接下来的任务将彻底由 MetaModel 负责,首先它会解析每一个实体有哪些属性,和别的实体有哪些关系,而后生成对应的 Swift/Objective-C 代码,打包成静态库,最终以面向对象的方式向开发者暴露增删改查的 API。

在实际使用时,咱们首先要写一个 Metafile 文件,它相似于 Podfile,用于规定实体的属性和关系:

define :Article do
  attr :title
  attr :content

  has_many :comments
end

define :Comment do
  attr :content

  belongs_to :article
end复制代码

执行完 MetaModel 的脚本后,就会生成相关代码,并封装在静态库中,而后能够这样调用:

let article = Article.create(title: "title1", content: "content1")
article.save // 执行 INSERT 语句
article.update(title: "newTitle")
let anotherArticle = Article.find(content:"content1")
print(Article.all)复制代码

MetaModel 的实现原理并不复杂,但真的作起来,仍是要考虑不少细节,本文不对它的内部实现作过多分析。MetaModel 已经开源,正在不断的完善中,想了解具体使用步骤或参与到 MetaModel 完善工做中的朋友,能够打开这个页面

相关文章
相关标签/搜索