[转]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,其中就包括Rspec,Rabl,Capistrano等。今天就以自动化部署工具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
从语义上分析,它完成了以下工作:
- 定义角色列表名demo, 列表中包含example.com等几个主机网址。
- 定义了任务 uptime, 然后通过方法on来定义任务流程和任务所针对的角色。
- 方法on的第一个参数是角色列表roles(:demo)
- 这个方法还接收一个代码块,并把主机对象host“暴露”(传)给代码块,以便运行对应的代码逻辑
- 任务代码块完成的功能:通过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应该包含以下功能
- 通过role配合角色名, 主机列表来注册相关的角色。
- 通过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}
接下来我们通过task
和on
配合上面所设置的基础信息来定义相关的任务:
这就是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 代码更优秀的更多相关文章
- 在Notepad++下运行ruby代码
轻量级,轻量级,所以用notepad++来运行ruby的代码最合适不过了,虽说有更好用的轻量级工具,但是用notepad++习惯了,也懒得去再装其他工具了.好了,进入主题,先安装插件NppExec,打 ...
- 可爱的豆子——使用Beans思想让Python代码更易维护
title: 可爱的豆子--使用Beans思想让Python代码更易维护 toc: false comments: true date: 2016-06-19 21:43:33 tags: [Pyth ...
- 基于AOP的MVC拦截异常让代码更优美
与asp.net 打交道很多年,如今天微软的优秀框架越来越多,其中微软在基于mvc的思想架构,也推出了自己的一套asp.net mvc 框架,如果你亲身体验过它,会情不自禁的说‘漂亮’.回过头来,‘漂 ...
- C#6新特性,让你的代码更干净
前言 前几天看一个朋友的博客时,看他用到了C#6的特性,而6出来这么长时间还没有正儿八经看过它,今儿专门看了下新特性,说白了也不过是语法糖而已.但是用起来确实能让你的代码更加干净些.Let's try ...
- 【TypeScript】如何在TypeScript中使用async/await,让你的代码更像C#。
[TypeScript]如何在TypeScript中使用async/await,让你的代码更像C#. async/await 提到这个东西,大家应该都很熟悉.最出名的可能就是C#中的,但也有其它语言也 ...
- 怎样让你的代码更好的被JVM JIT Inlining
好书推荐:Effective Java中文版(第2版) JVM JIT编译器优化技术有近100中,其中最最重要的方式就是内联(inlining).方法内联可以省掉方法栈帧的创建,方法内联还使让JIT编 ...
- Lambda表达式, 可以让我们的代码更优雅.
在C#中, 适当地使用Lambda表达式, 可以让我们的代码更优雅. 通过lambda表达式, 我们可以很方便地创建一个delegate: 下面两个语句是等价的 Code highlighting p ...
- mysql 利用触发器(Trigger)让代码更简单
一,什么触发器 1,个人理解 触发器,从字面来理解,一触即发的一个器,简称触发器(哈哈,个人理解),举个例子吧,好比天黑了,你开灯了,你看到东西了.你放炮仗,点燃了,一会就炸了. 2,官方定义 触发器 ...
- 50行ruby代码开发一个区块链
区块链是什么?作为一个Ruby开发者,理解区块链的最好办法,就是亲自动手实现一个.只需要50行Ruby代码你就能彻底理解区块链的核心原理! 区块链 = 区块组成的链表? blockchain.ruby ...
随机推荐
- PHP SQL注入
开发者容易遗漏的输入点: HTTP头 X-Forwarded-For 获取用户ip User-Agent 获取浏览器 Referer 获取之 ...
- Redis集群的原理和搭建(转载)
转载来源:https://www.jianshu.com/p/c869feb5581d Redis集群的原理和搭建 前言 Redis 是我们目前大规模使用的缓存中间件,由于它强大高效而又便捷的功能,得 ...
- 算法巩固的第一天-java冒泡排序算法
自媒体萌新一枚,不对的地方各路大神可以指点指点!个人理解: 冒泡排序算法<插入排序算法<快速排序算法 /** * 冒泡排序算法 * @author sj * */ public class ...
- 结构体structure
结构体是值类型 import Foundation struct TV{ var keyName="a" var keyNumber=9 func getKey()->Int ...
- MySQL线程池(THREAD POOL)的原理
MySQL常用(目前线上使用)的线程调度方式是one-thread-per-connection(每连接一个线程),server为每一个连接创建一个线程来服务,连接断开后,这个线程进入thread_c ...
- logid让你的请求完整可追溯
今天是在博客园开园的第一天 一时间其实并不能想起来到底该写什么文章,其实想写的东西挺多 今天就以logid这个主题开始吧,网上写这个的文章似乎不多,但是的确是在实际生产中相当重要的一个能力,也是容易被 ...
- Java回调实现异步 (转)
出处: Java回调实现异步 在正常的业务中使用同步线程,如果服务器每处理一个请求,就创建一个线程的话,会对服务器的资源造成浪费.因为这些线程可能会浪费时间在等待网络传输,等待数据库连接等其他事情上, ...
- JS中的继承(上)
JS中的继承(上) 学过java或者c#之类语言的同学,应该会对js的继承感到很困惑--不要问我怎么知道的,js的继承主要是基于原型(prototype)的,对js的原型感兴趣的同学,可以了解一下我之 ...
- k-means算法处理聚类标签不足的异常
k-means算法在人群聚类场景中,是一个非常实用的工具.(该算法的原理可以参考K-Means算法的Python实现) 常见调用方式 该算法常规的调用方式如下: # 从sklearn引包 from s ...
- 带坑使用微信小程序框架WePY组件化开发项目,附带第三方插件使用坑
纯粹用来记录wepy及相关联内容,以防再犯~ 1. 接手的wepy项目版本是 1.7.2 ,so我没有初始化的过程.... 2. 安装wepy命令工具,npm install wepy-cli -g ...