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. GitHub项目精选(持续更新)

    ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190815110442972.jpg?x-oss-process=image/watermark,type_Zm ...

  2. 【并行计算-CUDA开发】CUDA线程、线程块、线程束、流多处理器、流处理器、网格概念的深入理解

    GPU的硬件结构,也不是具体的硬件结构,就是与CUDA相关的几个概念:thread,block,grid,warp,sp,sm. sp: 最基本的处理单元,streaming processor  最 ...

  3. Spring源码分析(1)容器的基本实现——核心类介绍

    bean是Spring中最核心的东西,因为Spring就像是个大水桶,而bean就像是容器中的水,水桶脱离了水便也没什么用处了,那么我们先看看bean的定义. public class MyTestB ...

  4. mysql数据库之视图、触发器

    视图 概念:通过查询得到的一张虚拟表,然后保存下来就是视图 视图的好处:如果要频繁使用某张虚拟表,那么就可以保存为视图,以后查找就直接拿这个视图就会非常方便 视图语法规则: create view t ...

  5. Linux文件属性之用户与用户组基础知识回顾

    回顾: 用户.用户组的概念: 每个文件和进程,都需要对应一个用户和用户组. linux系统通过UID和GID来识别用户和组的. 用户名相当于人名 UID和GID  身份证号 管理员:root   do ...

  6. mysql 基础练习题(一)

    一.先创建几个要用的库表 create database zuoye; -- 创建数据库 use zuoye; -- 使用数据库 #创建几个库表 create table Student -- 学生表 ...

  7. HDU 4123 Bob’s Race 树的直径+ST表

    Bob’s Race Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://acm.hdu.edu.cn/showproblem.php?pid=41 ...

  8. PAT A1012 Best Rank(25)

    题目描述 To evaluate the performance of our first year CS majored students, we consider their grades of ...

  9. PHPRedis教程之geo

    前言 支持 GEO 系列命令的 Redis 版本从 3.2.0 起开始才可以使用,所以之前版本就不要想了. 函数列表 geoadd - 将指定的地理空间项(纬度,经度,名称)添加到指定的键, 数据作为 ...

  10. Xshell~工作中访问Linux服务器

    1.下载Xshell 下载地址:https://xshell.en.softonic.com/ 2.安装(无特殊修改,自行安装即可) 3.使用 登录(1.新建->输入主机IP,点击确定-> ...