リファクタリングとは「ソースコードを読みやすく、保守しやすいように整理すること」です。
大きなソフトウェアを作っていると、時間とともにソースコードが長く複雑になっていきます。
複雑なソースコードは読みにくく、修正が難しくなります。
修正しにくいソースコードは、開発の遅れやバグの原因になります。
このような事態を防ぐため、ソースコードを整理するのがリファクタリングです。
リファクタリングはいつやる?
リファクタリングはソフトウェアの開発中は常にやります。
プログラミングを続けているとコードはどんどん複雑になります。
部屋の掃除と同じで、複雑になってからリファクタリングすると時間がかかってしまいます。
そのため、優れたエンジニアはコーディングをしながら同時並行でリファクタリングを進めます。
リファクタリングは呼吸するようにやり続けるべきものなのです。
とはいえ、いきなりコーディングとリファクタリングを同時にやれといわれても難しいかもしれません。
そのときは、以下のタイミングでリファクタリングをするのが推奨されています。
- 機能を追加するとき
- ソースコードの中身を理解するためにコードリーディングをするとき
- 小さなゴミ(ソースコードの良くない部分)を発見した時
1. 機能を追加するとき
新しい機能を追加するタイミングは、リファクタリングをする絶好のチャンスです。
新しい機能を追加するには、既存機能の実装を理解しなければなりません。
リファクタリングをすればソースコードが整理されて読みやすくなり、既存機能の理解が進みます。
その結果、新しい機能を追加しやすくなります。
したがって、機能追加のタイミングでリファクタリングをするべきです。
2. コードリーディングをするとき
コードリーディングのタイミングは、リファクタリングに適しています。
リファクタリングは、コードの理解につながります。
機械的にリファクタリングをしたとしても、結果としてコードが読みやすくなります。
リファクタリングをしながらコードリーディングをすれば、コードを理解できるだけでなく、既存のコードが整理されて保守性が高まるので、一石二鳥です。
3. 小さなゴミを発見した時
ソースコードの中に「小さなゴミ」を発見したときにリファクタリングをします。
「小さなゴミ」とは「コードの重複」や「長すぎるメソッド」など、ソースコードの理解や修正を難しくしている部分を指します。
このようなコードを見つけたとき、そのまま放置しているとコードはどんどん汚くなります。
そうならないために「小さなゴミ」を見つけ次第、リファクタリングをしましょう。
リファクタリングで絶対にやってはいけないこと
リファクタリングでは、既存のソースコードを整理しますが、ソフトウェアの機能を破壊してはいけません。
リファクタリングを始めると、ソースコードに大規模な修正を加えなければいけないことがしばしばあります。
しかし、大規模な修正の結果、機能が使えなくなったり、新たなバグが発生したら困ります。
そのため、既存の機能を絶対に壊さないように注意しながらリファクタリングを進めなければいけません。
リファクタリングの前にやるべきこと
リファクタリングでは、既存機能を壊してはいけません。
既存機能を壊さないために、リファクタリングを始める前にテストコードを書きます。
テストコードとは、ある機能が期待通りに動くことをチェックするためのコードです。
たとえば、こんな関数があるとしましょう。
引数のaとbを合計して返すだけの関数です。
def sum(a, b):
return a + b;
この関数が正しく実装されていることをチェックするテストコードを書いてみましょう。
def test_sum():
assert(sum(1, 1) == 2) # 引数に1と1が与えられたときに正しく2を返せるか
assert(sum(-1, 1) == 0) # 引数に-1と1が与えられたときに正しく0を返せるか
assert(sum(-1.5, 2.5) == 1) # 引数に-1.5と2.5が与えられたときに正しく1を返せるか
assert
は「引数に与えた式が偽のときにAssertionError
を送出する関数」です。
ここで作成したtest_sum
関数を使って、sum
関数をテストします。
sum
関数が間違って実装されていれば、test_sum
関数はエラーを出します。
以下では、あえてsum
関数の実装を間違えています。
def sum(a, b):
return a - b; # 間違って実装されている
def test_sum():
assert(sum(1, 1) == 2)
assert(sum(-1, 1) == 0)
assert(sum(-1.5, 2.5) == 1)
test_sum()
'''
Traceback (most recent call last):
File "main.py", line 11, in <module>
test_sum()
File "main.py", line 7, in test_sum
assert(sum(1, 1) == 2)
AssertionError
'''
AssertionError
が送出されましたね。
このようにtest_sum
関数を実行すれば、sum
関数が間違っているかいないか(少なくとも記述した3つのパターンを期待通りに処理できるかどうか)を確認できます。
def sum(a, b):
return a + b; # 正しく実装されている
def test_sum():
assert(sum(1, 1) == 2)
assert(sum(-1, 1) == 0)
assert(sum(-1.5, 2.5) == 1)
test_sum() # 何もエラーを出さない
ソフトウェアを納品する前にtest_sum
関数を実行すれば、sum
関数が壊れていないことをチェックできます。
これがテストコードの役割です。
リファクタリングには、うっかり機能を壊してしまうリスクがありますが、事前にテストコードを準備して、リファクタリング後に実行すれば、機能を壊していないことをチェックできます。
リファクタリングの手順
リファクタリングは以下の手順で行います。
- リファクタリングすべきコードを見つける
- テストコードを書く
- ソースコードを修正する
- テストコードを実行して機能を壊していないことを確認する
1. リファクタリングすべきコードを見つける
まずはリファクタリングすべきコードを見つけます。
リファクタリングすべきコードは「コードの臭い(Code Smell)」として知られています。
「コードの臭い」には以下の例があり、これらのコードは積極的にリファクタリングすべきとされています。
- 重複したコード
- 長すぎるメソッド
- 巨大なクラス
- 機能の横恋慕
- 不適切な関係
- 相続拒否
- 怠け者クラス
- 重複メソッド
- 不自然な複雑さ
これらの詳細は「リファクタリング(第2版)既存のコードを安全に改善する」という有名な本に書かれているので参照してください(これらの一部については、後述します)。
「重複したコード」や「長すぎるメソッド」「巨大なクラス」等を発見したら、それがリファクタリングの出発点になります。
2. テストコードを書く
リファクタリングすべきコードを見つけたら、テストコードを書きます。
テストコードを書くことによって、リファクタリングで機能を壊してしまったとしても、すぐに気付くことができます。
3. ソースコードを修正する
ソースコードを修正します。
具体的な修正方法については後述します。
「コードの臭い」によって修正方針が決まります。
4. テストコードを実行する
ソースコードを修正したら、テストコードを実行します。
テストコードがエラーを吐くなら、リファクタリングによってどこかの機能を壊したということです。
エラーを吐いた場合は、リファクタリングした部分を再修正します。
テストコードをすべて通過したら、リファクタリングは完了です。
リファクタリングの具体例
リファクタリングの具体例を示します。
ここで挙げるのは、実際のコードにもよく出現するパターンです。
「どこをリファクタリングしていいかわからない」「リファクタリングすべきコードはあるけど修正方法がわからない」ときの参考にしてください。
例1:重複したコードのリファクタリング
巨大なプログラムには、重複したコードが頻出します。
以下は、複数の関数に同様の処理を書いているパターンです。
def calc_total_cost_of_chocolate(number):
price_of_chocolate = 200
total_cost = price_of_chocolate * number
return total_cost
def calc_total_cost_of_cookie(number):
price_of_cookie = 100
total_cost = price_of_cookie * number
return total_cost
def calc_total_cost_of_coffee(number):
price_of_coffee = 300
total_cost = price_of_coffee * number
return total_cost
calc_total_cost_of_chocolate(2)
calc_total_cost_of_cookie(3)
calc_total_cost_of_coffee(4)
3つの関数で「商品の値段(price_of_xxx)と個数(number)の積を計算して返す」という処理が重複しています。
重複したコードは、ひとつにまとめると見通しがよくなります。
重複をひとつにまとめるために、それぞれの共通部分と相違した部分を整理します。
ここでは「商品の値段と個数の積を計算する」部分は共通していますが「price_of_xxx」の値がそれぞれの関数で異なります。
相違した部分である「price_of_xxx」を引数で受け取れるように修正しましょう。
def calc_total_cost_of_chocolate(price_of_chocolate, number):
total_cost = price_of_chocolate * number
return total_cost
def calc_total_cost_of_cookie(price_of_cookie, number):
total_cost = price_of_cookie * number
return total_cost
def calc_total_cost_of_coffee(price_of_coffee, number):
total_cost = price_of_coffee * number
return total_cost
PRICE_OF_CHOCOLATE = 200
PRICE_OF_COOKIE = 100
PRICE_OF_COFFEE = 300
calc_total_cost_of_chocolate(PRICE_OF_CHOCOLATE, 2)
calc_total_cost_of_cookie(PRICE_OF_COOKIE, 3)
calc_total_cost_of_coffee(PRICE_OF_COFFEE, 4)
それぞれの商品の価格を引数で受け取れるように変更しました。
また、商品の価格はコード中で変更されるべきでないため定数におきます。
この修正によって、重複した処理(total_cost = price_of_xxx * number
)が際立って見えるようになりました。
書き換えたコードを見ると、関数名と引数名が異なるだけで、それぞれの関数のロジックは全く同じであることがわかります。
それでは、これらをひとつの関数にまとめましょう。
ここではcalc_total_cost
という新たな関数にまとめます。
def calc_total_cost(price, number):
total_cost = price * number
return total_cost
PRICE_OF_CHOCOLATE = 200
PRICE_OF_COOKIE = 100
PRICE_OF_COFFEE = 300
calc_total_cost(PRICE_OF_CHOCOLATE, 2)
calc_total_cost(PRICE_OF_COOKIE, 3)
calc_total_cost(PRICE_OF_COFFEE, 4)
コードの行数が減って、ずいぶんと見通しがよくなりました。
最後に、関数の戻り値を修正します。
calc_total_cost
関数は return total_cost
のように戻り値を返していますが、関数名を見れば戻り値が「total_cost」であることは明らかです。
そのため、あえて戻り値を変数に置く必要がありません。
次のように修正できます。
def calc_total_cost(price, number):
return price * number
PRICE_OF_CHOCOLATE = 200
PRICE_OF_COOKIE = 100
PRICE_OF_COFFEE = 300
calc_total_cost(PRICE_OF_CHOCOLATE, 2)
calc_total_cost(PRICE_OF_COOKIE, 3)
calc_total_cost(PRICE_OF_COFFEE, 4)
ずいぶんとすっきりしましたね。
重複したコードをひとつにまとめたことで、コードが短く、保守しやすくなりました。
例2:長すぎる関数のリファクタリング
試行錯誤しながらコードを書いていると、関数やメソッドは長くなりがちです。
関数やメソッドが長いと感じたらリファクタリングをすべきタイミングです。
以下は、WEBサイトからHTMLデータを取得・解析して、テキストを取り出し、単語に分けてcsvファイルに保存するPythonの関数です。
import requests
from bs4 import BeautifulSoup
import csv
def save_website_text():
# WEBサイトデータの取得
url = "https://example.com"
response = requests.get(url)
html_data = response.text
# HTMLデータの解析
soup = BeautifulSoup(html_data, 'html.parser')
# CSVファイルの作成
csv_filename = f"{soup.title.string}.csv"
with open(csv_filename, mode='w', newline='') as f:
writer = csv.DictWriter(
f,
fieldnames=['Words']
)
# HTMLのpタグのテキストを単語に分解してCSVファイルに書き込む
for word in soup.find('p').text.split(" "):
writer.writerow({'Words': word})
save_website_text()
save_website_text
関数にWEBサイトの取得・解析とテキストデータの保存機能を書いています。
ひとつの関数に複数の機能が実装されており、保守性が低下しています。
このような「長すぎる関数」はいくつかの関数に分割しましょう。
この関数は、以下の機能を含んでいます。
- WEBサイトデータの取得
- HTMLデータの解析
- 解析結果のCSVファイルへの保存
これらをそれぞれ別の関数に切り出します。
切り出すときは、まずは内部関数として関数化します。
内部関数とは、関数の中の関数のことです。
たとえば、WEBサイトデータの取得部分は以下のようにして内部関数にします。
Before
def save_website_text():
# WEBサイトデータの取得
url = "https://example.com"
response = requests.get(url)
html_data = response.text
After
def save_website_text():
# 内部関数にまとめる
def fetch_website():
# WEBサイトデータの取得
url = "https://example.com"
response = requests.get(url)
return response.text
html_data = fetch_website() # 内部関数を呼び出す
このようにして機能ごと関数に切り出すと以下のようになります。
import requests
from bs4 import BeautifulSoup
import csv
def save_website_text():
def fetch_website():
# WEBサイトデータの取得
url = "https://example.com"
response = requests.get(url)
return response.text
def parse_html(html_data):
# HTMLデータの解析
return BeautifulSoup(html_data, 'html.parser')
def to_csv(soup):
# CSVファイルの作成
csv_filename = f"{soup.title.string}.csv"
with open(csv_filename, mode='w', newline='') as f:
writer = csv.DictWriter(
f,
fieldnames=['Words']
)
# HTMLのpタグのテキストを単語に分解してCSVファイルに書き込む
for word in soup.find('p').text.split(" "):
writer.writerow({'Words': word})
html_data = fetch_website()
soup = parse_html(html_data)
to_csv(soup)
save_website_text()
WEBサイトのデータを取得するメソッド(fetch_website)、HTMLデータを解析するメソッド(parse_html)、CSVにデータを保存するメソッド(to_csv)の3つに分割しました。
それぞれの機能の境界が明確になりました。
ただし、to_csvメソッドはまだ分割の余地があります。
「csvに書き込む処理」と「テキストを単語に分割する処理」が混在しているので、これらを分割します。
# CSVファイルの作成
def to_csv(soup):
# HTMLタグを指定して単語を抽出
def extract_words(tag, soup):
return soup.find(tag).text.split(" ")
# CSVの特定の列にデータを書き込む
def write_rows(filename, fieldname, rows):
with open(filename, mode='w', newline='') as f:
writer = csv.DictWriter(
f,
fieldnames=[fieldname]
)
for row in rows:
writer.writerow({fieldname: row})
csv_filename = f"{soup.title.string}.csv"
write_rows(
csv_filename,
"Words",
extract_words("p", soup)
)
最後にファイル全体で共通して利用できそうなメソッドを内部関数から外に移動して整理します。
整理したものが以下です。
import requests
from bs4 import BeautifulSoup
import csv
def fetch_website(url):
return requests.get(url).text
def parse_html(html_data):
return BeautifulSoup(html_data, 'html.parser')
def extract_words(tag, soup):
return soup.find(tag).text.split(" ")
def to_csv(filename, fieldname, rows):
with open(filename, mode='w', newline='') as f:
writer = csv.DictWriter(
f,
fieldnames=[fieldname]
)
for row in rows:
writer.writerow({fieldname: row})
def save_website_text(url):
html_data = fetch_website(url)
soup = parse_html(html_data)
to_csv(
f"{soup.title.string}.csv",
"Words",
extract_words("p", soup)
)
save_website_text("https://example.com")
長い関数が機能ごとに分割されました。
コードは長くなっていますが、それぞれの関数の責務をはっきりさせたことで、保守性が高まっています。
このように「長すぎる関数」は小さな関数に分割することで読みやすく、保守しやすくなります。
リファクタリングの本
リファクタリングの基本は以下の本で詳しく学べます。
リファクタリングの名著であり、ソフトウェアエンジニアなら一度は読むべきです。
※以下はアフィリエイトリンクではありませんのでご安心ください。
まとめ
リファクタリングとは、「ソースコードを読みやすく、保守しやすいように整理すること」です。
リファクタリングでは、既存機能を壊さないように注意しなければなりません。
万が一、既存機能を壊してしまっても気付けるように事前にテストコードを書きましょう。
また、リファクタリングは開発中は常に行うのがベターですが、「コードの臭い(Code Smell)」を発見したら必ず実施します。
リファクタリングはやればやるほど上達し、よりよいコードを書けるようになります。
リファクタリングの習慣を身に付けて、いいコードを書けるエンジニアになりましょう。