Day16. Service, Strategy and Singleton Pattern

设计流程的出现,让我们可以写出一套好的流程,并且帮助团队少写多余的程序码。由於Ruby不像Javascript,是标准的物件导向语言,当然也可以使用各种形式的设计流程。今天,我们会介绍常用、漂亮的设计流程。

Service pattern

我们先举以下为寄发简讯的 Message服务为例

class Sms::SendMessage
  def self.call(phone, code: nil)
    new(phone, code).call
  end

  def initialize(phone, code)
    # @host, @username, @password 为存在专案内部的加密变数
    @host = Rails.application.credentials.sms[:host]
    @username = Rails.application.credentials.sms[:username]
    @password = Rails.application.credentials.sms[:password]
    # @dstaddr: 电话号码
    # @smbody: 简讯欸荣
    @dstaddr = phone
    @smbody = "#{code} is your verification code."
  end

  attr_reader :host, :username, :password, :dstaddr, :smbody

  def call
    ApiClient.post host, headers: nil, payload: { username: username, password: password, dstaddr: dstaddr, smbody: smbody }
  end
end

其中ApiClientDay12提到过,是专门打API用的类别方法。

Sms::SendMessage使用方式如下,第一个参数为欲寄发简讯的电话号码,第二个参数为验证码。

Sms::SendMessage.call('0983168969', code: '123456')

只要填妥两个参数,就可以发简讯了!

我们习惯使用call表示我们要呼叫该参数,而call 里面包含呼叫实体变数,而这种行为我们称为委托delegate。将self.new(*params).call的行为委托给call去执行的设计流程,称为Service Pattern

def self.call(phone, code: nil)
  # self.new(phone, code).call 的简写
  new(phone, code).call
end

像这种单一class负责单一职责的设计模式Service Pattern, 是Rails常见的设计模式之一。

自己曾做过的电商专案中,除了简讯发送Service以外,还有以下这些 Service。

第三方相关
  金流相关
    绿界付款
    绿界退款
    绿界开立发票
    绿界折让发票
    绿界查询付款状态
  物流相关
    顺丰物流下单
    顺丰物流查询货态
  简讯相关
    发送验证码

电商相关
  付款动作
  退款动作
  POS给点动作

这些Service的目的只为单一职责(一次只做一件事)。举例来说因此若排程端、後台使用者端、顾客端需要做退款动作的时候,只需要呼叫退款流程 service即可。而这些ServiceMVC核心架构耦合性越低,重复使用的可能性跟便利性就更高,在同个专案所使用的Service 多和 ActiveRecord耦合,若要成为给大家复用的Gem,那就必须写出耦合性更低的程序码。

举简单的例子讲解Service Pattern

class AppleService
  def self.call(*args, &block)
    new(*args).call(&block)
  end

  def initialize(a, b = nil, options = {})
    @a, @b,@c = a, b, options[:c]
  end

  attr_reader :a, :b, :c

  def call
    p "=== what is a: #{a} ==="
    p "=== what is b: #{b} ==="
    p "=== what is c: #{c} ==="
    
    if block_given?
      yield(a, b, c) 
    else
      {a: a, b: b, c: c}
    end
  end
end

至於如何使用参数,我们在Day11提及过,这里就先简单举例AppleService带入不同参数的结果。

AppleService.call("a")             #=> {:a=>"a", :b=>nil, :c=>nil}
AppleService.call("a", c: 1)       #=> {:a=>"a", :b=>{:c=>1}, :c=>nil}
AppleService.call("a", nil, c: 1)  #=> {:a=>"a", :b=>nil, :c=>1}

