🍅

Rubyで書くデザインパターン

2023/01/03に公開約31,500字

概略

Template Method 系
名前 概要
Template Method initialize に書けば最初に実行されると決まっているそういうやつ
Abstract Class Ruby だとあまり利点がない Template Method 風なやつ
Factory Method new を別オブジェクトが行うだけ(?)
Singleton 系
名前 概要
Singleton インスタンスをグローバル変数にしたいとき用
Monostate new できるけど状態は共有
Single-Active-Instance Singleton 複数あるインスタンスのなかで一つだけが有効になる
リファクタリング(?)
名前 概要
Composed Method 巨大なメソッドを分割する。リファクタリングの第一歩
Adapter ダメなインターフェイスをいろんな手段で隠す
Value Object 値をクラス化する。Immutable にする
メモ化
名前 概要
Flyweight 効率化を目的とする。Immutable が多い
Sharable データの一貫性を目的とする。Mutable
Imposter
名前 概要
Null Object データ不在を存在するかのように扱う。条件分岐したら負け
Composite 集合を単体のように扱う
  • Imposter には「偽物」「詐欺師」の意味がある
  • Imposter は「振りをする」パターンの総称と思われる
その他 (分類を諦めた)
名前 概要
Strategy 挙動を他のクラスに任せる (外から明示的に)
State 挙動を他のクラスに任せる (内でこっそり) 3択以上が多い
Pluggable Object State と同じ。冗長なif文を改善した直後の、2択の State なことが多い
Observer 仲介して通知する。一方通行であること
Component Bus Observer が Subject を握っている
Iterator each のこと
Prototype new ではなく clone で
Builder xml.body { xml.p("x") } みたいなやつとかいろいろ
Abstract Factory クラスをハードコーディングしない
Decorator 元のクラスを汚したくない潔癖症な人向け
Visitor Pathname.glob("*.rb") {...}
Chain of Responsibility resolve? なら受ける。(順にリンクしてないといけないか疑問)
Facade 単にメソッド化?
Mediator A と B で困ったら Mediator クラスが必要
Memento 前の状態に戻りたいとき用
Proxy すり替えて、呼んだり呼ばなかったり、まねたり、あとで呼ぶ
Command 命令をクラスにする。migrate のあれ。お気に入り。
Policy Command パターンで複雑な条件の組み合わせをほぐす
Interpreter 文法規則をクラスで表現
DSL ドメイン特化言語
Object Pool 生成に時間がかかるものを使い回す
Pluggable Selector 横着ポルモルフィック
Before / After 後処理を必ず実行
Bridge よくわからない。クラスが増えないようにする
Typed Message GUI でよくある XXX::Event::Mouse::Click みたいなあれ
Cache Manager 使ったキャッシュは先頭に移動する
Marker Interface 印付けとしてのインターフェイスというかモジュール?
Generation Gap ソースコードジェネレーターは親クラスだけ再生成する
Hook Operation 実行処理の前後に何かを実行できるようにしておく
Collecting Parameter 結果を集める
CoC 設定より規約
Transaction Script 巨大なメソッド1つであれこれやっている
First Class Collection だいたい Value Object と同じで配列をラップする

Mediator

class Mediator
  attr_reader :a, :b

  def initialize
    @a = A.new(self)
    @b = B.new(self)
  end

  def changed
    @b.visible = @a.state
  end
end

class A
  attr_accessor :state

  def initialize(mediator)
    @mediator = mediator
    @state = true
  end

  def changed
    @mediator.changed
  end
end

class B
  attr_accessor :visible
  def initialize(mediator)
    @mediator = mediator
  end
end

m = Mediator.new
m.a.state = true
m.a.changed
m.b.visible # => true
  • 特徴
    • A と B は互いのことを知らない
    • A は変更したことを B ではなく Mediator に伝える
    • C ができたとしても A の挙動は変わらない
  • メリット
    • 関連オブジェクトへの調整を一箇所で行える
    • 疎結合化
  • デメリット
    • Mediator がいい感じに存在してくれるせいでなんでも屋になってしまう
    • 間違って適用すると疎結合にしたことで逆に扱いにくくなる場合がある

Factory Method

class Base
  def run
    object
  end
end

class App < Base
  def object
    String.new
  end
end

App.new.run
  • 人によって解釈というか使用目的がかなり異なる
    • クラスの選択をサブクラスに押し付けるのが目的
    • 単に複雑なインスタンス生成手順を別のクラスにまかせるのが目的
  • クラスの選択をサブクラスに押し付けるのが目的とした場合
    • Template Method パターンでもある
    • Java ではクラスの選択が難しいゆえに無駄に階層ができてしまう
    • Ruby ならクラスをそのまま渡せばいい

