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
- 順番に並んだ数値のコレクションを扱うには?