我是一个极简主义和清晰代码的爱好者。最近,我遇到了一些重复的代码。我相信,每个人都应该尽可能地保持代码的DRY(不要重复自己)。代码是这样的:调用第三方API,检查响应,如果成功就做一些事情,如果不成功就做其他事情。一个基本的代码,但实施起来很无聊。
问题是
为了不啰嗦,不重复我已经说过的话,我只是提供一个代码的例子:
response = StripeCall.new(number: 'valid').call
if response.success?
puts response.body
else
puts response.body
end
基本上,这段代码没有任何问题。但是,我发现这还不够好,因为有太多的细节需要注意了:
- 必须知道它们的公共接口的两个类(它是
StripeCall和response对象的类)。 - 不要忘了在所有使用它的地方检查响应,并为这个
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来解决你的问题。这样一来,代码将是可读的,并接近业务领域,这是每个开发人员的梦想。编码愉快!