Abstract Factory

group = { a: X, b: Y }
  • X と Y の組み合わせで作ればいいことが group 経由で保証される
    • X と Y しか無いならやる意味がない
    • X と Y の他に10個ぐらいあっても迷わないなら不要
    • X と Y の他に100個ぐらいあって間違えるならやっと使うぐらいでいい
  • group で挙動が切り替わるという意味で言えば Strategy でもある
  • 柔軟性を封じる意味では CoC なところもある

Chain of Responsibility

class Chainable
  def initialize(next_chain = nil)
    @next_chain = next_chain
  end

  def support(q)
    if resolve?(q)
      answer(q)
    elsif @next_chain
      @next_chain.support(q)
    else
      "?"
    end
  end
end

class Alice < Chainable
  def resolve?(q)
    q == "1+2"
  end

  def answer(q)
    "3"
  end
end

class Bob < Chainable
  def resolve?(q)
    q == "2*3"
  end

  def answer(q)
    "6"
  end
end

alice = Alice.new(Bob.new)
alice.support("1+2") # => "3"
alice.support("2*3") # => "6"
alice.support("2/1") # => "?"
  • A は B を持ち、B は C を……な構造は本当に必要なんだろうか?
    • A.new(B.new(C.new(D.new(E.new)))) になってしまう
  • 単に [A, B, C, D, E] と並べて上から順に反応したやつを実行じゃだめ?

Proxy

Decorator に似ているけど Decorator ほどデコレートしないし便利メソッドも追加しない
元のインスタンスをどうするかはだいたい次の3つに分かれる

種類 意味
防御 呼ぶか、呼ばないか
仮想 まねる
遅延 あとで呼ぶ
防御 (呼ぶか、呼ばないか)
require "active_support/core_ext/module/delegation"

class User
  attr_accessor :name, :score

  def initialize(name)
    @name = name
    @score = 0
  end
end

class UserProxy
  BLACK_LIST = ["alice"]

  delegate :score, to: :@user

  def initialize(user)
    @user = user
  end

  def method_missing(...)
    unless BLACK_LIST.include?(@user.name)
      @user.send(...)
    end
  end
end

user = User.new("alice")
user.score += 1
user.score                      # => 1

user = UserProxy.new(User.new("alice"))
user.score += 1
user.score                      # => 0
仮想 (まねる)
class VirtualPrinter
  def name
    "初期化が遅いプリンタ"
  end

  def print(str)
  end
end

printer = VirtualPrinter.new
printer.name        # => "初期化が遅いプリンタ"
printer.print("ok") # => nil
遅延 (あとで呼ぶ)
class VirtualPrinter
  def name
    "初期化が遅いプリンタ"
  end

  def print(str)
    @printer ||= RealPrinter.new
    @printer.print(str)
  end
end

class RealPrinter
  def initialize
    puts "とてつもなく時間がかかる初期化処理..."
  end

  def name
    "初期化が遅いプリンタ"
  end

  def print(str)
    str
  end
end

printer = VirtualPrinter.new
printer.name        # => "初期化が遅いプリンタ"
printer.print("ok") # => "ok"
# >> とてつもなく時間がかかる初期化処理...

Command

commands = []
commands << -> { 1 }
commands << -> { 2 }
commands.collect(&:call)        # => [1, 2]
  • Rails の Migration のような仕組みもそう
  • perform, call, run, evaluate, execute のようなメソッドがよく使われる
    • 一つのクラスで混在させてはいけない
  • 逆にそのメソッドがあれば Command パターンだと推測できる
  • Ruby なら call で統一すると一貫性が保てる
    • Proc や lambda に置き換えれる

Policy

ありがちなやつ
require "active_support/core_ext/module/delegation"

class User
  attr_accessor :name

  delegate :editable?, to: :policy

  def initialize(name)
    @name = name
  end

  def policy
    UserPolicy.new(self)
  end
end

class UserPolicy
  def initialize(user)
    @user = user
  end

  def editable?
    @user.name == "alice"
  end
end

User.new("alice").editable?        # => true
User.new("bob").editable?          # => false
  • 関心事「権限」で分離したいとき用

Command パターンの活用

class PositiveRule
  def valid?(value)
    value.positive?
  end
end

class EvenRule
  def valid?(value)
    value.even?
  end
end

rules = []
rules << PositiveRule.new
rules << EvenRule.new
rules.all? { |e| e.valid?(2) } # => true
  • 複雑な条件の組み合わせを Command パターンでほぐせる
  • ActiveRecord の Validator もこれ
  • 単に成功/失敗ではなく、あとからエラーメッセージも構築したいとなったときも分離しているとやりやすい
  • メソッドに引数を渡すのではなくインスタンス生成時に渡してもいい

