こんにちは。開発課のtaku_76です。
最近業務で機能開発をしているときに、要件として実現する内容が単純であっても手を加えるコードが複雑であったため機能改修に時間がかかることがありました。
そこからリファクタリングの意識が強くなったため、社内で行われたリファクタリングの輪読会に参加したり、個人的に書籍を読んだりしているので今回はリファクタリングの基礎について記事を書こうと思います。
リファクタリングとは
リファクタリングとは、ソフトウェアの外部の振る舞いを保ったままで内部の構造を改善していくことです。
リファクタリングによってコードの可読性が上がったり、改修時のコードの変更を容易にしたりすることができます。
可読性が高まることで、設計時や実装、テストなどの工程の助けにもなりますので結果的に今後の開発にも役立ちます。
リファクタリングのメリット
リファクタリングを行うメリットとして次のようなことがあります。
可読性の向上
リファクタリングを行うことで可読性が上がり、ソフトウェアを理解しやすくなります。
リファクタリングを行う前はコードを読むのに時間がかかったり、意図のわからないコードがあり理解が困難なことがあります。
それらに対してリファクタリングを行うことでコードの目的がわかるようになり、実現したいことを明確に表現することができます。
コードの変更が容易になる
整理されたコードは変更が簡単に行えます。
仮に重複コードがあった場合、同じ変更を複数箇所で行う必要があります。
しかしリファクタリングによって重複コードを除いておくことで一箇所の変更だけで済み、修正漏れなどの心配もありません。
また、複雑な条件分岐が存在している場合は1つ条件を加えることの難易度が上がります。
複雑な条件分岐は、意図しないデグレを発生される危険性があります。
開発スピードが向上する
リファクタリングによって内部設計が優れているコードは、新規開発時にどこを変更すれば良いかすぐ判断ができます。
また、うまくモジュール化されているとコードを修正するために理解する箇所が限定されます。
機能開発を進めていく中で、テスト時にバグが見つかったとしてもデバッグが容易ですぐ対応することもできます。
このように開発時に無駄なことが省かれるので、開発スピードを向上することに繋がります。
リファクタリングの対象
リファクタリングの対象をいくつか紹介します。
他にも様々なパターンがありますが、コードを読むときに記載しているようなことがあればリファクタリングを行うきっかけとなります。
わかりにくい名前
コードの理解を進めるために大切なのは適切な名前付けです。
そのためクラス、関数、変数などに意図のわからない名前や、名前と異なる処理が混じっている場合には変更する必要があります。
また、良い名前が思いつかない時は設計が固まっていない可能性がありますので設計を見直しましょう。
重複コード
同じコードの構造が複数存在している場合は、1箇所にまとめることでコードが改善されます。
重複コードがあると、コピーされた箇所に出くわすたびに、差分がないか注意する必要があります。
そして修正時には重複部分をもれなく同様に修正しなければなりません。
変更可能なデータ
変更可能なデータは予期せぬ挙動や、厄介なバグを引き起こす原因となりやすいです。
仕様変更で処理が変わったときに意図しない値に書き変わる可能性もあるため、設計時に可変にすべきか不変にすべきか注意が必要です。
長い関数
周知の事実ですが、関数が長くなればなるほどコードの理解が難しくなります。
長いコードを見つけた時は関数として切り出せる処理がないか確認しましょう。
その中でパラメータや一時変数が多すぎる関数は、関数を切り出してもその分だけパラメータの受け渡しが必要になりますので先に一時変数を減らす必要があります。
基本的なリファクタリングの紹介
リファクタリングの手法は書籍で多く紹介されています。
今回は簡単な例ですが基本的なリファクタリングと条件分岐のリファクタリングをいくつか紹介します。
※考え方を重視しているのでクラス設計は考慮していません。
関数として切り出す
処理ごとのまとまりを独立した関数として切り出します。(関数名に注意)
コードを読んでいて何をしているのか調べなければならない箇所があるのなら、目的を示す名前で関数として抽出するべきです。
関数にすることで目的がすぐ分かるため中身を細かく気にする必要がなくなります。
JavaScriptで以下に簡単な例を示します。
function printOwing(invoice) { printBanner(); let unpaidMoney = calculateUnpaidMoney(); // 明細の印字 console.log(`name: ${invoice.customer}`); console.log(`amount: ${unpaidMoney}`); }
上記の関数で、コメントで「明細の印字」と補足している箇所があります。
このような何をするかを説明したコメントで始まるコードが見つかったときは、必要に応じて関数に切り出すことでコードが見やすくなります。
function printOwing(invoice) { printBanner(); let unpaidMoney = calculateUnpaidMoney(); printInvoiceDetails(unpaidMoney); function printInvoiceDetails(unpaidMoney) { console.log(`name: ${invoice.customer}`); console.log(`amount: ${unpaidMoney}`); } }
関数を切り出す際の注意点として、目的にふさわしい命名をしなければ逆に理解しにくいコードのとなるので命名には注意が必要です。
また、逆に関数にすることで分かりにくくなってしまっているコードに関しては関数を取り除いてインライン化を行う場合もあります。
条件の分解
複雑な条件の処理は、プログラムを複雑にする原因の一つです。
様々な条件に応じて処理をするコードを書くだけで、長い関数となり読みにくくなります。
その結果、そのコードの「意図」を理解するのが難しくなります。
解決策として、必要に応じて意図に沿った名前の関数の呼び出しに置き換えることで意図を明確にできます。
条件分岐の場合は、条件判定と条件ごとの処理をそれぞれ関数に置き換えることがおすすめされています。
JavaScriptで例として以下のような、土日だけ割引される料金計算があるとします。
if (days[today.getDay()] == "土曜日" || days[today.getDay()] == "日曜日") { price = quantity * plan.specialRate; } else { price = quantity * plan.regularRate + plan.regularServicePrice; }
まず、曜日判定の条件記述を抽出します。
if (specialDayOfWeek()) { price = quantity * plan.specialRate; } else { price = quantity * plan.regularRate + plan.regularServicePrice; } function specialDayOfWeek() { return days[today.getDay()] == "土曜日" || days[today.getDay()] == "日曜日"; }
次に、then節を関数に抽出します。
if (specialDayOfWeek()) { price = specialPrice(); } else { price = quantity * plan.regularRate + plan.regularServicePrice; } function specialDayOfWeek() { return days[today.getDay()] == "土曜日" || days[today.getDay()] == "日曜日"; } function specialPrice() { return quantity * plan.specialRate; }
最後にelse節を関数に抽出します。
if (specialDayOfWeek()) { price = specialPrice(); } else { price = regularPrice(); } function specialDayOfWeek() { return days[today.getDay()] == "土曜日" || days[today.getDay()] == "日曜日"; } function specialPrice() { return quantity * plan.specialRate; } function regularPrice() { return quantity * plan.regularRate + plan.regularServicePrice; }
好みもあると思いますが、金額計算を参考演算子にしてもよいかもしれません。
price = specialDayOfWeek() ? specialPrice() : regularPrice();
このように修正することで、金額は特別な曜日だったら割引され、そうでなければ通常価格であると直感で分かるかと思います。
条件の統合
複数の条件判定がありそれぞれ条件は異なりますが、結果が同じ場合があります。
このような条件記述は単一の結果を返す条件判定に統合します。
条件を統合するメリットは以下2つあります。
- 複数の判定をまとめることで、行っている判定が1つであるという意図を明示できる
- 条件判定を抽出して関数としてまとめることができる
注意点として、条件判定を統合しても他箇所に影響がないかを事前に確認する必要があります。
また、複数の判定が別々のもので単一の判定としてまとめることで可読性が落ちるようならこのリファクタリングは行いません。
以下過程は割愛しますが、JavaScriptで簡単な例を示します。
if (player.accountLevel < 100) return 0; if (player.loginPeriod < 100) return 0;
それぞれ条件結果が同じなので、条件判定を取り出し論理演算子を使って統合します。
結果として得られた条件判定を関数化することで判定は1つである意図が明示できます。
if (noBonusAccount()) return 0; function noBonusAccount() { return player.accountLevel < 100 || player.loginPeriod < 100; }
ガード節による置き換え
条件には以下2つの形式があります。
- then節とelse節の両方が正常動作
- 正常動作と例外的な動作
例外的な動作に対しては、成立した時点でリターンすることをガード節と呼びます。
ガード節を使用することで主要な処理を明確に伝えることができます。
また、コード上ではネストを減らすことができるので可読性の向上につながります。
function getPayAmount() { let result; if (isDead) { result = deadAmount(); } else { if (isSeparated) { result = separatedAmount(); } else { if (isRetired) { result = retireAmount(); } else { result = normalPayAmount(); } } } return result; }
deadAmount()とseparatedAmount()とretireAmount()は例外的な動作として扱われているため、成立した段階でreturnするように修正します。
function getPayAmount() { if (isDead) return deadAmount(); if (isSeparated) return separatedAmount(); if (isRetired) return retireAmount(); return normalPayAmount(); }
最後に
今回はリファクタリングの初歩ということで概要と例の紹介をしました。
読んだ本の内容をすぐにすべて反映するということはできませんが、どうすれば可読性の向上、変更を容易にできるか常に意識して機能改修していきたいと思います。
参考書籍
エンジニア中途採用サイト
ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
ご興味ありましたら是非ご確認をお願いします。
https://career-recruit.rakus.co.jp/career_engineer/
カジュアル面談お申込みフォーム
どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
以下フォームよりお申込みください。
rakus.hubspotpagebuilder.com
ラクスDevelopers登録フォーム
https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/
イベント情報
会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください!
◆TECH PLAY
techplay.jp
◆connpass
rakus.connpass.com