Rubyで書くデザインパターン
概要
名前 | 内容 |
---|---|
Template Method | initialize に書けば最初に実行されると決まっているそういうやつ |
Abstract Class | Ruby だとあまり利点がない Template Method 風なやつ |
Factory Method | new を別オブジェクトが行うだけ(?) |
Singleton | インスタンスをグローバル変数にしたいとき用 |
Monostate | new できるけど状態は共有 |
Single-Active-Instance Singleton | 複数あるインスタンスのなかで一つだけが有効になる |
Composed Method | 巨大なメソッドを分割する。リファクタリングの第一歩 |
Adapter | ダメなインターフェイスをいろんな手段で隠す |
Value Object | 値をクラス化する。Immutable にする |
Flyweight | 効率化を目的とする。Immutable が多い |
Sharable | データの一貫性を目的とする。Mutable |
Null Object | データ不在を存在するかのように扱う。条件分岐したら負け |
Composite | 集合を単体のように扱う |
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 でよくある Event::Mouse::Click みたいなあれ |
Cache Manager | 使ったキャッシュは先頭に移動する |
Marker Interface | 印付けとしてのインターフェイスというかモジュール? |
Generation Gap | ソースコードジェネレーターは親クラスだけ再生成する |
Hook Operation | 実行処理の前後に何かを実行できるようにしておく |
Collecting Parameter | 結果を集める |
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
class X; end
class Y; end
class Z; end
group = { a: X, b: Y }
group[:a].new
group[:b].new
- 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
p "とてつもなく時間がかかる初期化処理..."
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
例1. ありがちなやつ
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
関心事「権限」で分離したいときに使う。
例2. 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 = {
first: "Alice",
last: "Smith",
name: -> c { [c[:first], c[:last]] * " " },
}
A[:name][A] # => "Alice Smith"
継承する。
B = A.clone
B[:first] = "Bob"
B[:name][B] # => "Bob Smith"
クラスのある言語でもオブジェクト生成時のコストが高い場合には有用なのかもしれない。
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| e }
- 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, 45, 28
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 # => 20, 20, 19
end
memento には復元に必要なものだけ入れておく。
Visitor
Pathname.glob("**/*.rb") { |e| e }
汎用性のある渡り歩く処理と、汎用性のない利用者側の処理を分ける。
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.20421287695439794
Sound.fetch(:battle) # => 0.20421287695439794
- メモ化で効率化すること
- 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
例1
改善前
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>"
例2
改善前
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"]
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:0x000000010350e750>
改善後
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:0x000000010350e020 @a=#<A1:0x000000010350e098>>
これなら組み合わせ爆発しない。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 Foo
def update(object)
end
end
class Bar
def update(object)
end
end
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
player.notify
標準ライブラリを使うとより簡潔になる。
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:0x00000001079e9960 @observer_peers={#<Player:0x00000001079e9960 ...>=>:update}, @observer_state=true>
end
end
player = Player.new
player.notify
これを「ぼっちObserverパターン」と勝手に呼んでいる。
Component Bus
Observer たちがデータ共有したいので Subject を共有することにしたパターン。
require "observer"
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:0x0000000105b8def8>
C.instance # => #<C:0x0000000105b8def8>
標準ライブラリを使った場合
require "singleton"
class C
include Singleton
end
C.instance # => #<C:0x0000000105b8def8>
C.instance # => #<C:0x0000000105b8def8>
どちらにしても 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 # => [6, 4, 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
# 前処理
ensure
# 後処理
end
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:0x0000000105c1e390 @active=true>
b = i.new_x # => #<X:0x0000000105c1dd78 @active=true>
c = i.new_x # => nil
a.active = false
c = i.new_x # => #<X:0x0000000105c1e390 @active=true>
Null Object
class NullLogger
def debug(...)
end
end
logger = NullLogger.new
logger.debug("x") # => nil
- インターフェイスが同じ「何もしない」オブジェクト
-
/dev/null
にリダイレクトするのに似ている
-
- 「無」を上手に表すとnull安全にできる
- たとえば、計算するとき「無し」を 0 と表現する
- こうすることで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
などとすると怒られない
- initialize の最後で freeze するのがわかりやすい
- デザインパターンのなかでいちばん効果がある
- ただの Integer や String な変数であっても、それをあちこちで引数に取る処理があったとすれば、方向を逆にできないか考える
- とりあえず動いたからOKの考えでは、逆にする発想が思い浮かばなくなっていく
- なので引数がくる度に頭の中で逆にしてみる癖をつける
- 例外として何を犠牲にしてでも処理速度を優先させたいところでは使わない方がいい
- Value Object に似ているけど Mutable で、識別子が同じであれば同一視するようなものは Entity というらしい
- たとえば、 ActiveRecord のインスタンス
- 四則演算子の他に
<=>
==
eql?
hash
やto_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
# 触るな
class A
end
# いじってよし
class B < A
end
- a.rb はコードジェネレーターによって生成される
- コードジェネレーターは再度実行する場合もある (なんで?)
- a.rb を利用者がいじると再度実行するときに上書きしてしまう (それはそう)
- だから利用者は a.rb を継承した b.rb をいじってください
- それなら何度でも a.rb を再生成できるじゃん!
このようなダメそうな仕組みのことをいう。
初期の Rails では似たような問題を抱えていて試行錯誤の過程に利用者は振り回された。
Hook Operation
class A
def call
# 何かする
after_call
end
def after_call
end
end
class B < A
def after_call
# call の処理の後で何かする
end
end
B.new.call
- Template Method にしないといけないわけじゃない
- 方法はなんでもいい
- 継承するなら super の前後で呼んでもいいけど、それは意図して用意した仕掛けではないので Hook Operation とは言いづらいかもしれない
Collecting Parameter
def f(output)
output << "foo"
end
- 上の例の特徴
- インスタンス生成が少なくなるぶん速い
- output は
<<
を持っているだけでいいので切り替えが可能
- 「テスト駆動開発」で紹介されているのはそういう意図とは異なるように見える
- 格納というより格納先のオブジェクトに強く依存している
- 整形しているだけ?
- 大胆に専用のクラスでラップしておけばあとあと管理が楽になる
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