Composite

class Node
  attr_accessor :left, :expr, :right

  def initialize(left, expr, right)
    @left = left
    @expr = expr
    @right = right
  end

  def to_s
    "(" + [@expr, @left, @right] * " " + ")"
  end
end

a = Node.new(1, :+, 2)
b = Node.new(3, :+, 4)
c = Node.new(a, :*, b)
c.to_s                          # => "(* (+ 1 2) (+ 3 4))"

再帰的に to_s が呼ばれる点を見れば Composite と言えなくもない

Prototype

A = Object.clone

B = A.clone.tap do |o|
  def o.foo
    true
  end
end

B.clone.foo # => true
  • 基本的に new は使わない
  • 何のメリットがあるのかはわからない
  • オブジェクト生成時のコストが高い場合には有用なのかもしれない

Template Method

class Base
  def run
    a + b
  end
end

class App < Base
  def a
    1
  end

  def b
    2
  end
end

App.new.run # => 3
  • 差分プログラミング最高
    • 綺麗に決まると気持ちよい
    • OAOO原則と相性が良い
  • 指定のメソッドだけ埋めればいいとはいえスーパークラスの意向を正確に把握しておかないといけない場合も多い
  • 神クラス化に注意
    • 多用しているとグローバル空間に大量のメソッドがあるのと変わらない状況になってくるので注意
    • 単にオプション引数を工夫するとかコンポジション構造にする方が適している場合もある
  • これも?
    • initialize メソッドも書けば最初に呼ばれると決まっているので Templete Method と言えなくもない
    • Arduino だと setup と loop 関数を書けばいい感じに呼ばれるようになっている
    • C の main 関数も広義の Templete Method か?

Abstract Class

class Player
  def play
    raise NotImplementedError, "#{__method__} is not implemented"
  end
end

class MusicPlayer < Player
  def play
  end
end

class VideoPlayer < Player
  def play
  end
end

players = []
players << MusicPlayer.new
players << VideoPlayer.new
players.each(&:play)
  • Java ならではの仰々しいパターンと言える
    • こうしないと同じ配列に入れられないから
  • Ruby なら何もしてない Player クラスはいらない

Iterator

class Iterator
  def initialize(object)
    @object = object
    @index = 0
  end

  def next?
    @index < @object.size
  end

  def next
    @object[@index].tap { @index += 1 }
  end
end

class Array
  def xxx
    it = Iterator.new(self)
    while it.next?
      yield it.next
    end
  end
end

%w(a b c).xxx { |e| p e }
# >> "a"
# >> "b"
# >> "c"
  • each みたいなやつのこと
  • 自力で書く機会は少ないけど構造は知っときたい

Memento

ブラックジャックを行うプレイヤーがいるとする

class Player
  attr_accessor :cards

  def initialize
    @cards = []
  end

  def take
    @cards << rand(1..13)
  end

  def score
    @cards.sum
  end
end

5回カードを引くゲームを3回行うと全部21を越えてしまった

3.times do
  player = Player.new
  5.times { player.take }
  player.score                  # => 33, 37, 52
end

そこで Memento パターン

class Player
  def create_memento
    @cards.clone
  end

  def restore_memento(object)
    @cards = object.clone
  end
end

21点未満の状態を保持しておき21を越えたら元に戻す

3.times do
  player = Player.new
  memento = nil
  5.times do
    player.take
    if player.score < 21
      memento = player.create_memento
    elsif player.score > 21
      player.restore_memento(memento)
    end
  end
  player.score                  # => 18, 19, 15
end

memento には復元に必要なものだけ入れとく

Visitor

Pathname.glob("**/*.rb") { |f| }

汎用性のある渡り歩く処理と、汎用性のない利用者側の処理を分ける

Flyweight

module Sound
  class << self
    def fetch(name)
      @cache ||= {}
      @cache[name] ||= load("#{name}.mp3")
    end

    private

    def load(name)
      rand
    end
  end
end

Sound.fetch(:battle) # => 0.16636604715291592
Sound.fetch(:battle) # => 0.16636604715291592
  • メモ化で効率化すること
  • Immutable にすることが多い
  • Immutable な点から見れば Value Object な役割りになっていることもある

Sharable

class Color
  class << self
    def create(name, ...)
      @create ||= {}
      @create[name] ||= new(name, ...)
    end
  end

  attr_accessor :name, :lightness

  def initialize(name)
    @name = name
  end
end

