[Turn] DSL- make your Ruby code better

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

The following excerpt

 

DSL and Gpl

DSL: domain-specific language. Such as HTML pages for the organization of 'language', CSS specially adapted page style 'language'.

SQL is a 'statement' database operations.

GPL: general-purpose language. General purpose language. That is not to specific areas of the design language. Ruby, Python, C are.

Simple DSL

We encountered a lot of open source Ruby library will have its corresponding DSL, including the Rspec , Rabl , Capistrano and so on. Today in an automated deployment tool Capistrano's do an example. About Capistrano follows A remote server automation and deployment tool written in Ruby.

Its role needs to declare some work done on the server side by defining the relevant tasks, and by limiting the role, so that we can accomplish a specific task for a specific host. Configuration file something like this:

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

 

From the analysis of semantics, it completed the following work:

  1. Defined list of roles name demo, the list contains several host URL example.com and so on.
  2. Defines the task uptime, then define the roles and tasks for the task flow by the method on.
    • The first parameter is the method on the list of roles roles (: demo)
    • This method also receives a block of code, and the host target host "exposure" (pass) to the code blocks corresponding to the code logic to run
  3. Task code block to complete the function: capture method by running on a remote host uptime command, and stores the result in the variable, and then the operation result puts, i.e., printed out.

If you switch to a normal Ruby code to implement the code might look like this:

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 visible than the original version, snippets of this implementation is relatively not so compact, and some vague logic will only be illustrated by the comments.

Moreover, Capistrano is mainly used for the automation of some remote job, which is a list of roles, the number of tasks in general, no less.

  • When there are many roles we have to declare an array of multiple variables.
  • When more tasks, you need to define multiple methods, and then to call in a different role, the code will be more difficult to maintain.

Perhaps this is where the value of DSL it, some routine operations into a clearer definition of special syntax, then we will be able to take advantage of these special syntax to organize our code, not only improve the readability of the code, but also to the subsequent programming easier.

⚠️. This is controversial http://www.yinwang.org/blog-cn/2017/05/25/dsl

Do everything possible to avoid the creation of DSL, because it will cause serious understanding, exchange and learning curve issues that may seriously reduce the efficiency of the team. If this is for DSL users, it will seriously affect the user experience, reduce the usability of the product. 
Most of the time write library code, to make the functionality required function, in fact, can solve the problem. 
If you really have the time to create a DSL, non-DSL can not solve problems before they can begin designing DSL. But DSL must be done by programming language experts, otherwise it is likely to have serious consequences for product and team. 
Most DSL problem to be solved, but a "dynamic logic loaded." For this purpose, you can use the existing language (such as JavaScript), or take part constructed by calling its dynamic interpreter (compiler) to achieve this objective, without the need to create new DSL

 

 

Construction of a frog

If you want to know a frog, you should go to build it, rather than dissecting it.

So then I try to understand their own DSL to build Capistrano, let us own scripts can also be organized as code like Capistrano.

a. Host class

From the behavior of the host variable DSL view, a need to package the object information of the remote host.

Design approach:

Do not use persistence mechanism:

Within the Host class maintains a list of hosts, host information, as defined by the class will be added to the list and you can check it on the hostname.

class the Host 
  the attr_accessor: hostname,: IP,: CPU,: Memory 
  @host_list = [] # all hosts will be defined appended to the temporary 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

 

In a manner to define a code block related host information, and to find the relevant host via Host # find_by_name.

 

b. Capture Method

captureThe method should be transmitted from the functional point of view to the remote host command, and acquires the operation result. Then usually communicate with a remote host using SSH protocol, for example, we want to send commands to a remote host system (assuming that is uptime) can be

ssh [email protected] uptime

To run the command-line instructions in Ruby by special syntax to wrap the corresponding system commands. Then the capturemethod can be broadly implemented

def capture(command) `ssh #{@user}@#{@current_host} #{command}` end

But here, I will not send commands to the remote host in order to simplify the process. But only print-related information, and always return to successstate

DEF Capture (Command)
   # is not transmitted to the remote host system command, but print-related information, and returns: Success 
  the puts " running Command '#} {Command' ON #{@current_host.ip by # {}} @ User " 
  # ` SSH User # {} @ @ # @ current_host.ip {} {#} `Command 
  : Success 
