最近一直在思考实现复杂业务逻辑的更好方法,于是有了 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 去重构比较复杂的业务代码~~
感觉这和直接写意大利面代码没什么区别
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
代码结构和 README 里面的例子是一模一样的,例子怎么做单元测试,上面的代码也是一样做,单一职责并没有违反,做的事情都在各自代码里面,和例子也是一模一样的
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 上
比如要加一个 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 的任何优势
两个误区:
业务层我喜欢显式的代码,需要一套组合逻辑就在 app/ 下开个文件夹放同类逻辑好了,放在 initializer 会让人困惑。而且方法也是显式调用,observer 模式在 debug 的时候很麻烦。
虽然 DHH 最近写了 https://m.signalvnoise.com/programming-with-a-love-of-the-implicit-66629bb81ee7 ,但我觉得魔法代码是框架创造者的特权,并且框架是有很多文档资料做补充的,业务层代码还是尽量显式。
和 wisper 本身没什么优势,但是后面准备添加 sidekiq 支持。wisper 的 sidekiq gem,已经很久不维护了。
主要是更新了 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)
x.call(a)
可以写成 x.(a)
,像 trailblazer 就是用这种语法 CreateUser.(name: 'daqing', age: 30)
dry-transaction 基于 wisper,功能很类似。