Rubyで書くデザインパターン
概略
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除算になったりと余計面倒になる場合もある
- 例えば計算するとき「無し」を 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 を再生成できるじゃん!
- というダメそうな仕組みのこと
- このダメそうな仕組みにちゃんと名前がついているのが良い
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