Smalltalkベストプラクティス・パターン

ケント・ベックのあれをRubyでメモしたメモ。

このメモについて

ケント・ベックのSmalltalkベストプラクティス・パターン―シンプル・デザインへの宝石集

ケント・ベックのSmalltalkベストプラクティス・パターンを読む時になんとなく メモしたメモなので、内容の要約であるわけではありません。 元の本はこのメモの1024倍以上の内容だよう。

「はじめに」の「この本で扱わない事柄」がなぜか大好き。

振る舞い

メソッド

Composed Method

メソッドを一つのことのみをするメソッドに分割しよう

class Controller
  def control_activity
    control_initialize
    control_loop
    control_terminate
  end
end

Constructor Method

  • インスタンスの生成を表現するには?

適切な値のインスタンスを生成できるようなメソッドを用意しよう。 必要なパラメータをそのメソッドに与えよう

class Point
  def initialize(x, y)
    @x = x
    @y = y
  end

  def self.polar(r, theta)
    self.new(r * Math.cos(theta), r * Math.sin(theta))
  end
end

Constructor Parameter Method

  • Constructor Methodのパラメータをインスタンス変数にセットするには?
# before
class Point
  def initialize(x_number, y_number)
    x = x_number
    y = y_number
  end
  attr_accessor :x, :y
end

インスタンス変数を一度に設定するメソッドを作ろう

# after
class Point
  def initialize(x_number, y_number)
    set_x_y(x_number, y_number)
  end

  def set_x_y(x_number, y_number)
    @x = x_number
    @y = y_number
  end
end

Shortcut Constructor Method

Constructor Methodの呼び出しが面倒な時、さらにインターフェイスを 追加するには?

Conversion

  • あるオブジェクトの形式から別のオブジェクトの形式へ変換するには?

あるオブジェクトのプロトコルを次々と増やすより、 別のオブジェクトに変換しよう

Converter Method

  • 内部の構造は異なるものの、同じプロトコルを持つオブジェクトに変換 するには
class Collection
  def to_set
  end
end

class Number
  def to_float
  end
end

Converter Constructor Method

  • 異なるプロトコルのオブジェクトへ変換するには?

変換元のオブジェクト引数として受け取るConstructor Methodを作ろう

Query Method

  • オブジェクトのプロパティを調べるには?
# before
class Switch
  def make_on
    @status = :on
  end
  def make_off
    @status = :off
  end
  attr_reader :status
end

class WallPlate
  def update
    light.make_on if @switch.status == :on
    light.make_off if @switch.status == :off
  end
end

真偽値を返すメソッドを用意しよう

# after
class Switch
  def on?
    # レシーバがonならtrue、offならfalseを返す
  end
end

Comparing Method

  • オブジェクト同士を並び替えるには?
class Event
  def <=>(other)
    time_stamp <=> other.time_stamp
  end
end

Reversing Method

  • メッセージの流れをスムーズに書くには?
# before
class Point
  def print_on(stream)
    @x.print_on(stream)
    stream.puts(' @ ')
    @y.print_on(stream)
  end
end

引数に対してメソッドを書こう

# after
class Stream
  def puts(obj)
    obj.print_on(self)
  end
end
class Point
  def print_on(stream)
    stream.puts(@x)
    stream.puts(" @ ")
    stream.puts(@y)
  end
end

FIXME。Rubyだとどうかな?

Method Object

  • たくさんの引数や一時変数であふれているメソッドを整理するには?

元のメソッドにちなんだ名前を持つクラスを定義しよう 元のメソッドのレシーバ、引数、一時変数を保持するインスタンス変数を 用意して、元のレシーバと引数を受け取るConstructor Method

# before
class Obligation
  def send_task(task, job)
    # 長いコード
  end
  def prepare_task(task, job, ...)
  end
end

# after
class TaskSender
  def initialize(obligation, task, job)
    @obligation = obligation
    @task = task
    @job = job
  end
  def compute
    # 長いコード
  end
