DSL-让你的 Ruby 代码更加优雅

栏目: Ruby · 发布时间: 5年前

内容简介:DSL是Ruby这门语言较为广泛的用途之一,不过如果不熟悉Ruby的元编程的话,难免会被这类语法弄得一脸蒙蔽。今天主要就来看看DSL它是个什么东西,它在Ruby社区中地位怎么样,以及如何实现一门简单的DSL。DSL的全称是domain specific language-领域特定语言。顾名思义,它是一种用于特殊领域的语言。我们最熟悉的HTML其实就是专门用于组织页面结构的“语言”,CSS其实就是专门用于调整页面样式的“语言”。SQL语句就是专用于数据库操作的“语句”。不过它们一般也就只能完成自己领域内的事情

DSL是 Ruby 这门语言较为广泛的用途之一,不过如果不熟悉Ruby的元编程的话,难免会被这类语法弄得一脸蒙蔽。今天主要就来看看DSL它是个什么东西,它在Ruby社区中地位怎么样,以及如何实现一门简单的DSL。

DSL与GPL

DSL的全称是domain specific language-领域特定语言。顾名思义,它是一种用于特殊领域的语言。我们最熟悉的HTML其实就是专门用于组织页面结构的“语言”,CSS其实就是专门用于调整页面样式的“语言”。SQL语句就是专用于数据库操作的“语句”。不过它们一般也就只能完成自己领域内的事情,别的几乎啥都做不了。就如同你不会想利用一支钢笔去弹奏乐曲或者利用一台钢琴来作画一样。此外,前端领域的最后一位“三剑客”JavaScript曾经也勉强能够算作一门专注于页面交互的DSL,不过随着标准化的推进,浏览器的进化还有进军服务端的宏图大志,它所能做的事情也就渐渐多起来,发展成了一门通用目的的编程语言。

与DSL相对的是GPL(这个简写跟某个开源证书相同),它的全称是general-purpose language-通用目的语言,指被设计来为各种应用领域服务的编程语言。一般而言通用目的编程语言不含有为特定应用领域设计的结构。我们常用的Ruby,Python,C语言都属于这类范畴。它们有自己的专门语法,但是并不限于特定领域。以 Python 为例子,如今它广泛用于人工智能领域,数据分析领域,Web开发领域,爬虫领域等等。遗憾的是这让许多人产生了一种 只有Python才能做这些领域 的幻觉。为了在指定的领域能够更加高效的完成工作,一些语言会研发出相应的框架,相关的框架越出色,对语言的推广作用就越好。Rails就是一个很好的例子,Matz也曾经说过

如果没有Ruby On Rails,Ruby绝对不会有如今的流行度。

语言之争也渐渐地演化成框架之争,如果哪天Ruby也开发出一个被广泛接受的人工智能框架,在效率与创新上能够吊打如今的龙头老大,说不定Ruby还能再度火起来吧(我还没睡醒)。不过今天的重点并非语言之争,让咱们再次回到DSL的怀抱中。

简要的DSL

我们遇到不少的Ruby开源库都会有其对应DSL,其中就包括 RspecRabl ,Capistrano等。今天就以自动化部署工具Capistrano来做个例子。Capistrano的简介如下

A remote server automation and deployment tool written in Ruby.
复制代码

它的作用可以简单概括为**通过定义相关的任务来声明一些需要在服务端完成的工作,并通过限定角色,让我们可以针对特定的主机完成特定的任务。**Capistrano的配置文件大概像下面这样

role :demo, %w{example.com example.org example.net}
task :uptime do
  on roles(:demo) do |host|
    uptime = capture(:uptime)
    puts "#{host.hostname} reports: #{uptime}"
  end
end
复制代码

从语义上看它完成了以下工作

  1. 定义角色列表名为 demo ,列表中包含 example.comexample.orgexample.net 这几台主机。
  2. 定义名为 uptime 的任务,通过方法 on 来定义任务流程以及任务所针对的角色。方法 on 的第一个参数是角色列表 roles(:demo) ,这个方法还接收代码块,并把主机对象暴露给代码块,借以运行对应的代码逻辑。
  3. 任务代码块所完成的功能主要是通过 capture 方法在远程主机上运行 uptime 命令,并把结果存储到变量中。然后把运行结果还有主机信息打印出来。

这是一个很简单的DSL,工作内容一目了然。但是如果我们不是采用DSL而是用正常的Ruby代码来实现,代码可能会写成下面这样

demo = %w{example.com example.org example.net} # roles list

# uptime task
def uptime(host)
  uptime = capture(:uptime)
  puts "#{host.hostname} reports: #{uptime}"
