[转]DSL-让你的 Ruby 代码更优秀

https://ruby-china.org/topics/38428

以下摘录

DSL和Gpl

DSL : domain-specific language。比如HTML是用于组织网页的‘语言’, CSS专门调整页面样式的‘语言’。

SQL是数据库操作的‘语句’。

GPL: general-purpose language。通用目的语言。即不是为了特定领域设计的语言。Ruby,Python,C都是。

简单的DSL

我们遇到不少的Ruby开源库都会有其对应DSL,其中就包括RspecRablCapistrano等。今天就以自动化部署工具Capistrano来做个例子。Capistrano的简介如下A remote server automation and deployment tool written in Ruby.

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

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.com等几个主机网址。
  2. 定义了任务 uptime, 然后通过方法on来定义任务流程和任务所针对的角色。
    • 方法on的第一个参数是角色列表roles(:demo)
    • 这个方法还接收一个代码块,并把主机对象host“暴露”(传)给代码块,以便运行对应的代码逻辑
  3. 任务代码块完成的功能:通过capture方法在远程主机上运行uptime命令,并把结果储存在变量内,然后把运行结果puts,即打印出来。

如果改用正常的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的价值所在吧,把一些常规的操作定义成更清晰的特殊语法,接着我们便可以利用这些特殊语法来组织我们的代码,不仅提高了代码的可读性,还让后续编程工作变得更加简单。

??。这是有争论的http://www.yinwang.org/blog-cn/2017/05/25/dsl

尽一切可能避免创造 DSL,因为它会带来严重的理解,交流和学习曲线问题,可能会严重的降低团队的工作效率。如果这个 DSL 是给用户使用,会严重影响用户体验,降低产品的可用性。
大部分时候写库代码,把需要的功能做成函数,其实就可以解决问题。
如果真的到了必须创造 DSL 的时候,非 DSL 不能解决问题,才可以动手设计 DSL。但 DSL 必须由程序语言专家来完成,否则它还是可能给产品和团队带来严重的后果。
大部分 DSL 要解决的问题,不过是“动态逻辑加载”。为了这个目的,你完全可以利用已有的语言(比如 JavaScript),或者取其中一部分构造,通过动态调用它的解释器(编译器)来达到这个目的,而不需要创造新的 DSL

构建一只青蛙

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

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

a. 主机类

从DSL中host变量的行为来看,需要把远程主机的信息封装的一个对象中。

设计方式:

不采用持久化机制:

在Host类内部维护一个主机列表,通过该类所定义的主机信息会被添加到列表内,并可以通过hostname进行查找。

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

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

    def find_by_name(hostname)
      @host_list.find {|host| host.hostname == hostname}
    end
  end
end

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

b. 捕获方法

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

ssh [email protected] 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应该包含以下功能

  1. 通过role配合角色名, 主机列表来注册相关的角色。
  2. 通过role配合角色名来获取角色对应的主机列表。

这两个功能其实可以简化成哈希表的取值,赋值操作。

不过我不想另外维护一个哈希表,我打算直接在当前环境中以可共享变量的方式来存储角色信息。

要知道我们平日所称的环境其实就是哈希表,而我们可以通过实例变量来达到共享的目的

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} end"
  @current_task = []  #@current_task可以被代码块(闭包)得到。
  yield if block_given?  #确认调用task方法后传入代码块了没有,有,执行这个代码块,即几个on方法。
  @current_task.each(&:call)  #在task方法中的on方法都执行完后,调用队列中的Proc对象。
  puts "task #{name} end"
end

定义on方法,它应该能定义需要在特定角色上运行的任务,并且把对应的任务追加到队列中,延迟执行。

延迟执行即使用

 @current_task << Proc.new do...end

把所有的任务放入队列中(@current_task),然后执行@current_task中的每一个Proc对象。

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配合上面所设置的基础信息来定义相关的任务:

这就是DSL的使用:本质上还是方法定义罢了(充分利用了Ruby的代码块)

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

??: on方法的第一参数是roles方法,第二个参数是代码块。

运行结果如下

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,其实只要懂得一点元编程的概念,这个过程还是比较容易的。

现在主流观点是能不用,就不用:

??。这是有争论的http://www.yinwang.org/blog-cn/2017/05/25/dsl

尽一切可能避免创造 DSL,因为它会带来严重的理解,交流和学习曲线问题,可能会严重的降低团队的工作效率。如果这个 DSL 是给用户使用,会严重影响用户体验,降低产品的可用性。
大部分时候写库代码,把需要的功能做成函数,其实就可以解决问题。
如果真的到了必须创造 DSL 的时候,非 DSL 不能解决问题,才可以动手设计 DSL。但 DSL 必须由程序语言专家来完成,否则它还是可能给产品和团队带来严重的后果。
大部分 DSL 要解决的问题,不过是“动态逻辑加载”。为了这个目的,你完全可以利用已有的语言(比如 JavaScript),或者取其中一部分构造,通过动态调用它的解释器(编译器)来达到这个目的,而不需要创造新的 DSL

原文地址:https://www.cnblogs.com/chentianwei/p/11447381.html

时间: 2024-10-25 00:42:37

