如何在Ruby中使用DSL的条件执行(附实例)

83 阅读3分钟

我是一个极简主义和清晰代码的爱好者。最近,我遇到了一些重复的代码。我相信,每个人都应该尽可能地保持代码的DRY(不要重复自己)。代码是这样的:调用第三方API,检查响应,如果成功就做一些事情,如果不成功就做其他事情。一个基本的代码,但实施起来很无聊。

问题是

为了不啰嗦,不重复我已经说过的话,我只是提供一个代码的例子:

response = StripeCall.new(number: 'valid').call
if response.success?
  puts response.body
else
  puts response.body
end

基本上,这段代码没有任何问题。但是,我发现这还不够好,因为有太多的细节需要注意了:

  • 必须知道它们的公共接口的两个类(它是StripeCallresponse 对象的类)。
  • 不要忘了在所有使用它的地方检查响应,并为这个if 子句使用。
  • 不要忘记正确地实例化StripeCall 类的对象(将params 传递到new ,但不要传递到call )。

可能还有其他的反对意见,但不幸的是,我现在还不能确定它们。总而言之,我们是人,每个人都有自己的感受。

解决方案

从我的实践来看,坏的感觉可以通过引入某种DSL来消除。从想象力开始,但不要离Ruby语法太远(否则将需要实施一种新的语言,但我今天不想这样,因为我对Ruby很在行)。首先,牢记细节的问题,是把params 传递到new 还是call ,可以通过在类的层面上定义call 方法来摆脱。然后,知道.call(params) 可以被替换成.(params) ,就可以减少输入的符号数量。在这之后,其他语言的知识也开始发挥作用:在Javascript中,有相当的语法来处理类似的情况--.onSuccess(func1).onError(func2) 。我个人觉得它很有用,很方便。因此,最终的解决方案可能是这样的:

StripeCall.(number: 'valid')
  .on_success { |response| puts response.body }
  .on_error { |response| puts response.body }

让我们来实现它:

# A base class for all classes implement calls to API.
class ApiCall
  attr_reader :params

  def self.call(params)
    new(params).call
  end

  def initialize(params)
    @params = params
  end

  def call
    @res = execute
    self
  end

  def on_success
    yield @res if @res.success
    self
  end

  def on_error
    yield @res unless @res.success
    self
  end

  private

  def execute
    fail NotImplementedError
  end
end
# A concrete class implements call to API.
class StripeCall < ApiCall
  Response = Struct.new(:success, :body)

  private

  def execute
    success = params[:number] == 'valid'
    body = success ? 'ok response' : 'bad response'
    Response.new(success, body)
  end
end

现在代码已经可以玩了:

StripeCall.(number: 'valid')
  .on_success { |response| puts response.body }
  .on_error { |response| puts response.body }
# => ok response

StripeCall.(number: 'invalid')
  .on_success { |response| puts response.body }
  .on_error { |response| puts response.body }
# => bad response

实际上,到处定义块是很烦人的。因此,也被简化了:

def handle_success(response)
  puts response.body
end

def handle_error(response)
  puts response.body
end

StripeCall.(number: 'valid')
  .on_success(&method(:handle_success))
  .on_error(&method(:handle_error))

现在只需要记住1个类的intercase--它是StripeCall 。行数从6个减少到3个(不考虑条件分支的实现)。但这样的DSL的主要优势在于,实现是隐藏的,而且沿途可能会有异常的产生和捕获。通过捕捉它们并在基类中进行处理,我们甚至可以减少更多的重复性代码。

例如,基类的调用方法可以这样实现:

class ApiCall
  ...
  def call
    @res = begin
             execute
           rescue StripeError => e
             OpenStruct.new(success: false)
           end
    self
  end
  ...
end

结语

一个大的项目通常有很多的代码(惊喜!)。每一行新的代码都会增加耦合性并引入复杂的问题。它变得更难维护和测试,特别是当代码不遵循DRY范式的时候,换句话说,它是重复的。保持你的代码干净,不要犹豫,引入你的DSL来解决你的问题。这样一来,代码将是可读的,并接近业务领域,这是每个开发人员的梦想。编码愉快!