end

class Obligation
  def send_task(task, job)
    TaskSender.new(self, task, job).compute
  end
end

Execute Around Method

  • 続けて実行しなければならないアクションのペアを表現するには?

ブロックを受け取るメソッドを書こう。

class Cursor
  def show_while
    old = current_cursor
    show
    yield
    old show
  end
end

File.openなど

class PgUtil
  def transaction
    aborted = false
    begin_transaction
    yield
  rescue
    aborted = true
    abort
  ensure
    end_transaction unless aborted
  end
end

Debug Printing Method

  • デバッグ用のプリントメソッドを書くには?

inspectをオーバーライドしよう

Method Comment

  • メソッドのコメントを書くには?

メソッドの先頭にコメントを書くことで、コードから読み取れない 重要な情報を伝えるようにしよう

メッセージ

Message

  • 処理を起動するには?

よい名前のついたメッセージを送り、何を行うかはメッセージを 受け取るメッセージに任せよう

Choosing Message

  • さまざまな選択肢のうちのひとつを実行させるには?
# before
def responsible
  if Film === entry
    entry.producer
  else
    entry.author
  end
end

# after
class Film
  def responsible
    return producer
  end
end
class Entry
  def responsible
    return author
  end
end

def responsible
  entry.responsible
end

いくつかのオブジェクトを用意して、そのうちの一つにメッセージを送り 各オブジェクトが一つの選択肢を実行するようにしよう

Decomposing Message

  • 分割した処理の一部を呼び出すには?

selfに対して複数のメッセージを送ろう

class Controller
  def controy_activity
    control_initialize
    control_loop
    control_terminate
  end
end

Intention Revealing Message

  • 単純な実装の意図を伝えるには?
class ParagraphEditor
  def highlight(rect)
    reverse(rect)
  end
end

selfにメッセージを送ろう。そのメッセージには処理の方法ではなく 何を行うか伝えられる名前にしよう。メッセージの実装メソッドは単純に しよう

class Collection
  def empty?
    size == 0
  end
end

class Number
  def reciprocal
    1 / self
  end
end

Intention Revealing Selector

  • メソッドにどんな名前をつける?

HowではなくWhat

メソッドには何を達成するかにちなんだ名前をつけよう

# before
class Array
  def linear_search_for(x)
    ...
  end
end

# after
class Array
  def include?(x)
    ...
  end
end

Dispatched Interpretation

  • 二つのオブジェクトがあり、片方のオブジェクトの表現形式を 隠ぺいしたい時、どうやって協調させる?
# before
class PostScriptPrinter
  def display(shape)
    shape.size.times do |n|
      command = shape.command_at(n)
      arg = shape.argument_at(n)
      case command
      when :line
        print_point(arg[0])
        space
        print_point(arg[1])
        space
        ....
      when ...
        ...
      end
    end
  end
end

クライアントからはエンコードオブジェクトにメッセージを送ろう。 エンコードオブジェクトにパラメータを渡し、デコード用のメッセージを 更に送らせるようにしよう。

# after-1
class PostScriptPrinter
  def line(from_point, to_point)
    print_point(from_point)
    space
    pint_point(to_point)
    space
    puts('line')
  end
end

class PostScriptPrinter
  def display(shape)
    shape.size.times do |n|
      shape.send_command_at(n, self)
    end
  end
end

Shape自身に繰り返しを行う責務を与えると…

# after-2
class Shape
  def send_command_to(printer)
    size.times do |n|
      send_command_at(n, printer)
    end
  end
end

class PostScriptPrinter
  def display(shape)
    shape.send_command_to(n, self)
  end
end

Double Dispatch

  • ふたつの継承木のオブジェクトが相互作用し、多くの場合分けが 起こる処理をコーディングするには?

引数にメッセージを送ろう。 セレクタはレシーバのクラス名を後ろに連結したものに。 そのメッセージを元のレシーバを引数に渡そう。

