Rails中的设计模式

Database and Ruby, Python, History


观察者模式

Rails之前有Observer,后来被单独抽离到rails-observer gem中去了。和callback比,observer一般处理model职责之外的行为,比如给用户发邮件。callback更加倾向于处理model职责内的行为,比如验证,默认值等。在我看来,有点像是消息队列,observer订阅/注册某个主题,主题推送通知。此外,观察者模式相对比callback,解耦方面更好些。

img

装饰器模式

在Python里面很多,比如特性。在Rails里面,是通过委派delegate来实现的。

Ruby中的include是修改了当前类,或者类的继承链。在我看来,不能够算是装饰器,是因为:

  1. 不能够多次装饰
  2. 一般装饰的对象是实例,而不是类

当然,依旧可以用extend实例的方式去实现,但依旧不能够多次装饰。

下面是用SimpleDelegator来实现。实际上是通过委派方式,把method在委派给super。在Rails中,你可以通过delegate来实现。1 2

require 'delegate'

class Coffee
  def cost
    2
  end

  def origin
    "Colombia"
  end
end

module DecoratorClass
  def class
    __getobj__.class
  end
end

class Milk < SimpleDelegator
  include DecoratorClass

  def cost
    super + 0.4
  end
end

class Sugar < SimpleDelegator
  include DecoratorClass

  def cost
    super + 0.2
  end
end

p Milk.ancestors # [Milk, DecoratorClass, SimpleDelegator, Delegator, #<Module:0x00007fb5eb8579f8>, BasicObject]
coffee = Coffee.new
Sugar.new(Milk.new(coffee)).cost   # 2.6
Sugar.new(Sugar.new(coffee)).cost  # 2.4
Milk.new(coffee).origin            # Colombia
Sugar.new(Milk.new(coffee)).class  # Coffee

单件模式

比较常见的是调用第三方API的client工具,一般只会initial一个实例,这样可以避免多个client导致连接数超多,而且可以复用内存里面已经存在的client。

工厂模式

定义一个用于创建对象的接口,通常为build,让子类决定实例化哪一个类。工厂方法是一个类的实例化延迟到了子类。比如,创建一个DNS记录,背后的provider有多个的时候,就可以通过这种方式,在创建的时候去判断用哪个类实例化。

img

适配器模式

可以用于对不同的接口进行包装以及提供统一的接口,或者是让某一个对象看起来像是另一个类型的对象。比较常见的就是ActiveRecord的各种adapter,比如背后支持sql-server, mysql就有不同的adapter。对于ActiveRecord, 只有简单的execute,但是会根据具体的类型,来判断生成什么样的query,如何去连接数据库执行。3

img

模板模式

个人觉得有点像接口,或者Python中的ABC。基类只声明接口,子类负责具体的实现,没有实现的话,就会调用基类的方法,一般会raise error。

迭代器模式

这个在Ruby里面比较容易实现。通过include Enumerable模块并自定义each method,就可以创建一个迭代器类。下面是一个二叉查找树的例子。

class Bst

  include Enumerable

  attr_reader :left, :right, :data

  def initialize(new_data)
    @data = new_data
  end

  def insert(new_data)
    if new_data <= @data
      @left ? @left.insert(new_data) : @left = Bst.new(new_data)
    else
      @right ? @right.insert(new_data) : @right = Bst.new(new_data)
    end
    self
  end

  def each(&block)
    return to_enum unless block_given?

    @left&.each(&block)
    yield @data
    @right&.each(&block)

    self
  end
end