color = Color.create(:white)
color.lightness = 1.0

Color.create(:white).lightness  # => 1.0
  • コードだけ見れば Flyweight と変わらない
  • データの一貫性を保つのが目的であれば Sharable になる
  • Mutable なのが特徴

Builder

なんか汚い

class Node
  attr_reader :name, :children

  def initialize(name)
    @name = name
    @children = []
  end
end

root = Node.new("root")
root.children << Node.new("a")
root.children << Node.new("b")
root.children << (c = Node.new("c"))
c.children << Node.new("d")
c.children << Node.new("e")
c.children << (f = Node.new("f"))
f.children << Node.new("g")
f.children << Node.new("h")

root.children.collect(&:name)                             # => ["a", "b", "c"]
root.children.last.children.collect(&:name)               # => ["d", "e", "f"]
root.children.last.children.last.children.collect(&:name) # => ["g", "h"]

改善後

class Node
  def add(name, &block)
    tap do
      node = self.class.new(name)
      @children << node
      if block_given?
        node.instance_eval(&block)
      end
    end
  end
end

root = Node.new("root")
root.instance_eval do
  add "a"
  add "b"
  add "c" do
    add "d"
    add "e"
    add "f" do
      add "g"
      add "h"
    end
  end
end

root.children.collect(&:name)                             # => ["a", "b", "c"]
root.children.last.children.collect(&:name)               # => ["d", "e", "f"]
root.children.last.children.last.children.collect(&:name) # => ["g", "h"]
Builder のもっとシンプルな例

AddressContainer なんて利用者にとっては知らなくていいもの

class AddressContainer
  def initialize(address)
    @address = address
  end
end

class Mail
  attr_accessor :to
end

mail = Mail.new
mail.to = AddressContainer.new("alice <alice@example.net>")

改善後

class Mail
  attr_reader :to

  def to=(address)
    @to = AddressContainer.new(address)
  end
end

mail = Mail.new
mail.to = "alice <alice@example.net>"

Facade

こんなのをあっちこっちに書かせるんじゃなくて

message = Message.new(date: Time.now)
message.from = User.find_by(name: "alice")
message.to   = User.find_by(name: "bob")
message.body = "..."
if message.valid?
  message.save!
end
MessageMailer.message_created(message).deliver_later

次のように使いやすいメソッドにしとけってことかな?

Message.deliver(from: "alice", to: "bob", body: "...")

Bridge

Aが2個でBが2個なので継承を重ねると組み合わせは 2 * 2 で4パターンになる
もしAが10個でBが10個なら100パターンになって破綻する

class A; end

class A1 < A; end
class A2 < A; end

class A1_B1 < A1; end
class A1_B2 < A1; end
class A2_B1 < A2; end
class A2_B2 < A2; end

A1_B1.new                       # => #<A1_B1:0x0000000108a2fe00>

改善後

class A
end

class A1 < A; end
class A2 < A; end

class B
  def initialize(a)
    @a = a
  end
end

class B1 < B; end
class B2 < B; end

B1.new(A1.new)                  # => #<B1:0x000000010f3efd68 @a=#<A1:0x000000010f3efde0>>

これなら組み合わせ爆発しない
Aシリーズと、Bシリーズを個々に作るだけ
普通にあるようなコードなのでどこが Bridge なのかはよくわかってない

Decorator

Proxy に似ているけど遅延実行や実行条件には関心がない

require "delegate"

class User
  def name
    "alice"
  end
end

class UserDecorator < SimpleDelegator
  def call_name
    "#{name}さん"
  end
end

UserDecorator.new(User.new).call_name # => "aliceさん"

モデルが肥大化してもなんら問題ないので基本使わない
使うとしても委譲して公表しない
モデルと Decorator の関係が1対1ならメリットは少ないしデメリットと相殺する
1対多なら挙動を切り替える目的で便利かもしれないがそれは Decorator とは呼べない

Observer

Subject からの一方通行でないといけない
たまに戻値が欲しくなる場合があるけど、それはもう Observer ではなくなっている
Observer 側に Subject (player) を渡して player.add_observer(self) は、まわりくどいので自分はやらない
Observer に player を握らせたら Component Bus パターンになるらしい

class Player
  def initialize
    @foo = Foo.new
    @bar = Bar.new
  end

  def notify
    if @foo
      @foo.update(self)
    end
    if @bar
      @bar.update(self)
    end
  end
end

上の密結合状態を解消する

class Player
  attr_accessor :observers

  def initialize
    @observers = []
  end

  def notify
    @observers.each do |observer|
      observer.update(self)
    end
  end
end

player = Player.new
player.observers << Foo.new
player.observers << Bar.new