[转]DSL-让你的 Ruby 代码更优秀的相关文章

让你的代码更优秀的 14 条建议

学习代码语法是一件比较简单的事情.但是如何利用简单的语法去组建庞大的项目,会衍生出很多问题.这里总结了一些编程过程中需要注意的陷阱和原则,之后如果有新的总结,我会继续更新. 1,不要编程 对,不要编程.能用草稿纸解决的问题,不用去写程序. 在写程序之前,应该先弄清楚问题.花更多的时间去听.读和理解问题. 非常多的程序员在写代码的过程中去梳理问题,所以他们会花更多时间去调试和找 bug. 2,不要太依赖工具 不管是编程语言,类库和成熟软件,都是解决问题的工具,真正值钱的并不是工具,而是掌握的编程技

在Notepad++下运行ruby代码

轻量级,轻量级,所以用notepad++来运行ruby的代码最合适不过了,虽说有更好用的轻量级工具,但是用notepad++习惯了,也懒得去再装其他工具了.好了,进入主题,先安装插件NppExec,打开notepad++,在Plugin->Plugin Manager->Available里面找到NppExec,勾选中Install.装好重启后,在notepad++中打开一段ruby代码,F6在弹出的对话框输入 cd $(CURRENT_DIRECTORY) ruby $(FULL_CURRE

让你的自动化代码更健壮

在做自动化测试时,尤其是UI级自动化,如何让你的代码更健壮可能是你经常要考虑的问题,这里分享几个小Tips. 多用WaitForXXXX 严格意义上讲,任何长时间的“硬等待”都是可取的!!到处随意的Sleep()只能显示你技能的匮乏,写出的Case也会效率低下.所以当此之时, 我们都应该多用WaitFor方法,而且任何的自动化框架都是有类似方法的,像Robotium的: solo.waitForDialogToClose() //waits for the dialog to close sol

怎样让你的代码更好的被JVM JIT Inlining

好书推荐:Effective Java中文版(第2版) JVM JIT编译器优化技术有近100中,其中最最重要的方式就是内联(inlining).方法内联可以省掉方法栈帧的创建,方法内联还使让JIT编译器更多更深入的优化变成可能.本人在fastxml(速度比XPP3(基于xmlpull)还快的xml解析器)开源项目中针对方法内联进行了很多学习和实践,这里总结一下,介绍一下怎么让你的代码更好的被JVM JIT Inlining. Inlining相关的启动参数 上一篇博客<Java JIT性能调优

让代码更帅一点

博主的私人博客 写代码最重要的是实现功能,但是除了实现功能之外,我们还应该想办法,让代码变得更规范,更漂亮 最近在读<禅与Objective-C编程艺术>和<Effective Objective C 2.0:编写高质量iOS与OS X代码的52个有效方法>,这两本都讲解了代码规范方面的东西,结合自己平时的代码习惯,发现有很多地方自己做的还是不够好,代码写得不够帅,所以总结一下,让以后的代码更帅一点 条件语句 条件语句一定要使用括号,如果不使用括号,if后面的那行代码删除,之后的代

基于AOP的MVC拦截异常让代码更优美

与asp.net 打交道很多年,如今天微软的优秀框架越来越多,其中微软在基于mvc的思想架构,也推出了自己的一套asp.net mvc 框架,如果你亲身体验过它,会情不自禁的说‘漂亮’.回过头来,‘漂亮’终归有个好的思想,其中类似于AOP的思想,就在其中体现的淋漓尽致,今天本文主要讨论的是基于AOP思想构成的‘异常过滤器’.我们的目的只有一个,让try...catch...无处盾形,让代码更健壮优美. 一.理解mvc里filter是怎么运行的 老外的一篇文章是这样的草图 通过翻译中文是这样的 其

依赖注入——让iOS代码更简洁

原文链接:https://corner.squareup.com/2015/06/dependency-injection-in-objc.html 本文是自己通过阅读 Dependency Injection:Give Your iOS Code a Shot in the Arm 进行总结+翻译的,有错误之处请不吝啬的指出.下面是正文: 依赖注入可以通过初始化方法(或构造函数)传递所需要的参数,或者通过属性(setter)传递.这里将对这两种方法进行讲解. 初始化方法注入: - (insta

180行ruby代码搞定游戏2048

最今在玩2048这款小游戏,游戏逻辑简单,非常适合我这种对于游戏新入行的人来实现逻辑.于是选择了最拿手的ruby语言来实现这款小游戏的主要逻辑.还是挺简单的,加起来4小时左右搞定. 上代码: require 'optparse' module Help HELP_TEXT =<<HELP press buttons for move l => move to left r => move to right t => move to top b => move to bo

如何在Android开发中让你的代码更有效率

如何在Android开发中让你的代码更有效率 最近看了一个视频,名字叫做Doing More With Less: Being a Good Android Citizen,主要是讲如何用少少的几句代码来改善Android App的性能.在这个视频里面,演讲者以一个图片app为例讲解如何应用Android中现有的东西来改善app性能问题. 这个图片app的代码:https://github.com/penkzhou/iogallery.ppt:http://greenrobot.qiniudn.