記事の背景
リファクタリングの勉強をしているので、その手法、特に変数のリファクタリングについてまとめました。
サンプルコードは Swift です。これは個人的に Swift が好きだからです。
少しとっつきにくい言語ではあると思いますが、最近は ChatGPT もありますし、お好きな言語・得意な言語に変換して読んでいただけたらなと思います。
また、内容は場合によって変更・追加されることがあります。その他しは編集リクエストなどを見ていただけますと変更点がわかると思います。
本論
変数の抽出
複雑で読みにくい処理は、ローカル変数を駆使し、処理を細分化することで管理が容易になる。
変数または定数はデバッガや print 文を仕掛ける際にも、その内容把握に威力を発揮する。
処理に名前を付与したいとき、即ち変数を抽出を検討するタイミングである。
その名前が関数内でのみ有意義なものであれば、変数の抽出は賢明な選択である。
手順
- 抽出しようとする式に副作用がないことを確認する
- 変更不可な変数を定義する。名付けたい処理の値をその変数に設定する
- 元の処理を新しい変数で置き換える
- テストする
式が 2 回以上現れる場合、それぞれの変数で置き換えて、置き換えるたびにテストする。
また、ここで「変更不可な変数」とは定数のことを指している。
はじめに定数で宣言し、変更する箇所や再代入する箇所が現れたら、変数に変更していくのがポイントである。(ビルドするときにエラーが出るので、わかりやすい)
例
具体的にどのようにして変数を抽出していくか見ていく。
まず、以下のような関数が存在する。
import Foundation
struct Order {
var quantity: Double
var itemPrice: Double
}
func price(order: Order) -> Double {
// 本体価格 = 数量 x 単価
return order.quantity * order.itemPrice - max(0, order.quantity - 500) * order.itemPrice * 0.05 + min(order.quantity * order.itemPrice * 0.1, 100)
}
let order = Order(quantity: 600, itemPrice: 10)
let orderPrice = price(order: order)
print(orderPrice)
この処理の中で変数抽出をしたい箇所は price
関数である。
func price(order: Order) -> Double {
// 本体価格 = 数量 x 単価
return order.quantity * order.itemPrice - max(0, order.quantity - 500) * order.itemPrice * 0.05 + min(order.quantity * order.itemPrice * 0.1, 100)
}
この関数を見てみると、return
文意向がベタ書きでどういった処理をして、どういった値を返すのかよくわからない。
そのため、変数抽出をして読みやすくしていく。
まず、本体価格(basePrice)は数量(quantity)と単価(itemPrice)をかけたものになる。
そのため、basePrice を定義し、処理の内容を設定する。
func price(order: Order) -> Double {
+ let basePrice = order.quantity * order.itemPrice
return order.quantity * order.itemPrice - max(0, order.quantity - 500) * order.itemPrice * 0.05 + min(order.quantity * order.itemPrice * 0.1, 100)
}
次に、元の式を新しい変数(この場合、basePrice)に置き換える。
すると、以下のような処理になる。
func price(order: Order) -> Double {
let basePrice = order.quantity * order.itemPrice
- return order.quantity * order.itemPrice - max(0, order.quantity - 500) * order.itemPrice * 0.05 + min(order.quantity * order.itemPrice * 0.1, 100)
+ return basePrice - max(0, order.quantity - 500) * order.itemPrice * 0.05 + min(basePrice * 0.1, 100)
}
これで、本体価格(basePrice)の抽出が完了した。
このように、
定義 → 処理のコピー → 置き換え → テスト
の順番でリファクタリングをしていく。
では次に数量値引き(quantity Discount)を抽出していく。
まず、定数を定義し、処理をコピーするていく。
func price(order: Order) -> Double {
let basePrice = order.quantity * order.itemPrice
+ let quantityDisount = max(0, order.quantity - 500) * order.itemPrice * 0.05
return basePrice - max(0, order.quantity - 500) * order.itemPrice * 0.05 + min(basePrice * 0.1, 100)
}
次に定数を置き換えていく
func price(order: Order) -> Double {
let basePrice = order.quantity * order.itemPrice
let quantityDisount = max(0, order.quantity - 500) * order.itemPrice * 0.05
- return basePrice - max(0, order.quantity - 500) * order.itemPrice * 0.05 + min(basePrice * 0.1, 100)
+ return basePrice - quantityDisount + min(basePrice * 0.1, 100)
}
これで、数量値引き(quantity Discount)を抽出できた。
最後に、送料(shipping)を抽出していくが、手順は全く同様のことを行うので、割愛する。
func price(order: Order) -> Double {
let basePrice = order.quantity * order.itemPrice
let quantityDisount = max(0, order.quantity - 500) * order.itemPrice * 0.05
+ let shipping = min(basePrice * 0.1, 100)
- return basePrice - quantityDisount + min(basePrice * 0.1, 100)
+ return basePrice - quantityDisount + shipping
}
変数のインライン化
変数は関数内の処理に名前を与える便利な道具である。
しかし、名前が処理そのものを超えて余計な情報を伝えることがある。
さらに、変数が周囲のコードのリファクタリングを妨害することもある。
そのような場合には、変数をインライン化するのが効果的である。
手順
- 代入の右辺に副作用がないか確認する
- その変数が定数と宣言されていなければ、定数にしてテストする:これにより、代入が 1 度しか行われていないことを確認する)
- その変数への最初の参照を探し、代入の右辺と置き換える
- テストする
- 変数を参照している箇所の置き換えを繰り返し、すべての参照箇所を更新する
- 変数の宣言と代入を取り除く
- テストする
例
具体的に先ほど変数抽出した関数を使って、変数のインライン化をしていく。
func price(order: Order) -> Double {
let basePrice = order.quantity * order.itemPrice
let quantityDisount = max(0, order.quantity - 500) * order.itemPrice * 0.05
let shipping = min(basePrice * 0.1, 100)
return basePrice - quantityDisount + shipping
}
まず、送料(shipping)をインライン化していく。
送料(shipping)はすでに定数で宣言されているため、変数への最初の参照を探し、右辺と置き換えていく。
func price(order: Order) -> Double {
let basePrice = order.quantity * order.itemPrice
let quantityDisount = max(0, order.quantity - 500) * order.itemPrice * 0.05
let shipping = min(basePrice * 0.1, 100)
- return basePrice - quantityDisount + shipping
+ return basePrice - quantityDisount + min(basePrice * 0.1, 100)
}
置き換えたらテストをする。
次に、他にも送料(shipping)が使われていないか確認する。
今回の場合はこれ以上使われていないため、置き換えは終了である。
最後に、宣言と代入されていた箇所を削除し、テストをするとインライン化の完了である。
func price(order: Order) -> Double {
let basePrice = order.quantity * order.itemPrice
let quantityDisount = max(0, order.quantity - 500) * order.itemPrice * 0.05
- let shipping = min(basePrice * 0.1, 100)
return basePrice - quantityDisount + min(basePrice * 0.1, 100)
}
これと同様なことを本体価格(basePrice)、数量値引き(quantityDiscount)にも施すと、元の price 関数の状態に戻る。
func price(order: Order) -> Double {
- let basePrice = order.quantity * order.itemPrice
- let quantityDisount = max(0, order.quantity - 500) * order.itemPrice * 0.05
- let shipping = min(basePrice * 0.1, 100)
- return basePrice - quantityDisount + min(basePrice * 0.1, 100)
+ return order.quantity * order.itemPrice - max(0, order.quantity - 500) * order.itemPrice * 0.05 + min(order.quantity * order.itemPrice * 0.1, 100)
}