標準ライブラリを使うと簡潔になる

require "observer"

class Player
  include Observable

  def notify
    changed
    notify_observers(self)
  end
end

player = Player.new
player.add_observer(Foo.new)
player.add_observer(Bar.new)
player.notify

人にはおすすめしないけど自分をオブザーバーにしてもいい

require "observer"

class Player
  include Observable

  def initialize
    add_observer(self)
  end

  def notify
    changed
    notify_observers(self)
  end

  def update(player)
    player # => #<Player:0x007ff9098472e0 ...>
  end
end

player = Player.new
player.notify

これを「ぼっちObserverパターン」と勝手に呼んでいる

Component Bus

Observer たちがデータ共有したいので Subject を共有することにしたパターン

class Player
  include Observable

  attr_accessor :xxx

  def notify
    changed
    notify_observers
  end
end

class Display
  def initialize(player)
    player.add_observer(self)
    @player = player    # Subjectを握っている
  end

  def update
  end

  def xxx
    @player.xxx
  end
end

一方通行だった Observer が Subject 依存してしまうデメリットも考慮すること

Singleton

class C
  private_class_method :new

  def self.instance
    @instance ||= new
  end
end

C.instance # => #<C:0x007f98e404a518>
C.instance # => #<C:0x007f98e404a518>

標準ライブラリを使った場合

require "singleton"

class C
  include Singleton
end

C.instance # => #<C:0x007f98e509f558>
C.instance # => #<C:0x007f98e509f558>
  • どちらにしろ instance って書かないといけないのがちょっと面倒だったりする
  • でもあとでやっぱりグローバル変数にするのやめたいってなったとき instance を new に置換するだけでいいのは楽そう
  • で、instance って書くのがやっぱり面倒なときは割り切って次のように書いてもいい
module Config
  class << self
    attr_accessor :foo
  end
end

Config.foo # => nil

または

require "active_support/core_ext/module/attribute_accessors"

module Config
  mattr_accessor :foo
end

Config.foo # => nil

Monostate

require "active_support/core_ext/module/attribute_accessors"

class C
  cattr_accessor :foo
end

a = C.new
b = C.new
a.foo = 1
b.foo                           # => 1
C.new.foo                       # => 1
  • instance ではなく new と書く
  • 結果として見れば Singleton だけど外からは Singleton のようには見えない
  • 実装者本人ですら騙される恐れあり

Single-Active-Instance Singleton

require "active_support/core_ext/module/attribute_accessors"

class Point
  cattr_accessor :current

  def self.run
    current&.name
  end

  attr_accessor :name

  def initialize(name)
    @name = name
  end

  def activate!
    self.current = self
  end
end

a = Point.new("a")
b = Point.new("b")
Point.run                      # => nil
a.activate!
Point.run                      # => "a"
b.activate!
Point.run                      # => "b"
  • 複数あるインスタンスのなかで一つだけが有効になる
  • 画面上にマウスで動かせる点が複数があってその一つを選択するようなときに使う(たぶん)

Strategy

class LegalDice
  def next
    rand(1..6)
  end
end

class CheatDice
  def next
    6
  end
end

class Player
  def initialize(dice)
    @dice = dice
  end

  def roll
    3.times.collect { @dice.next }
  end
end

Player.new(LegalDice.new).roll  # => [5, 2, 3]
Player.new(CheatDice.new).roll  # => [6, 6, 6]
  • Player のコードはそのままでサイコロのアルゴリズムを切り替える
  • 利用者は LegalDice や CheatDice を知っている
  • State と似ているが内部で切り替えるのではなく利用者が外から渡す
    • そういう意味では意図せず引数が Strategy になっていたりする
  • 上の例は大袈裟
    • Ruby ならコードブロックでいい

State

class OpenState
  def board
    "営業中"
  end
end

class CloseState
  def board
    "準備中"
  end
end

class Shop
  def change_state(hour)
    if (11..17).include?(hour)
      @state = OpenState.new
    else
      @state = CloseState.new
    end
  end

  def board
    @state.board
  end
end

shop = Shop.new
shop.change_state(10)
shop.board                      # => "準備中"
shop.change_state(11)
shop.board                      # => "営業中"
  • ちょっと例が悪かった
    • この例だと Pluggable Object とも言えてしまう
    • 管理しきれないほど多くの状態があったとき State になる
  • Strategy と似ているが内部で使うだけ
    • 利用者は OpenState CloseState のことを知らない
  • Pluggable Selector の対極にあるパターンでもある

Pluggable Object

