リファクタリング実践
以上でリファクタリングの概要を押さえたら、早速実践といきましょうか。低品質コードをリファクタリングしてみましょう。以下は、オンラインショップの商品とその価格に関するコードの例です(C++)。2つの異なる製品カテゴリー(Tシャツとジーンズ)の合計価格と平均価格を計算し、これらの情報を標準出力に表示しています。
#include <iostream>
#include <vector>
#include <map>
int main() {
std::map<std::string, std::vector<double>> products = {
{"T-Shirts", {29.99, 34.99, 24.99, 19.99}},
{"Jeans", {49.99, 59.99, 69.99, 79.99}}
};
// T-Shirtsの合計価格と平均価格を計算
double totalTShirts = 0.0;
for (double price : products["T-Shirts"]) {
totalTShirts += price;
}
double averageTShirts = totalTShirts / products["T-Shirts"].size();
// Jeansの合計価格と平均価格を計算
double totalJeans = 0.0;
for (double price : products["Jeans"]) {
totalJeans += price;
}
double averageJeans = totalJeans / products["Jeans"].size();
std::cout << "Total price of T-Shirts: $" << totalTShirts << std::endl;
std::cout << "Average price of T-Shirts: $" << averageTShirts << std::endl;
std::cout << "Total price of Jeans: $" << totalJeans << std::endl;
std::cout << "Average price of Jeans: $" << averageJeans << std::endl;
return 0;
}
コンパイルして実行する場合は、C++11のオプションをつけてください。
$ g++ -std=c++11 -o main main.cpp
ステップ1:コードレビュー
まずはコードを読んでみてください。このコードのどこに問題があり、どんな改善をすべきでしょうか?
正直に言うと、このぐらい小さなプログラムであれば、実際にはリファクタリングを行う価値はほとんどありません。全体を把握するのに、大した時間はかかりませんし、冗長と言っても製品は2つだけです。今回は記事の都合上、上記のソースコードを題材にリファクタリングしていきます。読者の皆様は、上記のコードが大規模なコードベースの一部であったり、将来的には扱う商品数が拡大していったりというような想定のもと、以下のリファクタリングにお付き合いいただければと思います。
いかがでしょう? すぐに思いつくこととしては、Tシャツ、ジーンズ、と、商品ごとの合計価格や平均価格を計算する部分が冗長に記述されていることですね。このロジックを関数化して切り出せば、似たようなコード(重複コード)を削減でき、メンテナンス性・可読性が上がりそうです。また、そもそも現状の設計では製品数が増えて来たら対応できません。製品の合計や平均を計算するというような基本的な処理は、このプログラムに限らずプロジェクト全体で度々使う可能性があります。別の関数として抽出することで再利用性を高めることもできます。
指摘の箇所としては同じになりますが、単一責任の原則(Single Responsibility Principle)という別の観点からも、オリジナルコードには問題があります。main関数がデータの初期化、処理、出力という複数の責任を持っています。これらの責任を分割することで、各部分を独立して変更・拡張できるようになります。
以上で、リファクタリングの基本線は見えてきました。合計と平均価格を計算するための関数を定義して、main関数から切り出してシンプルにするとともに、今後、他のコードから再利用できるようにしてみましょう。
ステップ2:関数の抽出
まずは、main関数には手をつけずに、合計価格と平均価格を計算する関数を実装してみましょう。コード例は以下のようになります。
#include <iostream>
#include <vector>
#include <map>
// ステップ2: 商品価格の合計と平均を計算する関数の実装
std::pair<double, double> calculateTotalAndAverage(const std::vector<double>& prices) {
double total = 0.0;
for (double price : prices) {
total += price;
}
double average = prices.empty() ? 0.0 : total / prices.size();
return {total, average};
}
int main() {
// 省略: オリジナルのコードと同じ
}
製品の合計価格と平均価格を計算する簡単な関数CalculateTotalAndAverageを実装しました。12行目がややわかりにくいかもしれませんが、価格のリストが空でないかを確認し、空でない場合だけ合計価格をリストの要素数で割って平均価格を計算しています。つまり、リストが空の際に0で割り算されて例外が発生することを防いでいます。
ここでは三項演算子と呼ばれるものを使っています。三項演算子の使い方は以下の通りです。
条件 ? 真の場合に評価される式 : 偽の場合に評価される式;
関数名や変数名は、チームの別のメンバーが読むことを意識して、わかりやすいものをつけましょう。実は筆者は関数名・変数名を考えるのが苦手なのですが、そんな時こそChatGPT の出番です。仕様を入力して、あるいはコードを直接貼り付けて、「適切な関数名を提案して」とお願いするといい感じの名前が出てきます。お試しあれ。
ここで一度、コードをコンパイルして生成された実行形式ファイルを実行してみます。エラーや警告なくコンパイルが完了し、オリジナルと同じ出力結果が得られることを確認しましょう。先に述べたとおり、リファクタリングとは「プログラムの外部から見た動作を変えずにソースコードの内部構造を整理すること」です。プログラムの外部から見た動作が変わっていないかどうか確認するため、リファクタリングの際には、コードに変更を加える毎にコンパイルを通し、テストを行うことを習慣化してください。
