Gem [介绍我的新 Gem] 范蠡:实现复杂业务逻辑的一种新方式

daqing · 2017年05月05日 · 最后由 tumayun 回复于 2017年05月15日 · 3913 次阅读

前言

最近一直在思考实现复杂业务逻辑的更好方法,于是有了 Fanli(范蠡)这个 Gem。

范蠡,是实现复杂业务逻辑的一种新思路。(如果你的 Rails model 超过了 200 行代码,也许你该试试它~~)

项目地址:https://github.com/daqing/fanli

设计思路

使用 Pub/Sub 模式,解除对象之间的耦合,轻松实现“单一职责原则”(https://en.wikipedia.org/wiki/Single_responsibility_principle)。

Alpha 版本,只支持事件同步触发,未来会添加异步执行的方式,例如通过 Sidekiq 去执行事件回调。

使用方法

基本用法

Fanli 可以当成 Event Hub 来使用,例如:

Fanli.when(:create_user, exec: proc { |args| puts "Your name is #{args[:name]}" })
Fanli.trigger(:create_user, with: {name: 'daqing'}) # prints "Your name is daqing"

业务逻辑封装

要实现一个业务逻辑,需要单独写一个类,然后继承 Fanli::Base:

class CreateUser < Fanli::Base
    def perform(args)
        # business logic goes here
    end
end

然后,通过这样的方法去调用:

CreateUser.exec(name: 'daqing', age: 30, from: 'Shandong')

这样会自动回调 #call 方法,并且在 #call 调用成功后,自动发布 :create_user 事件。

然后,要监听 :create_user 事件,需要提前注册:

Fanli.when(:create_user, exec: [DoStepA, DoStepB, proc { |args| puts "one liner processing logic" }])

这样当 :create_user事件发生时,会自动回调 DoStepA.on_create_user(args), DoStepB.on_create_user(args),以及最后一个 Proc 对象。

详细介绍,请看 README: https://github.com/daqing/fanli/blob/master/README.md#usage


有任何想法,欢迎讨论。

P.S. 已经打算在本公司的项目上,用 Fanli 去重构比较复杂的业务代码~~

do 是 Ruby 的关键字,用来做方法名感觉不太好

对 numbcoder 回复

改成 .call 呢?

2 楼 已删除

还不如直接用 .new

感觉这和直接写意大利面代码没什么区别

class CreateUser
  def perform(args)
    user = User.create!(args)

    [SendWelcomeEmail, GiveCouponIfXmas, GiveInitialCash].each do |clazz|
      clazz.(args)
    end
  end
end

class SendWelcomeEmail
  def self.call(args)
  end
end

class GiveCouponIfXmas
  def self.call(args)
  end
end

class GiveInitialCash
  def self.call(args)
  end
end
对 lithium4010 回复

是的,受到 wisper 启发

对 quakewang 回复

你那样写,就把依赖写死了,不方便进行单元测试,而且违反了“单一职责”原则

对 daqing 回复

代码结构和 README 里面的例子是一模一样的,例子怎么做单元测试,上面的代码也是一样做,单一职责并没有违反,做的事情都在各自代码里面,和例子也是一模一样的

对 quakewang 回复
class CreateUser
  def perform(args)
    user = User.create!(args)

    [SendWelcomeEmail, GiveCouponIfXmas, GiveInitialCash].each do |clazz|
      clazz.(args)
    end
  end
end

从职责的角度,“创建用户”这个业务逻辑,不应该关心是否要“发邮件”“发优惠券”等职责。所以,这个代码,让 CreateUser 有了多个职责,违反了“单一职责原则”。

另外,还违反了 Open/closed principle。因为我要想在用户注册之后,加入更多逻辑的话,使用我的设计,只需要修改 config/initializer/business.rb 里面的配置,不需要修改 CreateUser的 perform() 方法。而你的例子,必须去修改 perform() 方法的实现。

其实是看你哪部分代码更加常变,更加容易与他人冲突

就像 java 的 aop,有人写在 xml,有人写在 model 的 annotation 上

对 daqing 回复

因为我要想在用户注册之后,加入更多逻辑的话

比如要加一个 FooBar 的逻辑,你的做法是在 config/initializer/business.rb 加一行:

Fanli.on(:create_user, [
  SendWelcomeEmail,
  GiveCouponIfXmas,
  GiveInitialCash,
  FooBar,
])

而我的做法是在 CreateUser 里面加一行

[SendWelcomeEmail, GiveCouponIfXmas, GiveInitialCash, FooBar].each

剩下其他代码都是一样的,你可以说你那个是配置,我那个需要修改代码,但是在这个例子里面看不出两者有什么本质区别。

如果非要改成配置的方式,那么我们改一下代码:

class CreateUser
  def perform(args)
    user = User.create!(args)
    callbacks(args)
  end

  def callbacks(args)
    callbacks_from_config.each do |clazz|
      clazz.(args)
    end
  end
end

即使抽象一个 Base 出来,看起来和你的例子一模一样了,但是本质上还是意大利面条:

class Base
  def perform(args)
    _perform(args)
    callbacks(args)
  end
end

class CreateUser < Base
  def _perform(args)
    user = User.create!(args)
  end
end

所以从这个例子上我看不出有用 Pub/Sub 的任何优势

对 quakewang 回复

那对于这类需求,有没有不是意大利面条的写法?

两个误区:

  1. Fat Models 不是 Rails 官方推荐的模式(没有来源证明这是官方推荐)。
  2. 把 send email 放到 callback 不是“The Rails Way” http://david.heinemeierhansson.com/2012/emails-are-views.html
对 Rei 回复

其实我这个就是 service 啊,只是没有用 XXXService来命名。而是用了动词 + 名词的方式。

对 daqing 回复

业务层我喜欢显式的代码,需要一套组合逻辑就在 app/ 下开个文件夹放同类逻辑好了,放在 initializer 会让人困惑。而且方法也是显式调用,observer 模式在 debug 的时候很麻烦。

虽然 DHH 最近写了 https://m.signalvnoise.com/programming-with-a-love-of-the-implicit-66629bb81ee7 ,但我觉得魔法代码是框架创造者的特权,并且框架是有很多文档资料做补充的,业务层代码还是尽量显式。

对 Rei 回复

并不一定放在 initializer 下面,也可以放到 app/business/registry.rb里面

对 daqing 回复

对于这种需求,我推荐 Decorator 模式,有现成的 gem draper 可以用

https://github.com/drapergem/draper

对 daqing 回复

和 wisper 比有什么优势?解决了什么问题?

对 lithium4010 回复

和 wisper 本身没什么优势,但是后面准备添加 sidekiq 支持。wisper 的 sidekiq gem,已经很久不维护了。

进展:发布 0.2.1 版本

主要是更新了 API 设计。

事件注册与触发

Fanli.when(:create_user, exec: proc { |args| # business logic goes here })
Fanli.trigger(:create_user, with: {name: 'daqing'})

业务逻辑类

class CreateUser < Fanli::Base
  def perform
    # business logic goes here
  end
end

调用方法:

CreateUser.exec(name: 'daqing', age: 30)

想法不错,好与坏不做评论,有点类似 serverless 的味道,只是离 serverless 还很遥远,鼓励一下。

不就是 wisper 做的事情吗?

支持一下

x.call(a) 可以写成 x.(a),像 trailblazer 就是用这种语法 CreateUser.(name: 'daqing', age: 30)

dry-transaction 基于 wisper,功能很类似。

wisper 已经很好了,就是 async 那几个 Gem 都不怎么维护了,用起来比较担心~

需要 登录 后方可回复, 如果你还没有账号请 注册新账号