end

demo.each do |hostname|
  host = Host.find_by(name: hostname)
  uptime(host)
end
复制代码

可见对比起最初的DSL版本,这种实现方式的代码片段相对没那么紧凑,而且有些逻辑会含混不清,只能通过注释来阐明。况且,Capistrano主要用于自动化一些远程作业,其中的角色列表,任务数量一般不会少。当角色较多时我们不得不声明多个数组变量。当任务较多的时候,则需要定义多个方法,然后在不同的角色中去调用,代码将越发难以维护。这或许就是DSL的价值所在吧,把一些常规的操作定义成更清晰的特殊语法,接着我们便可以利用这些特殊语法来组织我们的代码,不仅提高了代码的可读性,还让后续编程工作变得更加简单。

构建一只青蛙

今天不去分析Capistrano的源码,其实我也从来没有读过它的源代码,想要在一篇短短的博客里面完整分析Capistrano的源码未免有点狂妄。记得之前有位大神说过

如果你想要了解一只青蛙,应该去构建它,而不是解剖它。

那么接下来我就尝试按照自己的理解去构建Capistrano的DSL,让我们自己的脚本也可以像Capistrano那样组织代码。

a. 主机类

从DSL中 host 变量的行为来看,我们需要把远程主机的关键信息封装到一个对象中去。那么我姑且将这个对象简化成只包含 ip , 主机名 , CPU核数内存大小 这些字段吧。另外我的脚本不打算采用任何持久化机制,于是我会在设计的主机类内部维护一个主机列表,任何通过该类所定义的主机信息都会被追加到列表中,以便日后查找

class Host
  attr_accessor :hostname, :ip, :cpu, :memory
  @host_list = [] # 所有被定义的主机都会被临时追加到这个列表中

  class << self
    def define(&block)
      host = new
      block.call(host)
      @host_list << host
      host
    end

    def find_by_name(hostname) # 通过主机名在列表中查找相关主机
      @host_list.find { |host| host.hostname == hostname }
    end
  end
end
复制代码

以代码块的方式来定义相关的主机信息,然后通过 Host#find_by_name 方法来查找相关的主机

Host.define do |host|
  host.hostname = happy.com'
  host.ip = '192.168.1.200'
  host.cpu = '2 core'
  host.memory = '8 GB'
end

p Host.find_by_name('happy.com') # => #<Host:0x00007f943b064bc8 @hostname="happy.com", @ip="192.168.1.200", @cpu="1 core", @memory="8 GB">
复制代码

限于篇幅,这里只做了个粗略的实现,能够存储并查找主机信息即可,接下来继续设计其他的部件。

b. 捕获方法

capture 方法从功能上来看应该是往远程主机发送指令,并获取运行的结果。与远程主机进行通信一般都会采用SSH协议,比如我们想要往远程主机发送系统命令(假设是uptime)的话可以

ssh user@xxx.xxx.xxx.xxx uptime
复制代码

而在Ruby中要运行命令行指令可以通过特殊语法来包裹对应的系统命令。那么 capture 方法可以粗略实现成

def capture(command)
  `ssh #{@user}@#{@current_host} #{command}`
end
复制代码

不过这里为了简化流程,我就不向远端主机发送命令了。而只是打印相关的信息,并始终返回 success 状态

def capture(command)
  # 不向远端主机发送系统命令,而是打印相关的信息,并返回:success
  puts "running command '#{command}' on #{@current_host.ip} by #{@user}"
  # `ssh #{@user}@#{@current_host.ip} #{command}`
  :success
end
复制代码

该方法可以接收字符串或者符号类型。假设我们已经设置好变量 @user 的值为 lan ,而 @current_host 的值是 192.168.1.218 ,那么运行结果如下

capture(:uptime) # => running command 'uptime' on 192.168.1.218 by lan
capture('uptime') # => running command 'uptime' on 192.168.1.218 by lan
复制代码

c. 角色注册

从代码上来看,角色相关的DSL应该包含以下功能

role
roles

这两个功能其实可以简化成哈希表的取值,赋值操作。不过我不想另外维护一个哈希表,我打算直接在当前环境中以可共享变量的方式来存储角色信息。要知道我们平日所称的环境其实就是哈希表,而我们可以通过 实例变量 来达到共享的目的

def role(name, list)
  instance_variable_set("@role_#{name}", list)
end


def roles(name)
  instance_variable_get("@role_#{name}")
end
复制代码

这样就能够简单地实现角色注册,并在需要的时候再取出来

