「リファクタリング:Rubyエディション」の内容を要約し、普段使いしやすくすることで、自分や他人のコードをより綺麗にしていくことを目的とする。全75パターン存在する。
とはいえ、この要約だけで「リファクタリング:Rubyエディション」の真髄を余すところ説明できているわけではない。「リファクタリング:Rubyエディション」の真の威力を知りたい・発揮したい方は「リファクタリング:Rubyエディション」を今すぐ購入されたし。
6章 メソッドの構成方法
6.1 メソッドの抽出
コードの一部をメソッドにして、その目的を説明する名前をつける。
メリット
- 詳細を見なくても、コードが何をしているかわかる
- 短い関数は読みやすい
- コメントを減らす
理由: メソッドの粒度が細ければ、流用性が高くなる。
注意: 意味のある良い名前が思いつかないなら、それは抽出すべきでない。
(感想: とても重要。<6.2 メソッドのインライン化>にもあるように。メソッドの粒度は粗すぎず細かすぎないように気をつける。)
6.1.1 メソッドの抽出::ローカル変数なし
def sample_method
puts "sample"
# その他諸々の処理...
end
def sample_method
print_sample
end
def print_sample
puts "sample"
# その他諸々の処理...
end
6.1.2 メソッドの抽出::ローカル変数あり
def sample_method
local_variable = 0
puts "instance_variable: #{@instance_variable}"
puts "local_variable: #{local_variable}"
# その他諸々の処理で、local_variableは使われる...
end
def sample_method
local_variable = 0
print_sample_method(local_variable)
# その他諸々の処理で、local_variableは使われる...
end
# ローカル変数は別メソッド間でアクセスできないため、引数で渡す必要がある。
def print_sample_method(local_variable)
puts "instance_variable: #{@instance_variable}"
puts "local_variable: #{local_variable}"
end
6.1.3 メソッドの抽出::ローカル変数への再代入
def sample_method
local_variable = 0
@sample.each do |sample|
local_variable += sample.num
end
# その他諸々の処理で、local_variableは使われる...
end
def sample_method
# ローカル変数に再代入することで、以降の処理に影響を与えない
local_variable = calculate_sum
# その他諸々の処理で、local_variableは使われる...
end
def calculate_sum
# local_variable = 0
# @sample.each do |sample|
# local_variable += sample.num
# end
# local_variable
# 上記はinjectでスマートに書ける
@sample.inject(0) { |local_variable, sample| local_variable + sample.num }
end
6.2 メソッドのインライン化
メソッドを呼び出し元に組み込み、そのメソッドを削除。
条件:
メソッドの本体が、そのメソッドの名前と同じぐらいわかりやすい場合。
メソッドグループが悪く構成されていて、それらをグループ化するとより明確になる場合。
理由:
過剰な間接化はイライラの原因。
不必要な間接参照(単純な委譲)は面倒になる。
def sample_method
# @sampleが5以上の時にtrueを判別するis_more_than_fiveメソッドを作っているが、
# 間接化が過剰なためクールではない。
is_more_than_five ? 2 : 1
end
def is_more_than_five
@sample > 5
end
def sample_method
@sample > 5 ? 2 : 1
end
6.3 一時変数のインライン化
一時変数を削除。
条件: 単純 かつ 1度しか代入されていない場合。
一時変数が式を1回割り当てられ、他のリファクタリングを妨げている場合。
理由: リファクタリングを妨げる。
def price
base_price = @quantity * @item_price
if base_price > 1000
base_price * 0.95
else
base_price * 0.98
end
end
def price
if @quantity * @item_price > 1000
@quantity * @item_price * 0.95
else
@quantity * @item_price * 0.98
end
end
6.4 一時変数から問合せメソッドへ
一時変数(式)をメソッドにする。
メソッドは最初は非公開にしておき、後で他の用途が見つかったら緩めればいい。
条件: なし。
理由: 一時変数の問題点は、一時的でローカルであること。一時変数にアクセスするためにはメソッドを長くする以外になく、メソッドの長大化を助長するため。
この変更で不安なのは下記2点。
同じメソッドを2回以上呼び出しても、パフォーマンスは大丈夫か。
リファクタリングは、わかりやすくすることに集中すべき。(パフォーマンスを上げるのは別の仕事)
メソッド呼び出し回数が増えても、ほとんどの場合問題ない。
複数回呼び出しているメソッドが、冪等かどうか。
一時変数は無いほうがいい。なぜそこに一時変数があるのか分からなくなってしまうため。
コードが何をしようとしているかが、はっきりと分かるようになる。
(感想: とても重要。特に「メソッドを複数回呼び出しまって良いのか」という点。この点は、個人的にはこれまで抵抗があったが、この本を読んで考え方が理解できた。)
def price
# base_priceを求めるロジック
base_price = @quantity * @item_price
# discount_factorを求めるロジック
if base_price > 1000
discount_factor = 0.95
else
discount_factor = 0.98
end
# 本質
base_price * discount_factor
end
def price
base_price * discount_factor
end
def base_price
@quantity * @item_price
end
def discount_factor
base_price > 1000 ? 0.95 : 0.98
end
# base_priceを一時変数から問合せメソッドにしなかった場合
def price
# base_priceが一時変数のままの場合
base_price = @quantity * @item_price
discount_factor(base_price)
base_price * discount_factor
end
# base_priceはローカル変数なのでメソッドからアクセスできず、外部から渡さないといけない(引数など)
def discount_factor(base_price)
base_price > 1000 ? 0.95 : 0.98
end
6.5 一時変数からチェインへ
チェイニング(=メソッドチェーンで繋いで)して、一時変数を削除。
条件: 一時変数に対して複数行に渡ってメソッドを複数回実行している状況。
mock = Mock.new()
expectation = mock.receive(:method_name)
expectation.with("args")
expectation.and_return("result")
mock = Mock.new()
mock.receive(:method_name).with("args").and_return("result")
6.6 説明用変数の導入
処理の目的を説明する名前の一時変数に、式orその一部 の結果を保管する。
条件: 式が複雑な場合。式が読みにくい場合
理由: 可読性向上のため。
6.6 ~ 6.8項で一時変数を導入するが、軽々しく一時変数を導入してはいけない。(理由: 6.4項に記載)
<6.6 説明用変数の導入>の前に、<6.1 メソッドの抽出>ができないか考えること。
if platform.upcase.index("MAC") && browser.upcase.index("IE") && was_initialized && resize > 0
# 何らかの処理
end
is_mac_os = platform.upcase.index("MAC")
is_ie_browser = browser.upcase.index("IE")
was_resized = resize > 0
if is_mac_os && is_ie_browser && was_initialized && was_resized
# 何らかの処理
end
6.7 一時変数の分割
代入ごとに別の一時変数を用意する。
条件: ループ変数でも計算結果蓄積用の変数でもないのに、複数回代入される一時変数がある場合。
一時変数が2回以上割り当てられ、ループ変数でも集計用一時変数でもない場合。
理由: 2つの異なる目的のために1つの一時変数を使い回すと混乱するため。
変数には1つの責任だけがあるべきである。
2つの異なる目的のために1つの一時変数を使用すると、読み手にとって非常に混乱する。
temp = 2 * (@height + @width)
puts temp
temp = @height * @width
puts temp
perimeter = 2 * (@height + @width)
puts perimeter
area = @height * @width
puts area
6.8 引数への代入の除去
引数への代入の代わりに一時変数を使う。
条件: 引数に代入を行っている場合。
理由: Rubyは(参照渡しでなく)値渡しなため、呼び出し元ルーチンには影響はないが、紛らわしく、読みにくいため。
また、引数の用途は渡されたものを表すことなので、それに代入して別の役割を持たせないようにするため。
コードがパラメータに代入している場合。
動機
渡されたオブジェクトの内部を変更することはできますが、他のオブジェクトを指すように変更しないように注意してください。
渡されたオブジェクトを代表するために、引数のみを使用してください。
def discount(input_val, quantity, year_to_date)
input_val -= 2 if input_val > 50
# ...
end
def discount(input_val, quantity, year_to_date)
result = input_val
result -= 2 if input_val > 50
# ...
end
6.9 メソッドからメソッドオブジェクトへ
メソッドを独自のオブジェクト(クラス)に変える。ローカル変数をそのオブジェクトのインスタンス変数にする。
条件: <メソッドの抽出>を適用できないようなローカル変数の使い方をしている場合。
理由: 独自のオブジェクト内で、メソッドの分解(<メソッドの抽出>)ができる。
(感想: 手間のかかるリファクタなので、巨大なメソッドに適用するのが費用対効果的に良いと思う。)
- メソッドオブジェクトの導入
長いメソッドで、ローカル変数を使用しているため、1. メソッドの抽出を適用できない場合。
class Order
def price
primary_base_price = 0
secondary_base_price = 0
tertiary_base_price = 0
# 長い計算処理
# ...
end
end
class Order
def price
PriceCalculator.new(self).compute
end
end
class PriceCalculator
def initialize(order)
@order = order
end
def compute
primary_base_price = 0
secondary_base_price = 0
tertiary_base_price = 0
# 長い計算処理
# ...
end
end
動機
メソッドに多数のローカル変数がある場合、分解することができない場合。
この例では、実際にはこのリファクタリングが必要ないかもしれませんが、方法を示すために記載しています。
class Account
def gamma(input_val, quantity, year_to_date)
important_value1 = (input_val * quantity) + delta
important_value2 = (input_val * year_to_date) + 100
if (year_to_date - important_value1) > 100
important_value2 -= 20
end
important_value3 = important_value2 * 7
# 以下略
important_value3 - 2 * important_value1
end
def delta
1
end
end
class Gamma
attr_reader :important_value1, :important_value2, :important_value3
def initialize(account, input_val, quantity, year_to_date)
@account = account
@input_val = input_val
@quantity = quantity
@year_to_date = year_to_date
end
def compute
@important_value1 = (@input_val * @quantity) + @account.delta
@important_value2 = (@input_val * @year_to_date) + 100
if (@year_to_date - @important_value1) > 100
@important_value2 -= 20
end
@important_value3 = @important_value2 * 7
# 以下略
@important_value3 - 2 * @important_value1
end
end
class Account
def gamma(input_val, quantity, year_to_date)
Gamma.new(self, input_val, quantity, year_to_date).compute
end
def delta
1
end
end
6.10 アルゴリズム変更
より簡単なアルゴリズムに変更。
(感想: 6.11項のコレクションクロージャメソッドを使うことで、アルゴリズムを簡単にすることがかなり実現できると思う。)
アルゴリズムをよりわかりやすいものに置き換えたい場合。
動機
複雑なものを単純なものに分解する。
アルゴリズムの変更を容易にする。
大規模な複雑なアルゴリズムの置換は非常に困難であり、簡単なアルゴリズムにすることで、置換を行いやすくすることができる。
def found_person(people)
people.each do |person|
return 'Don' if person == 'Don'
return 'John' if person == 'John'
return 'Kent' if person == 'Kent'
end
''
end
def found_person(people)
candidates = ['Don', 'John', 'Kent']
people.each do |person|
return person if candidates.include?(person)
end
''
end
6.11 ループからコレクションクロージャメソッドへ
ループでなく、コレクションクロージャメソッドを使う。
each系の処理をmap系の処理にすること。
繰り返し処理した結果を返却したい場合に、ループの外から変数として渡す必要がなくなる。
(感想: とても重要。eachを使いたいと思ったとき、必ずmap系で実現できないか確認する。)
select
条件に合致するものだけを抽出する。
managers = []
employees.each do |e|
managers << e if e.manager?
end
# ↓
managers = employees.select { |e| e.manager? } # {}内は真偽値
map
ブロックを実行した戻り値を格納する。
offices = []
employees.each { |e| offices << e.office }
offices = employees.map { |e| e.office } # {}内は実行するブロック
managerOffices = []
employees.each do |e|
managerOffices << e.office if e.manager?
end
# チェインで繋げられる
managerOffices = employees.select { |e| e.manager? }
.map { |e| e.office }
inject
合計を出すとき など、ループ内で値を生み出すような場合に使う。
返り値の初期値が設定できる。(例: 0からスタートする、10からスタートする)
inject(返り値の初期値) { | 返り値, 配列内のオブジェクト| ブロック }
total = 0
employees.each { |e| total += e.salary }
total = employees.inject(0) { |sum, e| sum + e.salary }
# (デフォルト引数は0 なので(0)は省略可)
6.12 サンドイッチメソッドの抽出
条件: ほぼ同じロジックだが、その中間ぐらいのロジックに差異がある複数のメソッドがあるとき。
重複部分を抽出してメソッドにする。
その複数のメソッドは、抽出したメソッドにブロックを渡すようにする。
(感想: よくあるシチュエーションだと感じる。よく使いそう。)
def sample_method_equal_one
if @count.present? && @count == 1
@count += 1
end
end
def sample_method_equal_arg(arg)
if @count.present? && @count == arg # 少しだけ違う
@count += 1
end
end
def sample_method_equal_one
sample_method_logic { |count| count == 1 } # {}内がブロック
end
def sample_method_equal_arg(arg)
sample_method_logic { |count| count == arg }
end
def sample_method_logic
if @count.present? && yield(@count) # ブロックを実行。
@count += 1
end
end
6.13 クラスアノテーションの導入
initializeの導入。
6.14 名前付き引数の導入
キーワード引数の導入。
6.15 名前付き引数の除去
キーワード引数の除去。引数が1つであるとき。
理由: 引数が1つだと、わざわざキーワードにする必要がない。メソッドを呼び出すたびにキーワード引数を書かないといけなく、費用対効果が小さいため。
6.16 使われていないデフォルト引数の除去
使われていないデフォルト引数を除去。
引数による条件分岐などが削除できる可能性がある。
(感想: 使われていないことをちゃんと確認しないといけない。)
6.17 動的メソッド定義
メソッドを動的に定義する。
本書にあったのはdef_each等を使う方法であったが、調べたところ古いなので、下記記事を参考にした。
https://tech.ga-tech.co.jp/entry/2019/10/ruby-metaprogramming
define_methodやsendを使うことで実現できる。
define_method
一言で言うなら、defでメソッド定義しなくてもメソッドを定義できる
https://fujiyasu.hatenablog.com/entry/2017/09/23/150205
メリット
define_method を使って、同じような処理を繰り返し書かずに済むようにすることで、コードの重複を減らすことができ、メンテナンス性を高めることができます。
デメリット
しかしながら、define_method を使いすぎると、可読性が低下し、デバッグが困難になることがあります。また、動的に生成されたメソッドは、メソッドテーブルに登録されるため、メモリを消費することにもなります。そのため、必要以上に使用しないように注意する必要があります。
また、define_method は、実行時にメソッドを生成するため、コンパイル時に発生するエラーを検出できないことがあります。そのため、可能な限り静的なコード生成を行う方法を検討することが重要です。
使われるタイミングとしては、以下のような場合によく使用されます。
- 同じような処理を繰り返し書く必要がある場合
- 動的にメソッドを生成する必要がある場合
- 汎用的な機能を提供するためにライブラリを作成する場合
また、上記のような場合でも、メソッドを定義する場合は、できるだけ静的な方法を検討することが重要です。define_method は、その場合に限定的に使用されるべきです。
define_methodとdef定義のメソッドはどちらの方が実行速度は高速ですか?
define_method と def 定義のメソッドは、それぞれ違った特性を持っていますが、一般的には def 定義のメソッドの方が実行速度が高速です。
これは、def 定義のメソッドは、コンパイル時に実行可能なメソッドに変換されるため、実行時に追加のオーバーヘッドが発生せず、高速に実行されるからです。
一方、define_method は、実行時に動的にメソッドを生成するため、実行時のオーバーヘッドが発生します。また、動的に生成されたメソッドは、メソッドテーブルに登録されるため、メモリを消費することにもなります。
ただし、実行速度よりも柔軟性や可読性を重視する場合には、define_method を使うこともあります。define_method を使うことで、動的にメソッドを生成できるため、柔軟性が高くなります。また、同じコードを繰り返し書かなくて済むため、可読性が向上することもあります。
総じて、実行速度を重視する場合には def 定義のメソッドを使い、柔軟性や可読性を重視する場合には define_method を使うことが適切です。
6.18 動的レセプタから動的メソッド定義へ
動的メソッド定義を行うことで、method_missingを使わずに同じような振る舞いを実現。
method_missing
メソッドが未定義の際にNoMethodErrorを発行するメソッド。ここにバグがあると発見しにくいため、自前でmethod_missingを実装するのは避けたほうがいい。
6.19 動的レセプ夕の分離
優先順位低
使用頻度低
6.20 evalを実行時からパース時へ
優先順位低
使用頻度低
7章 オブジェクト間でのメンバの移動
7.1 メソッドの移動
メソッドを最もよく使っている他のクラスに、同じ内容の新メソッドを作る。
古いメソッドは、そのメソッドに処理を委ねる or 削除する。
動機
カプセル化の改善、疎結合化
条件: メソッドが、自身のクラスよりも他クラスをよく利用している or 利用されている場合。(今はそうでなくても、そうなりつつあるとき。)
現在のコンテキストよりも他のコンテキストの要素を参照する機能を移動する
class Account
def overdraft_charge
end
end
class AccountType
end
から
class Account
end
class AccountType
def overdraft_charge
end
end
7.2 フィールドの移動
移すクラスに新しいフィールドのreader(必要ならwriterも)を作り、フィールドを使っているコードを書き換える。
条件: フィールドが、自身のクラスよりも他クラスをよく利用している or 利用されている場合。(今はそうでなくても、そうなりつつあるとき。)
動機
常に一緒に関数に渡されるデータのピースは、単一のレコードに入れる方が良い
あるレコードの変更が別のレコードのフィールドを変更する原因となる場合、それはフィールドが間違った場所にある兆候です
フィールドを一つのクラスから別のクラスに移動する
class Class1
- attr_accessor :a_field # hogehogeしたいので
end
class Class2
+ attr_accessor :a_field # hogehogeに変更
end
7.3 クラスの抽出
新しいクラスを作成。関連フィールド・メソッドを移す。
条件: 2つのクラスで行うべき仕事をしているクラスがある場合。
class Person
attr_accessor :office_area_code
attr_accessor :office_number
def telephone_number
'(' + @office_area_code + ')' + @office_number
end
end
person = Person.new
person.office_area_code = 123
class Person
# 新しいクラスへのリンクを作る
def initialize
@office_telephone = TelephoneNumber.new
end
# 少し違う手段として、下記のコメント部のようにゲッター・セッターを用意すれば、フィールドへのアクセス方法は変えないで済む。
# -----------------------------------------------
# 既存のフィールドは、新しいクラスのフィールドに向ける
# ゲッター
# def office_area_code
# @office_telephone.area_code
# end
# # セッター
# def office_area_code(arg)
# @office_telephone.area_code = arg
# end
# -----------------------------------------------
# 移動したフィールドへのアクセスは、office_telephone経由となる。
def office_telephone
@office_telephone
end
# メソッドは新しいクラスに移動
def telephone_number
@office_telephone.telephone_number
end
end
class TelephoneNumber
attr_accessor :area_code, :number
def telephone_number
'(' + area_code + ')' + number
end
end
person = Person.new
# 移動したフィールドへのアクセスは、office_telephone経由となる。
person.office_telephone.area_code = 123
7.4 クラスのインライン化
すべての機能を他のクラスに移して、クラスを削除する。(<7.3 クラスの抽出>の逆)
動機
リファクタリングを行った後、元のクラスから多くの責任が外れ、役に立たなくなった場合。
条件: 大した仕事をしていないクラスがある場合。
<7.3 クラスの抽出>の逆のことをすればいい。
クラスがあまり役に立っていないと感じられた場合、そのクラスに含まれる機能を、別のクラスに移動させ、元のクラスを削除します。
class Person
attr_reader :name, :telephone_number
def initialize(name, telephone_number)
@name = name
@telephone_number = telephone_number
end
end
class TelephoneNumber
attr_reader :area_code, :number
def initialize(area_code, number)
@area_code = area_code
@number = number
end
def telephone_number
"#{area_code}-#{number}"
end
end
上記のコードを以下のようにインライン化できます。
class Person
attr_reader :name, :office_area_code, :office_number
def initialize(name, office_area_code, office_number)
@name = name
@office_area_code = office_area_code
@office_number = office_number
end
def telephone_number
"#{office_area_code}-#{office_number}"
end
end
7.5 委譲の隠蔽
サーバに、委譲を隠すためのメソッドを作る。
条件: クライアント(呼び出し元)が、サーバ(呼ばれる側)クラスの委譲クラスを呼び出している。
サーバ(呼ばれる側) : 下記例では、Person。
委譲クラス : 下記例では、Department。
理由: カプセル化できるため。
カプセル化
オブジェクトがシステムの他の部分についてあまり知識を持たなくていい。
→ システムに変更を加えても、その変更を知らせないといけないオブジェクトが減るため、変更しやすい。
# 社員
class Person
attr_accessor :department
end
# 部門
class Department
attr_reader :manager
def initialize(manager)
@manager = manager
end
end
manager = person.department.manager
class Person
attr_writer :department # ゲッターは不要になった
def manager
@department.manager
end
end
class Department
attr_reader :manager
def initialize(manager)
@manager = manager
end
# ...
end
# managerを取得するためにはdepartmentを経由しないといけない という知識が必要なくなる
manager = person.manager
7.6 横流しブローカーの除去
クライアントに、委譲オブジェクトを直接呼び出させる。(<7.5 委譲の隠蔽>の逆)
条件: クラスが単純な委譲をやりすぎている。
理由: 委譲オブジェクトに新しいメンバが追加されるたびに、サーバクラスに委譲メソッドを作らないといけないため。(<7.5 委譲の隠蔽>の代償)
<7.5 委譲の隠蔽>の逆のことをすればいい。
クラスが単純な委譲を行いすぎている場合。
クライアントが直接デリゲートを呼び出すようにします。
"中間者" (サーバー) が行う作業が多すぎる場合、クライアントが直接デリゲートを呼び出す必要があります。
class ClientClass
def do_something
person = Person.new
person.do_something
end
end
class Person
def initialize
@department = Department.new
end
def do_something
@department.do_something
end
end
class ClientClass
def do_something
# Dependencies
person = Person.new
department = Department.new
person.do_something
department.do_something
end
end
8章 データの構成
8.1 自己カプセル化フィールド
フィールド(インスタンス変数)に直接アクセスするのでなく、ゲッター・セッターを作りそれを経由する。
パブリックフィールドがあります。
・それをプライベートにし、アクセサを提供します。
動機
データを公開するべきではありません。
class Item
def initialize(base_price, tax_rate)
@base_price = base_price
@tax_rate = tax_rate
end
def raise_base_price_by(percent)
@base_price = @base_price * (1 + percent/100)
end
def total
@base_price * (1 + @tax_rate)
end
end
class Item
attr_accessor :base_price, :tax_rate
def initialize(base_price, tax_rate)
@base_price = base_price
@tax_rate = tax_rate
end
def raise_base_price_by(percent)
base_price = base_price * (1 + percent/100)
end
def total
base_price * (1 + tax_rate)
end
end
class ImportedItem < Item
attr_reader :import_duty
def initialize(base_price, tax_rate, import_duty)
super(base_price, tax_rate)
# 関税
@import_duty = import_duty
end
def tax_rate
# Item側の振る舞いを変えず、簡単にtax_rateをオーバーライドできた
super + import_duty
end
end
8.2 データ値からオブジェクトへ
データ項目をオブジェクトに変換する。
理由: 1つの単純なデータ(項目)として表現していたものが、それほど単純でないと分かることがある。そういったときに、振る舞いを追加できるようにするため。
class Order
attr_accessor :customer
def initialize(customer)
@customer = customer # 顧客名(文字列)
end
end
# ----------------------
# 別のクラスのどこかのコード
# 顧客が注文したorderの数を取得
def self.number_of_orders_for(orders, customer)
orders.select { |order| order.customer == customer }.size
end
class Order
def initialize(customer)
@customer = Customer.new(customer)
end
def customer
@customer.name
end
def customer=(value)
@customer = Customer.new(value)
end
end
class Customer
attr_reader :name
def initialize(name)
@name = name
end
end
8.3 値から参照へ
条件: 同じインスタンス(値オブジェクト)をいくつも生成しており、それらを1つに圧縮したい場合。
その値オブジェクトを参照オブジェクトに変える。
毎回インスタンスを生成するのでなく、オブジェクト群をハッシュに格納(ロード)しておき、そのハッシュから探して返却する。
class Sample
instances = {}
def store
instances[name] = self
end
def self.with_name(name)
instances[name]
end
end
8.4 参照から値へ
オブジェクトの参照が小さく、不変であり、扱いが厳しい場合があります。
値オブジェクトに置き換えます。
動機
分散システムや並行システムで使用するため、参照オブジェクトは小さく不変でなければなりません。しかし、参照オブジェクトを扱うのが厳しい場合があります。
class Currency
def initialize(code)
@code = code
end
end
Currency.new("USD") == Currency.new("USD") # falseを返す
class Currency
attr_reader :code
def initialize(code)
@code = code
end
def ==(other)
return false unless other.is_a?(Currency)
code == other.code
end
alias eql? ==
def hash
code.hash
end
end
Currency.new("USD") == Currency.new("USD") # trueを返す
8.5 配列からオブジェクトへ
ある配列の要素が異なる意味を持つ場合があります。
配列をオブジェクトに置き換えます。各要素にはフィールドを持たせます。
動機
配列は、類似したオブジェクトのコレクションを保持するためだけに使用するべきです。
row = []
row[0] = "Liverpool"
row[1] = "15"
class Performance
attr_accessor :name, :wins
end
row = Performance.new
row.name = "Liverpool"
row.wins = "15"
8.6 ハッシュからオブジェクトへ
条件: 異なる種類のオブジェクトを格納しているhashがある。
各キーに対応するフィールドを持つオブジェクトを定義し、hashを削除する。
(感想: フィールドをメソッドに変換できるのがメリット。)
new_network = { nodes: [], old_networks: [] }
new_network[:old_networks] << node.network
new_network[:nodes] << node
new_network[:name] = new_network[:old_networks].map { |network| network.name }.join(" - ")
# このように使いたい↓
new_network = NewworkResult.new
new_network.old_networks << node.network
new_network.nodes << node
new_network.name = new_network.old_networks.map { |network| network.name }.join(" - ")
# なので、クラスを定義する↓
class NewworkResult
attr_reader :old_networks, :nodes
# attr_accessor :name ← メソッドに移した
def initialize
@old_networks, @nodes = [], []
end
# nameフィールド→nameメソッドに移し、処理を定義できる
def name
@old_networks.old_networks.map { |network| network.name }.join(" - ")
end
end
8.7 片方向リンクから双方向リンクへ
互いに相手の機能を使用する必要がある2つのクラスがあり、一方向のリンクしかない場合。
・バックポインタを追加し、修飾子を変更して両方のセットを更新する。
動機
参照されるオブジェクトが、自分を参照するオブジェクトにアクセスする必要がある場合。
class Order
def initialize(customer)
@customer = customer
end
def customer
@customer
end
def customer=(customer)
@customer.friend_orders.delete(self) if @customer
@customer = customer
@customer.friend_orders << self if @customer
end
end
class Customer
def initialize(name)
@name = name
@orders = []
end
def add_order(order)
order.customer = self
@orders << order
end
def remove_order(order)
order.customer = nil
@orders.delete(order)
end
def orders
@orders.dup.freeze
end
def friend_orders
@orders
end
end
8.8 双方向リンクから片方向リンクへ
2方向の関連がありますが、1つのクラスはもう他のクラスの機能が必要ありません。
・関連の不要な側を削除する。
動機
双方向の関連が必要でない場合は、複雑さを減らし、ゾンビオブジェクトを処理し、相互依存関係を排除します。
8.9 マジックナンバーからシンボル定数へ
マジックナンバーを意味がわかる名前の定数に置き換える。
(感想: とても重要。これは今後必ず行う。)
def sample_method(number)
number * 3.14
end
PI = 3.14
def sample_method(number)
number * PI
end
8.10 コレクションのカプセル化
あるメソッドがコレクションを返している。
読み取り専用のビューを返し、追加・削除用のメソッドを提供する。
動機
カプセル化することで、所有クラスとそのクライアントの結合度が低減される。
ゲッターはコレクションオブジェクト自体を返すべきではない。
ゲッターは、コレクションの操作を防止し、不必要な詳細を隠すものを返すべきである。
コレクションに対するセッターは存在しないべきで、要素の追加・削除に対応する操作のみを提供するべきである。
class Person
def initialize(name)
@name = name
@courses = []
end
def courses
@courses
end
class Person
def initialize(name)
@name = name
@courses = []
end
def courses
@courses.dup.freeze
end
def add_course(course)
@courses << course
end
def remove_course(course)
@courses.delete(course)
end
end
8.11 レコードからデータクラスへ
伝統的なプログラミング環境でレコード構造とやり取りする必要があります。
レコード用にダムなデータオブジェクトを作成してください。
動機
既存のプログラムをコピーする場合。
伝統的なプログラミングAPIやデータベースで構造化されたレコードを通信する場合。
8.12 タイプコードからポリモーフィズムへ
- サブクラスによるタイプコードの置き換え(Replace Type Code with Subclasses)
クラスに数値のタイプコードがあり、そのコードが動作に影響を与えません。
その数値を新しいクラスに置き換えてください。
class Employee
ENGINEER = 0
SALESMAN = 1
MANAGER = 2
def initialize(type)
@type = type
end
def pay_amount
case @type
when ENGINEER
@monthly_salary
when SALESMAN
@monthly_salary + @commission
when MANAGER
@monthly_salary + @bonus
else
raise 'Incorrect Employee'
end
end
end
class Employee
def initialize(type)
@type = EmployeeType.new(type)
end
def pay_amount
@type.pay_amount(@monthly_salary, @commission, @bonus)
end
end
class EmployeeType
ENGINEER = 0
SALESMAN = 1
MANAGER = 2
def self.new(type)
case type
when ENGINEER
Engineer.new
when SALESMAN
Salesman.new
when MANAGER
Manager.new
else
raise 'Incorrect Employee Code'
end
end
def pay_amount(salary, commission, bonus)
raise 'Not Implemented'
end
end
class Engineer < EmployeeType
def pay_amount(salary, _commission, _bonus)
salary
end
end
class Salesman < EmployeeType
def pay_amount(salary, commission, bonus)
salary + commission
end
end
class Manager < EmployeeType
def pay_amount(salary, _commission, bonus)
salary + bonus
end
end
8.13 タイプコードからモジュールのextend
8.14 タイプコードからState/Strategyへ
クラスに数値のタイプコードがあるが、サブクラス化はできない。
状態オブジェクトまたは戦略オブジェクトでタイプコードを置き換える
動機
タイプコードに応じて異なるコードを実行するとき
各タイプコードオブジェクトに固有の機能がある場合
30. クラスによるタイプコードの置き換え(Replace Type Code with Class)を実装するための構造
38. 条件記述の削除(Replace Conditional with Polymorphism)を実現するための構造
31. サブクラスによるタイプコードの置き換え(Replace Type Code with Subclasses)と32. サブクラスによるタイプコードの置き換え(Replace Subclass with Fields)が適用できない場合に使用する。
class Employee
ENGINEER = 0
SALESMAN = 1
MANAGER = 2
attr_reader :type
attr_accessor :monthly_salary, :commission, :bonus
def initialize(type)
@type = EmployeeType.new_type(type)
end
def pay_amount
@type.pay_amount(self)
end
end
class EmployeeType
def initialize(code)
@code = code
end
def self.new_type(code)
case code
when Employee::ENGINEER
Engineer.new
when Employee::SALESMAN
Salesman.new
when Employee::MANAGER
Manager.new
else
raise ArgumentError, 'Incorrect Employee Code'
end
end
def pay_amount(_employee)
raise NotImplementedError, "abstract method"
end
end
class Engineer < EmployeeType
def pay_amount(employee)
employee.monthly_salary
end
end
class Salesman < EmployeeType
def pay_amount(employee)
employee.monthly_salary + employee.commission
end
end
class Manager < EmployeeType
def pay_amount(employee)
employee.monthly_salary + employee.bonus
end
end
8.15 サブクラスからフィールドへ
条件: 定数を返すメソッド以外には違いがない複数のサブクラス(子クラス)がある場合。
メソッドをスーパークラス(親クラス)のフィールドに変えて、サブクラスを削除する。
class Person
# ..
end
class Female < Person
def female?
true
end
def code
'F'
end
end
class Male < Person
def female?
false
end
def code
'M'
end
end
class Person
def initialize(female, code)
@female = female
@code = code
end
def female?
@female
end
def code
@code
end
def self.create_female
Person.new(true, 'F') # フィールドに詰めて初期化する
end
def self.create_male
Person.new(false, 'M')
end
end
8.16 属性初期化の遅延実行 , 8.17 属性初期化の先行実行
属性を初期化するタイミングをどうするか(どちらがいいか)という話。
(著者の周りでも2つの意見で5分に分かれたと記載ある。)
メリット・デメリットがあるため、どちらの手法を取るかはチームで決めればいい。
属性初期化の先行実行
class Sample
def initialize
@samples = []
end
end
属性初期化の遅延実行
class Sample
def samples
# メリット: initializeでいちいち初期化しなくていい,コードが減る
# デメリット: アクセスの度に値が変わるためデバッグしにくい
@samples ||= []
end
end
寄り道
上で出てきた ||= はRuby独自の記法。メモ化やローカルキャッシュと呼ぶ。他にも便利なイディオムがあるはずだと思い調べた。
上記サイトを参考に、メモする。
def xxx!
この場合の「!」はメソッド名の一部です。
慣用的に、同名の(! の無い)メソッドに比べてより破壊的な作用をもつメソッドで使われます。
(例: map と map!)
def xx?
この場合の「?」はメソッド名の一部分です。
慣用的に、真偽値を返すメソッドを示すために使われます。
def xxx(&yyy)
&がついた引数はブロック引数。
xxx&.yyy
ぼっち演算子。
xxxがnilでないときにメソッドyyyを呼び出す。
a ||= xxx
「||」演算子の自己代入演算子。
aが偽か未定義 なら aにxxxを代入する。
a ||= :some
p a #=> some
a ||= :sec
p a #=> some # aは定義済みだったので、:secはaに代入されなかった
ls
コマンド出力。
バッククォート(`)で囲まれた文字列は、コマンドとして実行され、その標準出力が文字列として与えられます。
p ls
#=> "README.md\ndata.txt\nrefactoring_1.rb\nrefactoring_2.rb\n"
9章 条件式の単純化
9.1 条件文の分解
条件部からメソッドを抽出する。
条件: 条件が複雑な場合。
(感想: とても重要。よく使用される条件はメソッドに抽出することで再利用しやすくなる。今後複雑な条件を書く必要が出た場合は、必ず検討をする。)
if number >= 10 || number <= -10
p "絶対値が10以上"
else
p "絶対値が10未満"
end
if is_over_ten(number)
p "絶対値が10以上"
else
p "絶対値が10未満"
end
def is_over_ten(number)
number >= 10 || number <= -10
end
9.2 条件分岐の組み替え
Rubyらしく書くことで読みやすくする。
(感想: とても重要。Rubyらしさ、PHPらしさ...というのは言語特有なので、言語を深く知ることはこういうときに力を発揮する。)
三項演算子 → OR代入 へ
samples = params.present? ? params : []
# ↓
samples = params : []
条件分岐 → 明示的なreturn へ
def sample_method(number)
if sample > 10
1
else
2
end
end
# ↓
def sample_method(number)
return 1 if sample > 10
2
end
9.3 条件式の統合
条件: 同じ結果になる条件が複数ある場合。
それらを1つにまとめ、メソッドとして抽出する。
def sample_method
return 0 if @data.size > 5
return 0 if @amount > 10 # 結果は上と同じ
1
end
def sample_method
return 0 if sample_condition
1
end
def sample_condition
@data.size > 5 || @amount > 10
end
9.4 重複する条件分岐の断片の統合
条件分岐の各ブランチに同じコードがある場合、条件式の外に移動する。
動機
何が変わり、何が変わらないかがより明確になる。
if special_deal?
total = price * 0.95
send
else
total = price * 0.98
send
end
↓
if special_deal?
total = price * 0.95
else
total = price * 0.98
end
send
9.5 制御フラグの除去
breakやreturnを使って、制御フラグを除去する。
条件: 制御フラグがある場合。
break使用
def check_security(people)
found = false # 制御フラグ
people.each do |person|
unless found
if person == "Taro"
send_alert
found = true # ある条件に合致したら制御フラグを切り替え
end
if person == "Hanako"
send_alert
found = true
end
end
end
end
def check_security(people)
people.each do |person|
if person == "Taro"
send_alert
break # ループを抜ける
end
if person == "Hanako"
send_alert
break
end
end
end
# まだリファクタの余地があるが、別の話になるので割愛
return使用
def check_security(people)
# 制御フラグ かつ 別で使用する値
found = "" # 初期値
people.each do |person|
if found == ""
if person == "Taro"
send_alert
found = "Taro"
end
if person == "Hanako"
send_alert
found = "Hanako"
end
end
end
some_later_code(found)
end
def check_security(people)
found = found_miscreant(people) # 2つのことをしていたので、メソッドに切り出し
some_later_code(found)
end
# foundの値を返すメソッド
def found_miscreant(people)
people.each do |person|
if person == "Taro"
send_alert
return "Taro" # 早期return
end
if person == "Hanako"
send_alert
return "Hanako"
end
end
end
"" # 条件に合致しなかった → 初期値のまま
end
9.6 条件分岐のネストからガード節へ
条件:
条件分岐によって、正常な実行経路がわかりづらい場合。
異常な状態を表す条件(特殊条件)がある場合。
特殊条件をガード節で処理(早期リターン)する。
(感想: とても重要。使い分けの指針を下記した。)
条件分岐には2種類ある。
分岐先のいずれも(if/else)、正常な振る舞いで、同じぐらい実行され、同じくらい重要な場合。 → if/else
分岐先の片方が、異常状態を表したり、まれなケースな場合。 → ガード節
# 支払う給与
def pay_amount
if @dead
result = dead_amount # 死亡
else
if @separated
result = separated_amount # 別居中
else
if @retired
result = retired_amount # 退職
else
result = normal_pay_amount # 通常
end
end
end
result
end
def pay_amount
# 特殊な条件は早期return
return dead_amount if @dead # 死亡
return separated_amount if @separated # 別居中
return retired_amount if @retired # 退職
normal_pay_amount # 通常
end
条件式を逆にしながら<条件分岐のネストからガード節へ>
(感想: 条件を逆にしたほうがすっきりするときがある ということだと思う。)
def adjussted_capital
result = 0
if @capital > 0
if @interest_rate > 0 && @duration > 0
result = ( @income / @duration) * ADJ_FACTOR
end
end
result
end
def adjussted_capital
return 0 if @capital <= 0 # 条件を逆にした
return 0 if @interest_rate <= 0 || @duration <= 0 # 条件を逆にした
(@income / @duration) * ADJ_FACTOR
end
9.7 条件分岐からポリモーフィズムへ
(ポリモーフィック関連の)オブジェクトの種類によって分岐する条件分岐を削除し、その分岐先の処理をオブジェクトに移す。
オブジェクトのタイプに応じて異なる動作を選択する条件があります。
条件の各脚をサブクラスのオーバーライドメソッドに移動し、元のメソッドを抽象化します。
動機
オブジェクトのタイプに応じて動作が異なる場合、明示的な条件文を避けます。
スイッチ文は、オブジェクト指向プログラムではあまり使われないべきです。
class Employee
ENGINEER = 0
SALESMAN = 1
MANAGER = 2
def initialize(type)
@type = type
end
def pay_amount
case @type
when ENGINEER
_monthly_salary
when SALESMAN
_monthly_salary + _commission
when MANAGER
_monthly_salary + _bonus
else
raise "Incorrect Employee"
end
end
end
class Employee
ENGINEER = 0
SALESMAN = 1
MANAGER = 2
def set_type(arg)
@type = EmployeeType.new_type(arg)
end
def pay_amount
@type.pay_amount(self)
end
end
class Engineer
def pay_amount(employee)
employee.monthly_salary
end
end
ポリモーフィズム(多相性)
多岐にわたるオブジェクトが、同じメッセージに応答できる能力。
9.8 nullオブジェクトの導入
null値の繰り返しチェックがあります。
null値をnullオブジェクトに置き換えます
動機
オブジェクトは、そのタイプに応じて適切なことを行うべきです。Nullオブジェクトもこのルールに従うべきです。
Nullオブジェクトパターンは、特殊ケースパターンの弟分です。
if customer.nil?
plan = BillingPlan.basic
else
plan = customer.plan
end
class Customer
end
class NullCustomer < Customer
end
9.9 アサーションの導入
コードの一部がプログラムの状態について何かを仮定しています。
アサーションを使って仮定を明示的にします
アサーションは、常に真であると仮定される条件文です。
アサーションの失敗は、常にチェックされない例外を引き起こすべきです。
アサーションは通常、本番コードでは削除されます。
コミュニケーションの助けとして: アサーションは、コードがどのような仮定をしているかを読者に理解させるのに役立ちます。
デバッグの助けとして: アサーションは、バグを発生源に近いところでキャッチするのに役立ちます。
def get_expense_limit
# should have either expense limit or a primary project
return (_expense_limit != NULL_EXPENSE) ?
_expense_limit :
_primary_project.member_expense_limit
end
def get_expense_limit
assert(_expense_limit != NULL_EXPENSE || !_primary_project.nil?)
return (_expense_limit != NULL_EXPENSE) ?
_expense_limit :
_primary_project.member_expense_limit
end
10章 メソッドの呼び出しの単純化
10.1 メソッド名の変更
メソッド名を修正。
条件: メソッド名からメソッドの目的がわからない場合。
# def sample_method (悪い)
def full_name
"#{name_last} #{name_first}"
end
10.2 引数の追加
条件: メソッドの振る舞いの変更により。引数を追加しないといけない。
引数を追加する。
ただし、代わりになる方法を考えてから行うこと。
オブジェクトに問い合わせればいい場合
オブジェクトを渡したほうがいいのでは?
逆に、そのオブジェクトに情報を提供するメソッドを追加するほうがいいのでは?
メソッドは呼び出し元からさらなる情報を必要とする。
この情報を渡すことができるオブジェクトのパラメータを追加する
動機
メソッドを変更した後、さらなる情報が必要となる。
get_contact()
get_contact(date)
10.3 引数の削除
メソッド本体で使用されなくなったパラメータがある。
それを削除する
動機
パラメータがもはや必要ではない。
get_contact(date)
get_contact()
10.4 問い合わせと更新の分離
問い合わせ用と更新用の2つのメソッドを作る。
条件: 値を返すとともに、オブジェクトの状態に変更を加えるメソッドがある場合。
理由: 副作用がないメソッドは、考えなければならないことが大幅に減る。
値を返すがオブジェクトの状態も変更するメソッドがある。
クエリ用のメソッドと変更用のメソッドの2つに分ける
get_total_out_standing_and_set_ready_for_summaries()
get_total_out_standing()
set_ready_for_summaries()
副作用があるメソッドか、副作用がないメソッドかどうかを明確にする。
メソッドの返り値が呼び出し元で使われている かつ 副作用がある場合は分離すべき。
(感想: 重要。使い手・読み手が考えることが少なくなり、結果として可読性も向上する。副作用があるかどうかは命名で明確にすること。GraphQLではQueryとMutationでこの考え方をしていたが、Railsでは考えたことが無かった。)
10.5 メソッドのパラメータ化
条件: 複数のメソッドが異なる値を使って、同じようなことをしている場合。
その異なる値を引数とする1つのメソッドにまとめる。
類似した処理を行うが、メソッド本体に含まれる値が異なるいくつかのメソッドがある。
異なる値をパラメータとして使用する1つのメソッドを作成する
動機
重複コードを削除し、柔軟性を向上させる。
five_percent_raise()
ten_percent_raise()
raise(percentage)
10.6 引数から別々のメソッドへ
列挙パラメータの値に応じて異なるコードを実行するメソッドがある。
パラメータの各値に対して別々のメソッドを作成する
動機
条件付きの振る舞いを避ける
コンパイル時のチェックを得る
インターフェースが明確になる
def set_value(name, value)
if name == "height"
@_height = value
return
end
if name == "width"
@_width = value
return
end
raise "should never reach here"
end
def set_height(arg)
@_height = arg
end
def set_width(arg)
@_width = arg
end
10.7 オブジェクト自体の受け渡し
オブジェクトから複数の値を取得し、それらの値をメソッド呼び出しのパラメータとして渡す。
代わりにオブジェクト全体を送る
動機
パラメータリストが変更に強くなる
コードがより読みやすくなる
渡されたオブジェクトで既に行われている可能性のある重複コードを削除する
マイナス:パラメータオブジェクトと呼び出されるオブジェクトの間に依存関係が生じる
low = days_temp_range().low
high = days_temp_range().high
within_plan = plan.within_range(low, high)
within_plan = plan.within_range(days_temp_range())
10.8 引数からメソッドへ
オブジェクトがメソッドを呼び出し、その結果をメソッドのパラメータとして渡す。呼び出されたオブジェクトもこのメソッドを呼び出すことができる。
パラメータを削除し、呼び出されたオブジェクトにメソッドを呼び出させる
動機
メソッドが別の方法で渡されるパラメータとしての値を取得できる場合、それを行うべきである。
受信メソッドが同じ計算を行うことができる場合(呼び出しメソッドのパラメータを参照しない)
呼び出し元オブジェクトへの参照を持つ別のオブジェクト上でメソッドを呼び出す場合。
base_price = @_quantity * @_item_price
discount_level = get_discount_level()
final_price = discounted_price(base_price, discount_level)
base_price = @_quantity * @_item_price
final_price = discounted_price(base_price)
10.9 引数オブジェクトの導入
条件: 自然にまとめられる引数のグループがある場合。
それらの引数を、オブジェクトにする。
いくつかのパラメータが常に一緒に渡される。
それらのパラメータをまとめて1つのオブジェクトにする
動機
メソッドシグネチャがより明確になる
データをクラスタ化し、コードの意図を明確にする
オブジェクトを操作する新しいメソッドを追加することが容易になる
def amount_in_range(min_amount, max_amount)
# ...
end
class AmountRange
attr_reader :min_amount, :max_amount
def initialize(min_amount, max_amount)
@min_amount = min_amount
@max_amount = max_amount
end
end
def amount_in_range(amount_range)
# ...
end
10.10 設定メソッドの削除
条件: インスタンス作成時に設定して、その後は変更すべきでないフィールドがある場合。
設定メソッド(セッター)を削除する。
→ attr_writer(attr_accessor) や def フィールド=(value)を削除する。
フィールドは作成時に設定され、それ以降は変更されないべきです。
そのフィールドに対する設定メソッドを削除してください
動機
意図を明確にする: 一度作成されたフィールドを変更したくない場合は、設定メソッドを提供しない(そしてフィールドをfinalにする)。
class Employee
def set_immutable_value
end
end
class Employee
# hogehoge
end
10.11 メソッドの隠蔽
あるメソッドが他のクラスで使用されていない場合。
メソッドをprivateにする
動機
メソッドがクラスの外部で必要ない場合は、隠すべきです。
class Employee
def method
end
end
から
class Employee
private
def method
end
end
10.12 コンストラクタからファクトリメソッドへ
条件: オブジェクトを生成するときに、単なる構築以上のことをしたい場合。
コンストラクタを取り除いて、ファクトリメソッドを作る。
コンストラクタ : クラスからインスタンスを作る時に実行される処理
class ProductController
def create
# どういう値かによって、作るプロダクトの種類が異なる
# → この判定をオブジェクトクラスでやっていないことが問題。色々な箇所で毎回このロジックを書く必要があるため。
@product = if imported
ImportedProduct.new(base_price)
else
if base_price > 1000
LuxuryProduct.new(base_price)
else
Product.new(base_price)
end
end
end
end
class ProductController
def create
@product = Product.create(base_price, imported)
end
end
class Product
# ファクトリメソッドへ
def self.create(base_price, imported=false)
if imported
ImportedProduct.new(base_price)
else
if base_price > 1000
LuxuryProduct.new(base_price)
else
Product.new(base_price)
end
end
end
end
オブジェクトの作成時に、単純な構築を超えた処理が必要です。
コンストラクタをファクトリーメソッドに置き換える
動機
サブクラス(タイプ)に応じたオブジェクトを作成する。コンストラクタは要求されたオブジェクトのインスタンスのみを返すことができるため、ファクトリーメソッドが必要です。
class Employee
def initialize(type)
@type = type
end
end
から
class Employee
def self.create(type)
new(type)
end
private
def initialize(type)
@type = type
end
end
10.13 エラーコードから例外へ
条件: エラーを示すために、特別なエラーコード(エラーと知らせるための変な値)を返している。
代わりに例外を生成する。
メソッドがエラーを示す特別なコードを返します。
代わりに例外をスローする
動機
エラーを検出したプログラムがどう対処すべきかわからない場合、それを呼び出し元に知らせる必要があり、呼び出し元はエラーをチェーンに渡すことがあります。
def withdraw(amount)
if amount > @balance
return -1
else
@balance -= amount
return 0
end
end
から
def withdraw(amount)
raise BalanceException if amount > @balance
@balance -= amount
end
10.14 例外からテストへ
呼び出し元が最初にチェックできる条件でチェックされた例外がスローされています。
呼び出し元に最初にテストを行うように変更する
動機
例外を過剰に使用しないでください。それらは例外的な動作(予期しない動作)に使用する必要があります。
条件テストの代替品として使用しないでください。
操作を呼び出す前に、予想される間違った条件をチェックしてください。
def get_value_for_period(period_number)
begin
return @values[period_number]
rescue IndexError
return 0
end
end
から
def get_value_for_period(period_number)
return 0 if period_number >= @values.length
return @values[period_number]
end
10.15 ゲートウェイの導入
10.16 式ビルダーの導入
11章 一般化の処理
11.1 メソッドの上位階層への移動
複数の子クラスで同じ結果になるメソッドは、親クラスへ移動する。
2つのサブクラスが同じフィールドを持っています。
フィールドをスーパークラスに移動する
動機
重複したデータ宣言を削除します。
サブクラスからスーパークラスに動作を移動できます。
class Salesman < Employee
attr_accessor :name
end
class Engineer < Employee
attr_accessor :name
end
class Employee
attr_accessor :name
end
class Salesman < Employee; end
class Engineer < Employee; end
11.2 メソッドの下位階層への移動
サブクラスで同じ結果のメソッドがあります。
それらをスーパークラスに移動する
動機
重複した動作を排除します。
class Salesman < Employee
def name; end
end
class Engineer < Employee
def name; end
end
class Employee
def name; end
end
class Salesman < Employee; end
class Engineer < Employee; end
11.3 モジュールの抽出
条件: 複数のクラスに重複する振る舞いがある場合。
振る舞いを新たなモジュールに移動させ、includeする。
重複を取り除くときは、できるだけクラスの抽出を使う。
ただし、他の(第3の)クラスに再利用できないなら、モジュールの抽出を使う。
11.4 モジュールのインライン化
11.5 サブクラスの抽出
条件: クラスが、一部のインスタンスしか使わないフィールドがある。
そのフィールドのために子クラスを作る。
class JobItem
attr_reader :quantity, :employee
def initialize(unit_price, quantity, is_labor, employee)
@unit_price = unit_price
@quantity = quantity
@is_labor = is_labor
@employee = employee
end
def total_price
unit_price * @quantity
end
def unit_price
# labor(修理するのが大変)かどうかで振る舞いが変わる
labor? ? @employee.rate : @unit_price
end
def labor?
@is_labor
end
end
class JobItem
attr_reader :unit_price, :quantity
# employeeは不要になった
def initialize(unit_price, quantity, is_labor=false)
@unit_price = unit_price
@quantity = quantity
@is_labor = is_labor
end
def total_price
unit_price * @quantity
end
def labor?
false
end
end
# 子クラス
class LaborItem < JobItem
attr_reader :employee
# LaborItemの生成には、unit_price, is_laborは不要
def initialize(quantity, employee)
super(0, quantity, true) # is_laborはtrue
@employee = employee
end
def labor?
true
end
def unit_price
@employee.rate
end
end
11.6 継承の導入
条件: 同じような機能を持つクラスが2つある場合。
継承を使い、親子にする。
継承の導入を使うか迷うような場合は、クラスの抽出を使えばいい。
クラスの抽出が使えないなら、モジュールの抽出。
2つのクラスが振る舞いだけでなく、インターフェイスも同じなら、継承の導入を使う。
class MountaionBike
TIRE_WIDTH_FACTOR = 6
attr_accessor :tire_diameter
def wheel_circumference
Math::PI * (@wheel_diameter + @tire_diameter)
end
def off_road_ability
@tire_diameter * TIRE_WIDTH_FACTOR
end
end
class FrontSuspensionMountainBike
TIRE_WIDTH_FACTOR = 6
FRONT_SUSPENTION_FACTOR = 8
attr_accessor :tire_diameter, :front_fork_travel
def wheel_circumference
# MountaionBikeと同じ
Math::PI * (@wheel_diameter + @tire_diameter)
end
def off_road_ability
# MountaionBikeと少し違う
@tire_diameter * TIRE_WIDTH_FACTOR + @front_fork_travel * FRONT_SUSPENTION_FACTOR
end
end
class MountaionBike
TIRE_WIDTH_FACTOR = 6
attr_accessor :tire_diameter
def wheel_circumference
Math::PI * (@wheel_diameter + @tire_diameter)
end
def off_road_ability
@tire_diameter * TIRE_WIDTH_FACTOR
end
end
# MountaionBikeの派生なので、MountaionBikeの子クラスにする
class FrontSuspensionMountainBike < MountaionBike
FRONT_SUSPENTION_FACTOR = 8
attr_accessor :front_fork_travel
def off_road_ability
# MountaionBikeの結果から、FrontSuspensionMountainBike独自の計算をする
super + @front_fork_travel * FRONT_SUSPENTION_FACTOR
end
end
11.7 階層構造の統合
スーパークラスとサブクラスの違いがほとんどありません。
それらを一緒にマージする
動機
価値を追加していないサブクラスがある場合。
class Employee; end
class Salesman < Employee; end
class Employee; end
11.8 テンプレートメソッドの作成
条件: 同じような処理をするメソッドが別々の子クラスにあるが、内容がわずかに違う。
違いのある処理を抽出して、子クラスで同じメソッド名で実装。
共通する処理を親クラスで実装。
継承を使ったテンプレートメソッド
# レシート表示
def statement
result = "レンタル #{name}"
@rentals.each do |rental|
result << "#{rental.movie.title} #{rental.charge}" # 映画のタイトル、料金
end
result << "合計#{total_charge}"
result << "#{total_frequent_rental_points}"
result
end
# レシートをHTMLで表示(なのでstatementと少し違う)
def html_statement
result = "<h1>レンタル #{name}</h1>"
@rentals.each do |rental|
result << "#{rental.movie.title} #{rental.charge}<br/>"
end
result << "<p>合計#{total_charge}</p>"
result << "<p>#{total_frequent_rental_points}</p>"
result
end
# クライアントコード``````
class Customer
def statement
TextStatement.value(self)
end
def html_statement
HtmlStatement.value(self)
end
end
# ````````
class Statement
# 2つの子クラスで共通している処理
def value(customer)
result = header_string(customer)
customer.rentals.each do |rental|
result << each_rental_string(rental)
end
result << footer_string(customer)
end
end
# 子クラス側では、独自の実装をする
class TextStatement < Statement
def header_string(customer)
"レンタル #{customer.name}"
end
def each_rental_string(rental)
"#{rental.movie.title} #{rental.charge}"
end
def footer_string(customer)
<<-EOS
"合計#{customer.total_charge}"
result << "#{customer.total_frequent_rental_points}"
EOS
end
end
class HtmlStatement < Statement
def header_string(customer)
"<h1>レンタル #{customer.name}</h1>"
end
def each_rental_string(rental)
"#{rental.movie.title} #{rental.charge}<br/>"
end
def footer_string(customer)
<<-EOS
"<p>合計#{customer.total_charge}</p>"
"<p>#{customer.total_frequent_rental_points}</p>"
EOS
end
end
モジュールのextendを使ったテンプレートメソッド
Statementクラスのインスタンスを作ることがない場合は、モジュールでextendするのが良い。
継承を使った場合、親クラスは1つだけなのに対し、
extendを使うと、複雑な継承の問題を避けられる。
extend
extendを使うと、moduleのメソッドをそのオブジェクトのインスタンスメソッドとして取り込むことができる。
# クライアントコード``````
class Customer
def statement
Statement.new.extend(TextStatement).value(self)
end
def html_statement
Statement.new.extend(HtmlStatement).value(self)
end
end
# ````````
class Statement
# ...
end
module TextStatement
# ...
end
module HtmlStatement
# ...
end
参考記事
→ 名前空間を作ってモジュール名やメソッド名の衝突を防ぐ。
→ ActiveSupport::Concernについても記載あり。
サブクラスの2つのメソッドが、同じ順序で同様のステップを実行していますが、ステップが異なります。
同じシグネチャでメソッドにステップを取得し、元のメソッドが同じになるようにします。その後、それらを引き上げることができます
動機
継承とポリモーフィズムを使用して、わずかに異なる重複動作を排除します。
サブクラスに似たメソッドがある場合、スーパークラスでそれらをまとめます。
class Site; end
class ResidentialSite < Site
def billable_amount; end
end
class LifelineSite < Site
def billable_amount; end
end
class Site
def billable_amount; end
def base_amount; end
def tax_amount; end
end
class ResidentialSite < Site
def base_amount; end
def tax_amount; end
end
class LifelineSite < Site
def base_amount; end
def tax_amount; end
end
11.9 継承から委譲へ
条件: 子クラスが親クラスのインターフェイスの一部しか使っていない or データを継承することが望ましくない 場合。
親クラスに処理を委譲し、継承構造を解消する。
サブクラスはスーパークラスのインターフェイスの一部のみを使用しているか、データを継承したくありません。
スーパークラスのフィールドを作成し、委譲にメソッドを調整し、サブクラスを削除する
動機
委譲を使用すると、デリゲートされたクラスの一部のみを使用していることが明確になります。
インターフェースのどの側面を取るか、どの側面を無視するかを制御できます。
class Vector
def empty?; end
end
class Stack < Vector; end
class Vector
def empty?; end
end
class Stack
def initialize
@vector = Vector.new
end
def empty?
@vector.empty?
end
end
11.10 委譲から継承へ
条件: 多数の委譲メソッドを書いている。
委譲先のクラスをモジュールにして、委譲元のクラスでincludeする。
(<11.9 継承から委譲へ>の逆だが、基本的には継承でなくモジュールを使って階層を作る。)
あなたは委譲を使っていて、しばしばインターフェース全体のために多くのシンプルな委譲を書いています。
デリゲートをサブクラスにすることで、デリゲートクラスを作成します
動機
デリゲートのすべてのメソッドを使用している場合。
デリゲートしているクラスのメソッドをすべて使用していない場合は、それを使用すべきではありません。
注意すべきは、デリゲートが複数のオブジェクトに共有され、変更可能である場合です。データ共有は、継承に戻すことはできません。
class Person
def name; end
end
class Employee
def initialize
@person = Person.new
end
def name
@person.name
end
end
class Person
def name; end
end
class Employee < Person; end
11.11 抽象スーパークラスからモジュールへ
条件: 継承階層を持っているが、親クラスのインスタンスは作るつもり(予定)がない。
継承→モジュールに書き換える。
12章 大規模なリファクタリング
この章では、6~11章のような1つ1つの指し手ではなく、試合全体を説明している。
12.1 リファクタリングという試合の性質
12.2 大規模リファクタリングが重要な理由
12.3 4つの大規模リファクタリング
12.4 複合的な継承階層の分割
条件: 2つの仕事をしている継承階層がある。
理由: 継承関係がもつれると、コードの重複が発生し、メンテしづらいコードになるため。また、単一責務でないため。
2つの階層に分け、片方からもう片方を実行するには委譲を使う。
一度に2つの仕事をしている継承階層があります。
2つの階層を作成し、一方から他方を呼び出すために委譲を使用します
動機
入り組んだ継承はコードの重複を引き起こします。
階層内の特定のレベルのすべてのクラスが、同じ形容詞で始まるサブクラスを持っている場合、おそらく1つの階層で2つの仕事をしています。
手順の注意
階層で行われている異なる仕事を特定します。2次元(またはx次元)グリッドを作成し、異なる仕事で軸にラベルを付けます。
class Deal; end
class ActiveDeal < Deal; end
class PassiveDeal < Deal; end
class TabularActiveDeal < ActiveDeal; end
class TabularPassiveDeal < PassiveDeal; end
から
class Deal
attr_accessor :presentation_style
end
class ActiveDeal < Deal; end
class PassiveDeal < Deal; end
class PresentationStyle; end
class TabularPresentationStyle < PresentationStyle; end
class SinglePresentationStyle < PresentationStyle; end
12.5 手続き型設計からオブジェクト指向設計へ
(感想: 本書にははっきりとした理由が書いていなかった。なので、手続き型/オブジェクト指向のメリット/デメリットを調べた。再利用ができない点、値がグローバルである点が手続き型のデメリットであり、大きなシステムになるとそのデメリットが膨れ上がると感じた。)
手続き型スタイルで書かれたコードがあります。
データレコードをオブジェクトに変換し、動作を分割し、動作をオブジェクトに移動します
動機
OOP(少なくともJAVAで)を使用する
class OrderCalculator
def determine_price(order); end
def determine_taxes(order); end
end
class Order; end
class OrderLine; end`
class Order
def price; end
def taxes; end
end
class OrderLine
def price; end
def taxes; end
end
12.6 ドメインのプレゼンテーションからの分離
ドメインロジックはモデルに移す。
ドメインロジックを含むGUIクラスがあります。
ドメインロジックを別のドメインクラスに分離します
動機
プログラムの2つの複雑な部分を、より簡単に変更できる部分に分割します。
同じビジネスロジックの複数のプレセンテーションを可能にします。
class OrderWindow; end
class OrderWindow
attr_accessor :order
end
ビューには、ユーザーインターフェイスを処理するために必要なロジックだけを収める。
12.7 継承階層の抽出
多くの条件文を通じて、あまりにも多くの作業をしているクラスがあります。
特殊なケースを表す各サブクラスを持つクラスの階層を作成します
動機
1つのアイデアを実装するクラスが2つ、3つ、10つになります。
単一責任の原則を維持してください。
class BillingScheme; end
class BillingScheme; end
class BusinessBillingScheme < BillingScheme; end
class ResidentialBillingScheme < BillingScheme; end
class DisabilityBillingScheme < BillingScheme; end
参考文献
復刊ドットコム: リファクタリング:Rubyエディション
ジェイ・フィールズ シェーン・ハービー マーティン・ファウラー 著 / 長尾高弘 訳