「リファクタリング (Rubyエディション)」要点
書籍
この記事について
個人のメモとして、下記の事項のみを抜粋して記載している。
- 自分が今後使いそうな内容
- 忘れそうな内容
- 初めて知って、感動した内容
注意
自分なりに解釈し、咀嚼した内容を記載しているため、実際にリファクタリングを適用する際は、本書の手順
項にちゃんと從って行わないといけない。
リファクタリングとは
コードの外から見た振る舞いは変えずに、内部構造を改良すること。
- バグを埋め込む危険を最小限に抑えてコードをクリーンにする方法。
(感想: 「最小限に抑えて」と書籍に書いていた。やはり、バグを作りだしてしまう危険は0ではないんだと感じた。) - コードが書かれた後で設計を改良すること。
コードは設計してから書いているはずではあるが、時間が経つとともにコードは書き換えられ、設計に基づく構造は次第に変わっていく。
(感想: 完璧な設計なんてものは無い。その理由は、ビジネスは変化するものだからだと思う。)
1章 リファクタリング -最初のサンプル
(この章は例題を取り上げて、リファクタリングの重要ポイントが解説されていた。詳細は割愛し、重要だと感じた部分のみ列挙する。)
重要ポイント
- 機能を追加しにくいプログラムに機能を追加するとき、まずリファクタリングして作業をしやすくしてから進めること。
- リファクタリングするときの第一歩は、テストを作ること。
- プログラムを少しずつ変更すること。そうすると、間違えても簡単にバグを見つけられる。
- コンピュータが理解できるコードは誰でも書ける。優れたプログラマは、人間が理解できるコードを書く。
(感想: この点が最重要。) - case文を使うときは、他のオブジェクトでなく、自身のデータ(状態)によって分岐すべき。
2章 リファクタリングの原則
リファクタリングはいつすべきか
特別な時間を割いて、決意してすることでなく、何か他のことをするときに、役立つからリファクタリングするという性質のもの。
(感想: 現状、自分が書くコードだけで完結している部分はリファクタしているが、既存のコードも絡んでくるコードについては全然できていない。息を吸って吐くようにリファクタできるようにレベルまで高めないといけない。)
3章 コードの臭い
リファクタリングしたほうがいいな と思うのはどういうときか、そのタイミングを説明。
3.1 コードの重複
複数の箇所で同じ構造のコードは、統一する方法を探す。
3.2 長いメソッド
手続きは長いほどわかりにくくなるため、メソッドは短くする。
(コメントを書きたいと思うたびにメソッドを書くぐらいに、分解をする。)
メソッドの名前の付け方が大事で、メソッド名が良ければその中身を見る必要がなくなる。
3.3 大きなクラス
1つのクラスの責務が多い場合は、大体インスタンス変数が多すぎる。
3.4 長い引数リスト
引数リストが長いとわかりにくく、一貫性が崩れて使いにくくなりやすく、必要なデータが増えるたびにメンテしなければいけない。
3.5 変更系統の分岐
バリエーションを処理するための変更は、1つのクラス/モジュールであるべき。
バリエーションの表現には、クラス/モジュールの新しい型を使うべき。
3.7 メソッドの浮気
メソッドが、自分のクラス以外のクラスに関心を持っている状態。
例えば、計算をするために、他のオブジェクトのゲッターを何回も呼び出しているメソッド 等。
3.10 case文
case文が少ないのはオブジェクト指向らしいコードの特徴。
case文には、コードの重複を生むという問題点がある。
case文を見かけたら、ほとんどの場合ポリモーフィズムを検討すべき。
(感想:ポリモーフィズムを検討すべきと言っているのは、おそらくオブジェクトのタイプによって分岐する場面を想定していると思う。)
3.12 仕事をしないクラス
1つ1つのクラスには、メンテナンスと理解のための時間というコストがかかる。
コストに見合う仕事をしないクラスは削除すべき。
3.13 空論的一般化
人が「いずれこういったこともできるようにする必要があると思うよ」と言ってあらゆるフックを欲しがったり、必要でないものを処理するための特殊条件を設けたりしている場合。
わかりにくく、メンテしづらいコードになることが多い。
使われていないなら、削除すべき。
3.15 メッセージの連鎖
クライアントがオブジェクトに別のオブジェクトを要求し、それが更に別のオブジェクトを要求...というような状態。
3.17 親密すぎるクラス
互いにプライベートな(非公開な)部分まで知っている状態。
3.22 コメント
コメントを消臭剤として使っていることが多い。その臭いの原因を取り除くべき。
(コメントを書いていけないということではない。)
4章 テストの構築
- リファクタリングにテストは必要。
- 赤→緑→リファクタリング
(感想: 「テスト駆動開発」を勉強済なのですんなり理解できた。) -
例外
もテストする必要がある。
- 赤→緑→リファクタリング
- バグレポートを受け取ったら、それを再現するテストを書くこと。
5章 リファクタリングのカタログに先立って
(この章は 6~12章の構成の説明 であったため省略。)
6章 メソッドの構成方法
6.1 メソッドの抽出
コードの一部をメソッドにして、その目的を説明する名前をつける。
- 理由: メソッドの粒度が細ければ、流用性が高くなる。
- 注意: 意味のある良い名前が思いつかないなら、それは抽出すべきでない。
(感想: とても重要。<6.2 メソッドのインライン化>にもあるように。メソッドの粒度は粗すぎず細かすぎないように気をつける。)
6.1.1 パターン: ローカル変数なし
def sample_method
puts "sample"
# その他諸々の処理...
end
def sample_method
print_sample_method
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
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度しか代入されていない場合。
- 理由: リファクタリングを妨げる。
6.4 一時変数から問合せメソッドへ
一時変数(式)をメソッドにする。
メソッドは最初は非公開にしておき、後で他の用途が見つかったら緩めればいい。
- 条件: なし。
- 理由: 一時変数の問題点は、一時的でローカルであること。一時変数にアクセスするためにはメソッドを長くする以外になく、メソッドの長大化を助長するため。
(感想: とても重要。特に「メソッドを複数回呼び出しまって良いのか」という点。この点は、個人的にはこれまで抵抗があったが、この本を読んで考え方が理解できた。)
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
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 一時変数からチェインへ
チェイニングして、一時変数を削除。
- 条件: 一時変数に対して複数行に渡ってメソッドを複数回実行している状況。
6.6 説明用変数の導入
処理の目的を説明する名前の一時変数に、式orその一部 の結果を保管する。
- 条件: 式が複雑な場合。
- 理由: 可読性向上のため。
6.7 一時変数の分割
代入ごとに別の一時変数を用意する。
- 条件: ループ変数でも計算結果蓄積用の変数でもないのに、複数回代入される一時変数がある場合。
- 理由: 2つの異なる目的のために1つの一時変数を使い回すと混乱するため。
6.8 引数への代入の除去
引数への代入の代わりに一時変数を使う。
- 条件: 引数に代入を行っている場合。
- 理由: Rubyは(参照渡しでなく)値渡しなため、呼び出し元ルーチンには影響はないが、紛らわしく、読みにくいため。
また、引数の用途は渡されたものを表すことなので、それに代入して別の役割を持たせないようにするため。
6.9 メソッドからメソッドオブジェクトへ
メソッドを独自のオブジェクト(クラス)に変える。ローカル変数をそのオブジェクトのインスタンス変数にする。
- 条件: <メソッドの抽出>を適用できないようなローカル変数の使い方をしている場合。
- 理由: 独自のオブジェクト内で、メソッドの分解(<メソッドの抽出>)ができる。
(感想: 手間のかかるリファクタなので、巨大なメソッドに適用するのが費用対効果的に良いと思う。)
6.10 アルゴリズム変更
より簡単なアルゴリズムに変更。
(感想: 6.11項のコレクションクロージャメソッドを使うことで、アルゴリズムを簡単にすることがかなり実現できると思う。)
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
ブロックについて
よく知らなかったため、下記の記事を参考に下記の通り理解した。
ブロック
メソッドに渡すコードの塊。
yeild
ブロックを起動する。
&block
&blockという引数を宣言すると、ブロックがblock変数に代入される。
block.callメソッドを呼ぶと、ブロックの処理が実行される。
6.13 クラスアノテーションの導入
initializeの導入。
6.14 名前付き引数の導入
キーワード引数の導入。
6.15 名前付き引数の除去
キーワード引数の除去。引数が1つであるとき。
- 理由: 引数が1つだと、わざわざキーワードにする必要がない。メソッドを呼び出すたびにキーワード引数を書かないといけなく、費用対効果が小さいため。
6.16 使われていないデフォルト引数の除去
使われていないデフォルト引数を除去。
引数による条件分岐などが削除できる可能性がある。
(感想: 使われていないことをちゃんと確認しないといけない。)
6.17 動的メソッド定義
メソッドを動的に定義する。
6.18 動的レセプタから動的メソッド定義へ
動的メソッド定義を行うことで、method_missing
を使わずに同じような振る舞いを実現。
7章 オブジェクト間でのメンバの移動
7.1 メソッドの移動
メソッドを最もよく使っている他のクラスに、同じ内容の新メソッドを作る。
古いメソッドは、そのメソッドに処理を委ねる or 削除する。
- 条件: メソッドが、自身のクラスよりも他クラスをよく利用している or 利用されている場合。(今はそうでなくても、そうなりつつあるとき。)
7.2 フィールドの移動
移すクラスに新しいフィールドのreader(必要ならwriterも)を作り、フィールドを使っているコードを書き換える。
- 条件: フィールドが、自身のクラスよりも他クラスをよく利用している or 利用されている場合。(今はそうでなくても、そうなりつつあるとき。)
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 クラスの抽出>の逆のことをすればいい。
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 委譲の隠蔽>の逆のことをすればいい。
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.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.9 マジックナンバーからシンボル定数へ
マジックナンバーを意味がわかる名前の定数に置き換える。
(感想: とても重要。これは今後必ず行う。)
def sample_method(number)
number * 3.14
end
PI = 3.14
def sample_method(number)
number * PI
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未満"
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.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 条件分岐のネストからガード節へ
- 条件:
- 条件分岐によって、正常な実行経路がわかりづらい場合。
- 異常な状態を表す条件(特殊条件)がある場合。
特殊条件をガード節で処理(早期リターン)する。
(感想: とても重要。使い分けの指針を下記した。)
# 支払う給与
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 条件分岐からポリモーフィズムへ
(ポリモーフィック関連の)オブジェクトの種類によって分岐する条件分岐を削除し、その分岐先の処理をオブジェクトに移す。
10章 メソッドの呼び出しの単純化
10.1 メソッド名の変更
メソッド名を修正。
- 条件: メソッド名からメソッドの目的がわからない場合。
# def sample_method (悪い)
def full_name
"#{name_last} #{name_first}"
end
10.2 引数の追加
- 条件: メソッドの振る舞いの変更により。引数を追加しないといけない。
引数を追加する。
ただし、代わりになる方法を考えてから行うこと。
- オブジェクトに問い合わせればいい場合
- オブジェクトを渡したほうがいいのでは?
- 逆に、そのオブジェクトに情報を提供するメソッドを追加するほうがいいのでは?
10.4 問い合わせと更新の分離
問い合わせ用と更新用の2つのメソッドを作る。
- 条件: 値を返すとともに、オブジェクトの状態に変更を加えるメソッドがある場合。
- 理由: 副作用がないメソッドは、考えなければならないことが大幅に減る。
(感想: 重要。使い手・読み手が考えることが少なくなり、結果として可読性も向上する。副作用があるかどうかは命名で明確にすること。GraphQLではQueryとMutationでこの考え方をしていたが、Railsでは考えたことが無かった。)
10.5 メソッドのパラメータ化
- 条件: 複数のメソッドが異なる値を使って、同じようなことをしている場合。
その異なる値を引数とする1つのメソッドにまとめる。
10.9 引数オブジェクトの導入
- 条件: 自然にまとめられる引数のグループがある場合。
それらの引数を、オブジェクトにする。
10.10 設定メソッドの削除
- 条件: インスタンス作成時に設定して、その後は変更すべきでないフィールドがある場合。
設定メソッド(セッター)を削除する。
→ attr_writer
(attr_accessor
) や def フィールド=(value)
を削除する。
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
10.13 エラーコードから例外へ
- 条件: エラーを示すために、特別なエラーコード(エラーと知らせるための変な値)を返している。
代わりに例外を生成する。
11章 一般化の処理
11.1 メソッドの上位階層への移動
複数の子クラスで同じ結果になるメソッドは、親クラスへ移動する。
11.3 モジュールの抽出
- 条件: 複数のクラスに重複する振る舞いがある場合。
振る舞いを新たなモジュールに移動させ、include
する。
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つある場合。
継承を使い、親子にする。
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.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を使うと、複雑な継承の問題を避けられる。
# クライアントコード``````
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についても記載あり。
11.9 継承から委譲へ
- 条件: 子クラスが親クラスのインターフェイスの一部しか使っていない or データを継承することが望ましくない 場合。
親クラスに処理を委譲し、継承構造を解消する。
11.10 委譲から継承へ
- 条件: 多数の委譲メソッドを書いている。
委譲先のクラスをモジュールにして、委譲元のクラスでincludeする。
(<11.9 継承から委譲へ>の逆だが、基本的には継承でなくモジュールを使って階層を作る。)
11.11 抽象スーパークラスからモジュールへ
- 条件: 継承階層を持っているが、親クラスのインスタンスは作るつもり(予定)がない。
継承→モジュールに書き換える。
12章 大規模なリファクタリング
この章では、6~11章のような1つ1つの指し手ではなく、試合全体を説明している。
12.4 複合的な継承階層の分割
- 条件: 2つの仕事をしている継承階層がある。
- 理由: 継承関係がもつれると、コードの重複が発生し、メンテしづらいコードになるため。また、単一責務でないため。
2つの階層に分け、片方からもう片方を実行するには委譲を使う。
12.5 手続き型設計からオブジェクト指向設計へ
(感想: 本書にははっきりとした理由が書いていなかった。なので、手続き型/オブジェクト指向のメリット/デメリットを調べた。再利用ができない点、値がグローバルである点が手続き型のデメリットであり、大きなシステムになるとそのデメリットが膨れ上がると感じた。)
12.6 ドメインのプレゼンテーションからの分離
ドメインロジックはモデルに移す。
以上
Discussion