class Int
  def +(num)
    num.add_int(self)
  end
end
class Float
  def +(num)
    num.add_float(self)
  end
end

class Int
  def add_int(num)
    self + num # primitive
  end
end
class Float
  def add_float(num)
    self + num # primitive
  end
end

Mediating Protocol

  • お互いに独立させておきたいオブジェクトの相互作用をコーディングするには?

オブジェクト間のプロトコルで使われている言葉が一貫したものに なるように推敲しよう

Super

  • スーパークラスの振る舞いを呼び出すには?

superだよーん。

Extending Super

  • あるメソッドのスーパークラスの実装に機能を追加するには?
class Super
  def initialize
    @a = default_a
  end
end

class Sub < Super
  def initialize
    super
    @b = default_b
  end
end

メソッドをオーバーライドし、そのメソッドの中でsuperを送ろう

Modifying Super

  • スーパークラスのメソッドを変更せずに振る舞いの一部を変えるには?
# before
class IntegerAdder
  def initialize
    @sum = 0
    @count = 0
  end
end
class FloatAdder
  def initialize
    super
    @sum = 0.0
  end
end

# after-1
class IntegerAdder
  def initialize
    @sum = default_sum
    @count = default_count
  end
  def default_sum
    0
  end
  def default_count
    0
  end
end
class FloatAdder < IntegerAdder
  def default_sum
    0.0
  end
end

スーパークラスを修正できないときは…

メソッドをオーバーライドして"super"を送り、その後で結果を修正する コードを実行しよう

class SuperFigure
  def initialize
    @color = Color.white
    @size = Point.new(0, 0)
  end
end
class SubFigure < SuperFigure
  def initialize
    super
    @color = Color.beige
  end
end

Delegation

  • 継承なしに実装を共有するには?

仕事の一部を他のオブジェクトにまかせよう

Simple Delegation

  • 呼び出し元に依存しない委譲を行うには?

メッセージをそのまま委譲しよう

class Vector
  def initialize(num)
    set_elements(Array.new(num))
  end
  def set_elements(ary)
    @elements = ary
  end
  def each(&block)
    @elements.each(&block)
  end
end

Self Delegation

  • 呼び出し元への参照が必要な委譲を実装するには?

呼び出し元のオブジェクト(self)をfor:というキーワードを 追加してパラメータで渡そう

うーん。うまくサンプルが作れない… FIXME

Pluggable Behavior

  • オブジェクトの振る舞いをパラメータ化するには?

変数を追加してそこから異なる振る舞いを実行できるように。

Pluggable Selector

  • インスタンス固有の振る舞いを簡単にコーディングするには?
# before
class ListPane
  def print_element(obj)
    obj.to_s
  end
end

class DollerListPane
  def print_element(obj)
    obj.doller_format_string
  end
end

class DescriptionListPane
  def print_element(obj)
    obj.inspect
  end
end

そこだけオーバーライドしたクラスがたくさんになっちゃった。

# after
class ListPane
  def print_element(obj)
    obj.send(@print_message)
  end
end

class ListPane
  def initialize
    @print_message = :to_s
  end
end

実行されるセレクタを保持する変数を追加しよう Role Sugessting Instance Variable Nameに「Message」をつけて 変数名にしよう。 そのセレクタを実行するためのComposed Methodを作ろう

class RelativePoint
  def self.centered(fig)
    self.new(fig, :center)
  end

  def initialize(fig, symbol)
    @figure = fig
    @message = symbol
  end

  def to_point
    @figure.send(@message)
  end

  def x
    to_point.x
  end
end

Pluggable Block

  • そのクラス自身ではまったく価値のない複雑なPluggable Behaviorを コーディングするには?

Blockを保持するインスタンス変数を追加しよう

class PluggableAdaptor
  def initialize(set_blk, get_blk)
    @set_block = set_blk
    @get_block = get_blk
  end

  def value
    @get_block.call
  end

  def value=(obj)
    @set_block.call(obj)
  end