class A
  def initialize(x)
    if x
      @object = X.new
    else
      @object = Y.new
    end
  end

  def foo
    @object.foo
  end

  def bar
    @object.bar
  end
end
  • そっくりな State との見分け方
    • State は3つ以上の状態があり、さらに増える可能性もあるときに使うパターンと考える
    • Pluggable Object はもともと同じ条件のif文による二択が同一クラス内で散乱している状態をリファクタリングしてポルモルフィックにしたものと考える
    • 状態を表わすかどうかで両者は分別できない
      • State は状態を表わす
      • 「テスト駆動開発」本の例だと Pluggable Object も(本意ではないかもしれないけど)状態を表している
  • 利用者には見えない形で使う
    • これも State と同じ
    • もし利用者が渡す形であればそれは Strategy になる

Adapter

require "matrix"

vector = Vector[2, 3]
vector[0]          # => 2
vector[1]          # => 3

class Vec2 < Vector
  def x
    self[0]
  end

  def y
    self[1]
  end
end

vector = Vec2[2, 3]
vector.x           # => 2
vector.y           # => 3
  • 他にも方法はいろいろある
    • 委譲する
    • オブジェクト自体に特異メソッドを生やす
    • Vector クラス側にメソッドを生やす
  • 委譲する場合 Proxy や Decorator と似たコードになる
  • が、重要なのはコードではなく意図

不適切なインターフェイスを伴う痛みがシステム中に広がることを防ぎたい場合に限りアダプタを選んでください
── Rubyによるデザインパターン P.155 より

Interpreter

シンプルなDSL

class Expression
end

class Value < Expression
  attr_accessor :value
  def initialize(value)
    @value = value
  end

  def evaluate
    "mov  ax, #{@value}"
  end
end

class Add < Expression
  def initialize(left, right)
    @left, @right = left, right
  end

  def evaluate
    [
      @left.evaluate,
      "mov  dx, ax",
      @right.evaluate,
      "add  ax, dx",
    ]
  end
end

def ADD(l, r)
  Add.new(Value.new(l), Value.new(r))
end

expr = ADD 1, 2
puts expr.evaluate
# >> mov  ax, 1
# >> mov  dx, ax
# >> mov  ax, 2
# >> add  ax, dx

Domain Specific Language (DSL)

class Environment
  def ax
    :AX
  end

  def bx
    :BX
  end

  def mov(left, right)
    puts "#{right} --> #{left}"
  end
end

Environment.new.instance_eval(<<~EOT)
mov ax, 0b1111
mov bx, ax
EOT
# >> 15 --> AX
# >> AX --> BX
  • rake や capistrano が有名
  • 一般的には eval(File.read(xxx)) の形で実行する
  • でも ActiveRecord の belongs_to, has_many なども DSL の一種
  • 括弧を使わなくなったら DSL みたいなもの

Typed Message

class MouseMotion
end

class App
  def receive(e)
    case e
    when MouseMotion
    end
  end
end

app = App.new
app.receive(MouseMotion.new)

GUI アプリでイベントが起きるといろんなものが飛んできて美しくない switch 文ができてしまうあれのこと

Cache Manager

class Cache
  attr_accessor :max, :pool

  def initialize
    @max = 2
    @pool = []
  end

  def fetch(key)
    v = nil
    if index = @pool.find_index { |e| e[:key] == key }
      v = @pool.slice!(index)[:val]
    else
      v = yield
    end
    @pool = ([key: key, val: v] + @pool).take(@max)
    v
  end
end

cache = Cache.new
cache.fetch(:a) { 1 }           # => 1
cache.pool                      # => [{:key=>:a, :val=>1}]
cache.fetch(:b) { 1 }           # => 1
cache.pool                      # => [{:key=>:b, :val=>1}, {:key=>:a, :val=>1}]
cache.fetch(:a) { 2 }           # => 1
cache.pool                      # => [{:key=>:a, :val=>1}, {:key=>:b, :val=>1}]
cache.fetch(:c) { 1 }           # => 1
cache.pool                      # => [{:key=>:c, :val=>1}, {:key=>:a, :val=>1}]
  • 最後に使ったキャッシュほど上に来る
  • a b で pool は b a の順になり、次の a で a b になり、次の c で c a b になる
  • しかしキャッシュサイズは 2 なので b が死んで c a

Before / After

基本形

begin
  p "before"
ensure
  p "after"
end

# >> "before"
# >> "after"

RSpec 風

require "active_support/callbacks"

class C
  include ActiveSupport::Callbacks
  define_callbacks :before, :after

  class << self
    def before(...)
      set_callback(:before, ...)
    end

    def after(...)
      set_callback(:after, ...)
    end
  end

  def run
    run_callbacks :before
  ensure
    run_callbacks :after
  end

  before { p 1 }
  before { p 2 }
  after  { p 3 }
  after  { p 4 }
