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 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应该包含以下功能

  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

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

  1. 在Notepad++下运行ruby代码

    轻量级,轻量级,所以用notepad++来运行ruby的代码最合适不过了,虽说有更好用的轻量级工具,但是用notepad++习惯了,也懒得去再装其他工具了.好了,进入主题,先安装插件NppExec,打 ...

  2. 可爱的豆子——使用Beans思想让Python代码更易维护

    title: 可爱的豆子--使用Beans思想让Python代码更易维护 toc: false comments: true date: 2016-06-19 21:43:33 tags: [Pyth ...

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

    与asp.net 打交道很多年,如今天微软的优秀框架越来越多,其中微软在基于mvc的思想架构,也推出了自己的一套asp.net mvc 框架,如果你亲身体验过它,会情不自禁的说‘漂亮’.回过头来,‘漂 ...

  4. C#6新特性,让你的代码更干净

    前言 前几天看一个朋友的博客时,看他用到了C#6的特性,而6出来这么长时间还没有正儿八经看过它,今儿专门看了下新特性,说白了也不过是语法糖而已.但是用起来确实能让你的代码更加干净些.Let's try ...

  5. 【TypeScript】如何在TypeScript中使用async/await,让你的代码更像C#。

    [TypeScript]如何在TypeScript中使用async/await,让你的代码更像C#. async/await 提到这个东西,大家应该都很熟悉.最出名的可能就是C#中的,但也有其它语言也 ...

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

    好书推荐:Effective Java中文版(第2版) JVM JIT编译器优化技术有近100中,其中最最重要的方式就是内联(inlining).方法内联可以省掉方法栈帧的创建,方法内联还使让JIT编 ...

  7. Lambda表达式, 可以让我们的代码更优雅.

    在C#中, 适当地使用Lambda表达式, 可以让我们的代码更优雅. 通过lambda表达式, 我们可以很方便地创建一个delegate: 下面两个语句是等价的 Code highlighting p ...

  8. mysql 利用触发器(Trigger)让代码更简单

    一,什么触发器 1,个人理解 触发器,从字面来理解,一触即发的一个器,简称触发器(哈哈,个人理解),举个例子吧,好比天黑了,你开灯了,你看到东西了.你放炮仗,点燃了,一会就炸了. 2,官方定义 触发器 ...

  9. 50行ruby代码开发一个区块链

    区块链是什么?作为一个Ruby开发者,理解区块链的最好办法,就是亲自动手实现一个.只需要50行Ruby代码你就能彻底理解区块链的核心原理! 区块链 = 区块组成的链表? blockchain.ruby ...

随机推荐

  1. kubeadm安装集群系列(kubeadm 1.15.1)

    kubeadm已经进入GA阶段,所以尝试使用kubeadm从零开始安装高可用的Kubernetes集群,并记录下过程和所有坑 本文基于kubeadm 1.15.1 目录 kubeadm安装集群系列-1 ...

  2. 【jQuery】attr()、prop()、css() 的区别(转载)

    .attr( ) 可以设置元素的属性(也就是给元素新增加一个原来并不存在的属性)也可以获取元素的本来就有的属性以及额外设置的属性.如果要获取的属性没有设置,那么获取到的结果是 undefined; . ...

  3. 使用zookeeper作为分布式锁以及设计一种通知监听模式

    1.创建实例/** * 初始化单例的便捷方法 */ public static void init() { getInstance(); } /** * 获取单例 * @return */ publi ...

  4. Tomcat: has been normalized to [null] which is not valid

    环境 tomcat 8.5 原因 在使用相对路径加载配置文件时,如果相对路径超出了 tomcat 容器的根目录,那么 tomcat 会提示 xxx has been normalized to [nu ...

  5. 通过模板创建一个ABP项目

    ⒈下载 进入ABP模板页面,选择模板后下载 ⒉运行 1.初始化数据库 修改xxxx.Migrator.xxxx.Web.Host appsettings.json中的连接字符串 2.还原数据库 在Nu ...

  6. Djangon简介

    目录 Djangon简介 MVC与MTV模型 MVC MTV python三大主流web框架 Django的下载与基本命令 基于Django实现的一个简单示例 Django静态文件配置 Django ...

  7. HTML DOM focus() 方法

    目录 HTML DOM focus() 方法 实例 定义和使用 浏览器支持 语法 参数 技术描述 更多实例 实例 实例 HTML DOM focus() 方法 实例 为 <a> 元素设置焦 ...

  8. Ruby Rails学习中:Sass 和 Asset Pipeline,布局中的链接(Rails路由,具名路由),用户注册: 第一步

    接上篇: 一.Sass 和 Asset Pipeline Rails 中最有用的功能之一是 Asset Pipeline, 它极大地简化了静态资源文件(CSS.JavaScript 和图像)的生成和管 ...

  9. LeetCode面试常见100题( TOP 100 Liked Questions)

    LeetCode面试常见100题( TOP 100 Liked Questions) 置顶 2018年07月16日 11:25:22 lanyu_01 阅读数 9704更多 分类专栏: 面试编程题真题 ...

  10. Java反射理解(五)-- 方法反射的基本操作

    Java反射理解(五)-- 方法反射的基本操作 方法的反射 1. 如何获取某个方法 方法的名称和方法的参数列表才能唯一决定某个方法 2. 方法反射的操作 method.invoke(对象,参数列表) ...