end

class Car
  def speed_adaptor
    get_blk = Proc.new { self.speed }
    set_blk = Proc.new { |new_speed| self.speed=(new_speed) }
    PluggableAdaptor.new(get_blk, set_blk)
  end
end

Collecting Parameter

  • いくつものメソッドの協調作業の結果としてコレクションを返すには?

すべてのサブメソッドの結果を集めるためのパラメータを追加しよう。

# before
def married_men_and_unmarried_women
  result = Array.new
  prople.each do |person|
    result.push(person) if person.married? && person.man?
    result.push(person) if person.unmarried? && person.woman?
  end
  result
end

Composed Methodを使って繰り返しを別々のメソッドにしよう。

# after
def married_men
  result = Array.new
  prople.each do |person|
    result.push(person) if person.unmarried? && person.woman?
  end
  result
end

def unmarried_women
  result = Array.new
  prople.each do |person|
    result.push(person) if person.married? && person.man?
  end
  result
end

def married_men_and_unmarried_women
  married_men + unmarried_women
end

Collectionを返す代わりに各々がオブジェクトをCollectionに追加しよう。

# after-2
def add_married_men_to(ary)
  prople.each do |person|
    ary.push(person) if person.unmarried? && person.woman?
  end
end

def add_unmarried_women_to(ary)
  prople.each do |person|
    ary.push(person) if person.married? && person.man?
  end
end

def married_men_and_unmarried_women
  result = Array.new
  add_married_men_to(result)
  add_unmarried_women_to(result)
  result
end

状態

インスタンス変数

Variable State

  • インスタンスごとに表現の仕方が異なるような状態を表わすには?

いくつかのインスタンスでしか使わない変数はHashに入れちゃえ。

でもRubyでは実行時にインスタンス変数を作れるから、 そういう手もあります。

Explicit Initialization

  • インスタンス変数をデフォルト値で初期化するには?

変数の値を初期化するメソッドを実装しよう。 Rubyではinitializeがnewの最後(?)に呼ばれます。

class Timer
  def initialize
    @count = 0
    @period = 1000
  end
end

さらにマジックナンバーを説明するようにすると…

class Timer
  def default_msec_period
    return 1000
  end

  def initialize
    @count = 0
    @period = default_msec_priod
  end
end

Lazy Initialization

  • インスタンス変数をデフォルト値で初期化するには?

アクセサ(attr_reader相当)を書いて、必要に応じてDefaultMethodを 使って初期化しよう。

class Timer
  def count
    @count = default_count if @count.nil?
    @count
  end

  def default_count
    return 0
  end

  def period
    @period = default_msec_period if @period.nil?
    @count
  end

  def default_msec_period
    return 1000
  end
end

Default Value Method

  • 変数のデフォルト値をどのように表現する?

値を返すメソッドを作ろう。メソッドの名前には変数名の前にdefaultを つけたものに。

class Book
  def default_synopsis
    return ""
  end
end

Constant Method

  • 定数をどのようにコーディングする?

うーん。Rubyの良い例はないかなあ。

定数を返すためのメソッドを書こう。

Direct Variable Access

  • インスタンス変数の値を取り出したり設定するには?

アクセサ経由で…

class Foo
  attr_accessor :x
  ..
  def foo
    ... x ...
  end
end

直接アクセスしよう

def foo
  .. @x ..
end

読みやすい

Indirect Variable Access

  • インスタンス変数の値を取り出したり設定するには?

アクセサ経由でアクセスしよう

拡張しやすい

Getting Method

  • インスタンス変数にどうやってアクセスする?

変数の値を返すメソッドを書こう。名前は変数と同じにしよう。

Rubyではattr_readerで簡単にかけるよ。

class Foo
  attr_reader :foo
end

Setting Method

  • インスタンス変数にどうやって変更する?

変数の値を変更するメソッドを書こう。名前は変数名=にしよう。

Rubyではattr_writerで簡単にかけるよ。