role :name, %w{ hello.com hello.net }
p roles(:name) # => ["hello.com", "hello.net"]
复制代码

此外,这个简单的实现有个比较明显的问题,就是有可能会污染当前环境中已有的实例变量。不过一般而言这种几率并不是很大,注意命名就好。

d. 定义任务

在原始代码中我们通过关键字 task ,配合任务名还有代码块来划分任务区间。在任务区间中通过关键字 on 来定义需要在特定的主机列表上执行的任务。从这个阵仗上来在 task 所划分的任务区间中或许可以利用多个 on 语句来指定需要运行在不同角色上的任务。我们可以考虑把这些任务都塞入一个队列中,等到 task 的任务区间结束之后再依次调用。按照这种思路 task 方法的功能反而简单了,只要能够接收代码块并打印一些基础的日志信息即可,当然还需要维护一个任务队列

def task(name)
  puts "task #{name} begin"
  @current_task = [] # 任务队列
  yield if block_given?
  @current_task.each(&:call)
  puts "task #{name} end"
end
复制代码

然后是 on 方法,它应该能定义需要在特定角色上运行的任务,并且把对应的任务追加到队列中,延迟执行。我姑且把它定义成下面这样

def on(list, &block)
  raise "You must provide the block of the task." unless block_given?
  @current_task << Proc.new do
    host_list = list.map { |name| Host.find_by_name(name) }
    host_list.each do |host|
      @current_host = host
      block.call(host)
    end
  end
end
复制代码

e. 测试DSL

相关的DSL已经定义好了,下面来测试一下,从设计上来看需要我们预先设置主机信息,注册角色列表以及具有远程主机权限的用户

# 设定有远程主机权限的用户
@user = 'lan'

# 预设主机信息,一共三台主机
Host.define do |host|
  host.hostname = 'example.com'
  host.ip = '192.168.1.218'
  host.cpu = '2 core'
  host.memory = '8 GB'
end

Host.define do |host|
  host.hostname = 'example.org'
  host.ip = '192.168.1.110'
  host.cpu = '1 core'
  host.memory = '4 GB'
end

Host.define do |host|
  host.hostname = 'example.net'
  host.ip = '192.168.1.200'
  host.cpu = '1 core'
  host.memory = '8 GB'
end

## 注册角色列表
role :app, %w{example.com example.net}
role :db, %w{example.org}
复制代码

接下来我们通过 taskon 配合上面所设置的基础信息来定义相关的任务

task :demo do
  on roles(:app) do |host|
    uptime = capture(:uptime)
    puts "#{host.hostname} reports: #{uptime}"
    puts "------------------------------"
  end

  on roles(:db) do |host|
    uname = capture(:uname)
    puts "#{host.hostname} reports: #{uname}"
    puts "------------------------------"
  end
end
复制代码

运行结果如下

task demo begin
running command 'uptime' on 192.168.1.218 by lan
example.com reports: success
------------------------------
running command 'uptime' on 192.168.1.200 by lan
example.net reports: success
------------------------------
running command 'uname' on 192.168.1.110 by lan
example.org reports: success
------------------------------
task demo end
复制代码

这个就是我们所设计的DSL,与Capistrano所提供的基本一致,最大的区别在于我们不会往远程服务器发送系统命令,而是以日志的方式把相关的信息打印出来。从功能上看确实有点粗糙,不过语法上已经达到预期了。

尾声

这篇文章主要简要地介绍了一下DSL,如果细心观察会发现DSL在我们的编码生涯中几乎无处不在。Ruby的许多开源项目会利用语言自身的特征来设计相关的DSL,我用Capistrano举了个例子,对比起常规的编码方式,设计DSL能够让我们的代码更加清晰。最后我尝试按自己的理解去模拟Capistrano的部分DSL,其实只要懂得一点元编程的概念,这个过程还是比较容易的。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

现代操作系统(第3版)

现代操作系统(第3版)

Andrew S. Tanenbaum / 陈向群、马洪兵 / 机械工业出版社 / 2009-7 / 75.00元

本书是操作系统领域的经典之作,与第2版相比,增加了关于Linux、Windows Vista和Symbian操作系统的详细介绍。书中集中讨论了操作系统的基本原理,包括进程、线程、存储管理、文件系统、输入/输出、死锁等,同时还包含了有关计算机安全、多媒体操作系统、掌上计算机操作系统、微内核、多核处理机上的虚拟机以及操作系统设计等方面的内容。此外,还在第2版的基础上对部分习题进行了增删,更有助于读者学......一起来看看 《现代操作系统(第3版)》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换