end

C.new.run
# >> 1
# >> 2
# >> 3
# >> 4

Pluggable Selector

class C
  def run(type)
    send("command_#{type}")
  end

  def command_a
  end

  def command_b
  end
end

C.new.run(:a)                   # => nil
  • 特徴
    • 動的に自分のメソッドを呼ぶ
    • SOLID の S こと「単一責任」から見ればアンチパターン(?)
    • Composed Method のつもりでいればそんな問題はない
  • メリット
    • クラス爆発を抑えられる
  • デメリット
    • クラスが悪い方向に肥大化しかねない
    • command_a を grep してもどこから呼ばれているかわからない
      • Ruby だとそんなのはしょっちゅう
  • 注意点
    • ユーザー入力を元にする場合、プレフィクスなどを付けておかないと、すべてのメソッドが呼び放題になる

Object Pool

メモ化というよりメモリと速度のトレードオフ

class X
  attr_accessor :active
end

class C
  attr_accessor :pool

  def initialize
    @size = 2
    @pool = []
  end

  def new_x
    x = @pool.find { |e| !e.active }  # pool から稼働してないものを探す
    unless x                          # なければ
      if @pool.size < @size           # pool の空きがあれば、新たに作成
        x = X.new
        @pool << x
      end
    end
    if x
      x.active = true
    end
    x
  end
end

i = C.new
a = i.new_x                  # => #<X:0x007fd1cb08d5c8 @active=true>
b = i.new_x                  # => #<X:0x007fd1cb08d140 @active=true>
c = i.new_x                  # => nil
a.active = false
c = i.new_x                  # => #<X:0x007fd1cb08d5c8 @active=true>

Null Object

class NullLogger
  def debug(...)
  end
end

logger = NullLogger.new
logger.debug("x")                # => nil
  • インターフェイスが同じ「何もしない」オブジェクト
    • /dev/null にリダイレクトするのに似ている
  • 「無」を上手に表すとnull安全にできる
    • 例えば計算するとき「無し」を 0 と表現する
      • こうすることで0除算になったりと余計面倒になる場合もある
  • あちこちで if を書き散らして除外している場合に置き換えると綺麗になる
  • 「Nullなんとか」な命名は技術駆動命名なので Null より Empty の方がいいかもしれない

Composed Method

class Item
  def validate
    methods.grep(/validate_/).each { |e| send(e) }
  end

  private

  def validate_length
  end

  def validate_range
  end

  def validate_uniq
  end
end
  • 巨大化したメソッドをいい感じに分割する
  • private にしとこう
  • リファクタリングの第一歩
  • 引数が多すぎるメソッドが量産されてしまうのは本末転倒な感がある
  • 引数が多すぎるメソッドだらけになりそうなら
    • → 委譲する
    • → 引数で渡すのではなくインスタンス変数にする

Value Object

class Vector
  def self.[](...)
    new(...)
  end

  attr_accessor :x, :y

  private_class_method :new

  def initialize(x, y)
    @x = x
    @y = y
    freeze
  end

  def +(other)
    self.class[x + other.x, y + other.y]
  end

  def inspect
    [x, y].to_s
  end
end

Vector[1, 2] + Vector[3, 4]     # => [4, 6]
  • Immutable な点が特徴
    • initialize の最後で freeze するのがわかりやすい
      • が、遅延実行時のメモ化ができなくなってはまることが多い
      • その場合は initialize の中でメモ化に使う Hash インスタンスを freeze 前に用意しておいてそれをメモに使う
      • 例えば @memo = {} を用意しておいて foo メソッド内で @memo[:foo] ||= 1 + 2 などとすると怒られない
  • デザインパターンのなかでいちばん効果がある
    • ただの Integer や String な変数であっても、それをあちこちで引数に取る処理があったとすれば、方向を逆にできないか考える
    • とりあえず動いたからOKの考えでは、逆にする発想が思い浮かばなくなっていく
    • なので引数がくる度に頭の中で逆にしてみる癖をつける
  • 例外として何を犠牲にしてでも処理速度を優先させたいところでは使わない方がいい
  • Value Object に似ているけど Mutable で、識別子が同じであれば同一視するようなものは Entity というらしい
    • 例えば ActiveRecord のインスタンス
  • 四則演算子の他に <=> == eql? hashto_s inspect をいい感じに定義しておくと使いやすくなる