class Foo
  attr_writer :foo
end

Collection Accessor Method

  • コレクションを持つインスタンス変数にアクセスするには?

コレクションを直接見せちゃうとちょっと困るよ。

コレクションにdelegateするメソッドを作ろう。 名前は変数名+メッセージにしよう

class Department
  def total_salary
    return @total_salary if @total_salary
    compute_total_salary
  end

  def compute total_salary
    sum = 0
    @employees.each do |x|
      sum += x.salary
    end
    sum
  end

  def add_employee(employee)
    clear_total_salary
    @employees.push(employee)
  end

  def remove_employee(employee)
    clear_total_salary
    @emplyoees.remove(employee)
  end
end

機械的に名前を付けないで、もっと意味の名前を付けよう

#before
def include_employee(employee)
  @employees.include?(employee)
end

#after
def employs(employee)
  @employees.include?(employee)
end

Enumeration Method

  • コレクションの要素に、安全、汎用的にアクセスするには?

コレクションの各要素にblockを実行するメソッドを書こう。

class Department
  def employees_each(&block)
    @employees.each(&block)
  end
end

Boolean Property Setting Method

  • 真偽値の属性を設定するには?
# before
class Switch
  def on=(bool)
    @on = bool
  end
end

# after
class Switch
  def on
    @on = true
  end
  def off
    @on = false
  end
end

beではじまるメソッドを書こう

  • be_visible, be_invisible, toggle_visible
  • be_drity, be_clean

Role Suggesting Instance Variable Name

  • インスタンス変数の名前はどうする?

役割を示す名前にしよう。コレクションのときは複数形にしよう。

一時変数

Temporary Variable

  • メソッド内で後で使用する値を保存するには?

スコープ、エクステントを1メソッドに絞った一時変数を作ろう。

Collecting Temporary Variable

  • 後で使う値を段階的に集めたいときは?
def foo
  sum = 0
  @children.each do |child|
    sum += child.size
  end
  sum
end

inject (ruby-1.8) を使うとsumが不要

def foo
  @children.inject(0) do |sum, child|
    sum + child.size
  end
end

複雑なenumerationでマージや収集を行う場合に、中間結果の コレクションを作ろう

Caching Temporary Variable

Explaining Temporary Variable

Reusing Temporary Variable

Role Suggestin Temporary Variable

  • 一時変数にはどんな名前をつける?

役割を表わす名前を付けよう

コレクション

クラス

Collection

  • 1対多の関係を表現するには?

コレクションクラスを使いましょう

Array

  • 作成時にサイズが決められないようなコレクションを使うには?

とりあえずArrayを使おう

RunArray

なし

Set

  • 要素が重複しないコレクションを扱いたいときには?

SetがないのでHashを使う?

class Set
  def push(obj)
    @hash[obj] = true
  end

  def each(&block)
    @hash.keys.each(&block)
  end
end

Equality Method

  • 新たに作ったオブジェクトの同値性を保証するには?

同値を判定するには==を定義しよう

class Book
  def ==(other)
    return (@author == other.author) && (@title == other.title)
  end
end

コレクションから探せるようになる

class Library
  def has_book(author_str, title_str)
    book = Book.new(author_str, title_str)
    @books.include?(book)
  end
end

さらにクラス判定を含めるのが一般的

class Book
  def ==(other)
    return false unless self.class === other
    return (@author == other.author) && (@title == other.title)
  end
end

Hashing Method

  • Hashに対応させるには?

「=」をオーバーライドしたオブジェクトをHashに入れるときは、 hashもオーバーライドしよう。

class Book
  def hash
    @author.hash ^ @title.hash
  end
end

Hash

  • オブジェクトを他のオブジェクトに対応付けるには?

Hashを使おう

Sorted Collection

  • コレクションをソートするには?

sortを使おう。「<=>」メソッドを定義しよう

Range

  • 順番に並んだ数値のコレクションを扱うには?

時間切れ…