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

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

前言

最近一直在思考实现复杂业务逻辑的更好方法,于是有了 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 呢?

3 楼 已删除

还不如直接用 .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 都不怎么维护了,用起来比较担心~

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