class Foo
  attr_accessor :my_value

  class << self
    def [](...)
      wrap(...)
    end

    def wrap(my_value)
      if self === my_value
        return my_value
      end
      new(my_value)
    end
  end

  private_class_method :new

  def initialize(my_value)
    @my_value = my_value
    freeze
  end

  def <=>(other)
    [self.class, my_value] <=> [other.class, other.my_value]
  end

  def ==(other)
    self.class == other.class && my_value == other.my_value
  end

  def eql?(other)
    self == other
  end

  def hash
    my_value.hash
  end

  def to_s
    my_value.to_s
  end

  def inspect
    "<#{self}>"
  end
end

Foo["a"] == Foo["a"]                          # => true
[Foo["a"], Foo["b"], Foo["c"]] - [Foo["b"]]   # => [<a>, <c>]
[Foo["b"], Foo["c"], Foo["a"]].sort           # => [<a>, <b>, <c>]

Marker Interface

  • 種類で判別するためにモジュールを入れる
  • これは Java ならではな感じはする
  • ダックタイピングな言語ではやらない
  • とはいえ以前ベクトルの定義で「普通のベクトル」と「繰り返すことができるベクトル」をクラスで判別したいがために分けて定義したときがあって次のように書いた。これはある種 Marker Class と言えるのかもしれない
require "matrix"

class RepeatableVector < Vector; end
class SingleVector < Vector; end

RepeatableVector[1, 1]          # => Vector[1, 1]
SingleVector[1, 1]              # => Vector[1, 1]

Generation Gap

a.rb
class A
end
b.rb
class B < A
end
  • a.rb はコードジェネレーターによって生成される
  • コードジェネレーターは再度実行する場合もある (なんで?)
  • a.rb を利用者がいじると再度実行するときに上書きしてしまう (それはそう)
  • だから利用者は a.rb を継承した b.rb をいじってください
  • それなら何度でも a.rb を再生成できるじゃん!
  • というダメそうな仕組みのこと
  • このダメそうな仕組みにちゃんと名前がついているのが良い

Hook Operation

class A
  def foo
    p 1
    after_foo
  end

  def after_foo
  end
end

class B < A
  def after_foo
    p 2
  end
end

B.new.foo
# >> 1
# >> 2
  • Template Method にしないといけないわけじゃない
  • 方法はなんでもいい
  • 継承するなら super の前後で呼んでもいいけど、それは意図して用意した仕掛けではないので Hook Operation とは言いづらいかもしれない

Collecting Parameter

これは違う?
def f(output)
  output << "foo"
end
  • 上のサンプルの特徴
    • インスタンス生成が少なくなるぶん速い
    • output は << を持っているだけでいいので切り替えが可能
  • 「テスト駆動開発」で紹介されているのはそういう意図とは異なるように見える
    • 格納というより格納先のオブジェクトに強く依存している
    • 整形しているだけ? あとで確認 TODO
  • 大胆に専用のクラスでラップしておけばあとあと管理が楽になる

Convention over Configuration (CoC)

  • シンプルさを無視した柔軟性は苦しみを生む
    • webpack や webpack あと webpack など
  • いちばん使われるケースをデフォルトにする
    • これはメソッドオプションなどにも言えること
  • テンプレートを提供する
  • ファイル名やディレクトリ構成をコードと結び付ける
    • foo.rb には Foo クラスを書きましょう、というのも当てはまる?
    • 個人的にディレクトリ構成がそのままURLになるフレームワークが好み
      • Rails はそうなってないけど
    • Pluggable Selector のような選択をファイル名に適用してライブラリを読み込む
      • :foo なら自動的に foo_adapter.rb が読み込まれるような感じ

Transaction Script

  • 特徴
    • 冗長で長い
    • 書く場所が間違っている
      • Controller や Helper が肥大化
      • 一方 ActiveRecord のモデルがスカスカ
    • Rake タスクのなかでもよく見つかる
    • いま動けば後のことはどうでもいいというスタンスが伝わってくる
  • メリット
    • ライブラリの使い方のサンプルだとわかりやすい
    • 負債を与えたいときに効果がある

First Class Collection

class Group
  include Enumerable

  def initialize(items)
    @items = items
  end

  def each(...)
    @items.each(...)
  end
end

Group.new([5, 6, 7]).collect(&:itself) # => [5, 6, 7]
  • 特徴
    • 配列をラップする
    • Value Object と考え方は同じ
    • Immutable にすべきとは決まっているわけじゃないけど Immutable にした方がいい
  • 特定の配列に対する処理があまりにも複雑で多く散乱してしまう場合に使う
  • Ruby なら each を定義しとこう
  • 面倒なので class Group < Array; end とすることが多い

参照

Discussion

ログインするとコメントできます