End

The method may receive a character string or a symbol type. Suppose we have set up a variable @uservalue lan, and @current_hostthe value 192.168.1.218, then the results are as follows

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. Registration role

From the code point of view, the role of the relevant DSL should contain the following features

  1. By role with the character name, host list associated roles registration.
  2. To get the list of hosts corresponding role by role with the role name.

In fact, these two functions can be reduced to the value of the hash table assignment.

But I do not want to maintain a separate hash table, I'm going to direct in the current environment in a manner that can be shared variable to store the role information.

To know that we normally call the environment is actually a hash table, but we can 实例变量aim to achieve share

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

def roles(name)
  instance_variable_get("@role_#{name}")
end

 

This can be achieved roles registered and taken out when needed:

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

In addition, this simple realization has a more obvious problem is that there is likely to pollute the environment existing in the current instance variables. But in general this is not a big chance, pay attention to name just fine.

 

d. define the task

In the original code we keyword taskto divide the task interval, with the task name as well as the code block.

In the task interval keyword onto define the tasks to be performed on a particular host list.

From this parade up in the taskdivided task interval, you can use multiple onstatements to specify the tasks required to run on different roles.

We can consider these tasks are inserted into a queue until taskthen in turn call after the end of the mission range.

According to this idea taskfunction rather simple process, as long as capable of receiving some basic block of code and print log information you can, of course, also need to maintain a task queue:

DEF Task (name) 
  the puts " Task # {name} End " 
  @current_task = []   # @current_task code block may be (closures) obtained. 
  yield  IF block_given?   # confirmation code block incoming call to task the method is not there, execute this block of code, that is, on several methods. 
  current_task.each @ ( &: Call)   # in the task methods on methods after the implementation, Proc target queue calls. 
  the puts " Task # {name} End " 
End

 

The method defined on, it should be able to define the tasks that need to run on a specific role, and the corresponding task is added to the queue, the delayed execution.

That the use of delayed execution

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

All the tasks in a queue (@current_task), then performs each of @current_task Proc object.

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. Test DSL

DSL has been related to the definition of good, let's test, we need to pre-set the host information from the design point of view, the registration list of roles and permissions of users with remote host

# Set the user has permission to the remote host's 
@user = ' LAN ' 

# default host information, a total of three hosts 
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}

 

Next we pass taskand onto define the tasks associated with the above basic information set:

This is the use of a DSL: The method is essentially nothing defined (full use of Ruby code blocks)

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

 

⚠️: The first parameter is the method of roles on the method, the second parameter is a code block.

Results are as follows

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

This is what we designed DSL, consistent with Capistrano provided, the biggest difference is that we do not send system commands to a remote server, but in a way to log information related to print out. From a functional point of view it is a bit rough, but the syntax has reached the expected.

end

This article briefly describes what DSL, DSL if careful observation will find almost everywhere in our coding career. Ruby's many open source projects will use their own language to design features associated with DSL, I use Capistrano to give an example, compared to conventional coding, designing DSL can make our code more clear. Finally, I try to go according to their own understanding of the analog portion of the DSL Capistrano, in fact, anyone who knows a little meta-programming concept, the process is relatively easy.

 

Now the mainstream view is that can not, do not:

⚠️. This is controversial http://www.yinwang.org/blog-cn/2017/05/25/dsl

Do everything possible to avoid the creation of DSL, because it will cause serious understanding, exchange and learning curve issues that may seriously reduce the efficiency of the team. 
If this is for DSL users, it will seriously affect the user experience, reduce the usability of the product. Most of the time write library code, to make the functionality required function, in fact, can solve the problem. If you really have the time to create a DSL, non-DSL can not solve problems before they can begin designing DSL. But DSL must be done by programming language experts, otherwise it is likely to have serious consequences for product and team. Most DSL problem to be solved, but a "dynamic logic loaded." For this purpose, you can use the existing language (such as JavaScript), or take part constructed by calling its dynamic interpreter (compiler) to achieve this objective, without the need to create new DSL

 

Guess you like

Origin www.cnblogs.com/chentianwei/p/11447381.html