我们也可以搭配 block 使用(Block 的使用可以参考Day9Day10

以下区块负责的内容是将收到的三个值用斜线接起来再回传,区块是个有趣的章节,推荐读者在学Rails的时候切万不要退避三舍

AppleService.call("a", "b", c: 1) {|x,y,z| [x,y,z].join('/')} #=> "a/b/1"

Singleton Pattern

当我们对某物件新增单体方法,只有该物件有独特技能,而若将某类别新增单体方法的话,则为类别方法。Day12Day13 提过,这边就不细谈

Strategy Pattern

这是我个人常使用的Pattern之一,不管是打Api、串接金流、串接物流、汇入表单等变化性很低(偶尔才有变化)的动作,都可以用Strategy Pattern 来去实现!Strategy Pattern可以用来设定一套流程,我们举一个不存取值的例子来讲:

module Strategy
  DEFAULT_STRATEGY = {
    params: -> { p '组成资料' },
    perform: -> { p '打Api' },
    receiver: -> { p '成功或失败' },
    handler: -> { p '取得 response payload 并处理' }
  }
  
  def get_sty_flow
    block_given? ? yield(DEFAULT_STRATEGY) : DEFAULT_STRATEGY
  end
  
  def sty_flow
    get_sty_flow.map do |k, v|
      # 先找有没有前缀为sty的方法
      go_method = "sty_#{k}".to_sym
      # 没有的话取得 get_sty_flow 的 key
      go_method = v if !self.respond_to?(go_method)
      next unless go_method.present?
      
      # 判断是否是Proc,若不是则视为 method
      if go_method.is_a? Proc
        go_method.call
      else
        self.send(go_method) 
      end
      
      # 回传key(无意义)
      k
    end
  end
end

看到DEFAULT_STRATEGY 对应 Proc,就有一种写Javascript function可以当作变数的感觉。

说明: 

1. 流程会写在 get_sty_flow 方法内,而DEFAULT_STRATEGY 为预设的流程
2. 执行流程的地方在 sty_flow
3. DEFAULT_STRATEGY 的value可以是Proc,也可以是symbol

接着我们介绍以下三种情境。

情境A ➡️ 使用预设的Strategy

class A
  include Strategy
end

A.new.sty_flow
#
#======== 印出以下资讯 ========#
# 
#   "组成资料", 
#   "打Api", 
#   "成功或失败", 
#   "取得 response payload 并处理"
#
#=> [:params, :perform, :receiver, :handler]

情境B ➡️ 使用继承,增添客制化的流程processor

class B
  include Strategy
  
  def get_sty_flow
    super do |strategy|
      { **strategy, processor: -> {'资料处理'} } 
    end
  end
end

B.new.sty_flow
#
#======== 印出以下资讯 ========#
#
#   "组成资料", 
#   "打Api", 
#   "成功或失败", 
#   "取得 response payload 并处理"
#
#=> [:params, :perform, :receiver, :handler, :processor]

情境C ➡️ 使用客制化的方法 sty_params

class C
  include Strategy
  
  def sty_params
    p '在C组成资料,而非在预设流程'
  end
end

C.new.sty_flow
#
#======== 印出以下资讯 ========#
# 
#   "在C组成资料,而非在预设流程", 
#   "打Api", 
#   "成功或失败", 
#   "取得 response payload 并处理"
#
#=> [:params, :perform, :receiver, :handler]

https://ithelp.ithome.com.tw/upload/images/20210914/20115854Ipakf00UGW.png

我们可以发现上面的方法是真的都可以被抽换的,符合 Strategy Pattern 的精神,也很Gurustrategy网站所使用的icon

结论

其实还有很多设计流程没有讲到,但碍於篇幅的关系,今天先讲三个。在Day32讲解摊提时,我们会介绍另一个设计流程Decorator Pattern

Class 系列到此告一个段落,明天开始讲Dynamic Programming

参考资料


<<:  无线上网:Wi-Fi, 3G, 4G 及 5G 都是些是什麽?

>>:  建立第一笔NoSQL资料

DAY9 MongoDB 文件与嵌入式(巢状)文件查询(Find)

DAY9 MongoDB 文件与嵌入式(巢状)文件查询(Find) Find 把 MongoDB 的...

Day16:【TypeScript 学起来】新增任意属性的好方法:Index Signatures 索引签名

在之前 interface 那篇文章, 认识到可以使用 Index Signatures, 发现他...

[Day28] 正规表达式 Regular Expression

这几天写 String methods 的时候,在句法里发现(regexp)这个词,查了一下原来是 ...

Day15-D3 的 Zoom 缩放

本篇大纲:d3.zoom( )、zoom 旗下的API、范例 上一篇看完让人烧脑的Force之後,...

Grid 展开 Detail - day18

之前范例执行结束如上所示,倘若我们希望点选学生即展开该学生成绩怎麽做? Grid 显示 Detai...