こんにちは!モジュール分割マスター講座の第3回へようこそ。前回はコードスメルを見つける技術について学びました。今日は実際にコードを改善する「関数レベルのリファクタリング」について学んでいきましょう。
リファクタリングとは何か
リファクタリングとは、プログラムの外部から見た動作を変えずに、内部の構造を改善することです。つまり、同じ機能を保ちながら、コードをより分かりやすく、保守しやすくする作業です。
料理で例えると、「同じ味だけど、作り方をもっと効率的にする」ということですね。
関数抽出の具体的な手順
1. 抽出すべきコードの特定
まず、独立した機能として分離できるコードの塊を見つけます。
例: 以下のコードで「割引計算」の部分を抽出してみましょう。
function calculateTotal(items, customer) {
let total = 0;
// 各商品の合計を計算
for (const item of items) {
total += item.price * item.quantity;
}
// 割引計算
let discount = 0;
if (customer.type === 'regular') {
discount = total * 0.05;
} else if (customer.type === 'gold') {
discount = total * 0.1;
} else if (customer.type === 'platinum') {
discount = total * 0.15;
}
// 最終金額を計算して返す
return total - discount;
}
Code language: JavaScript (javascript)
ここでは「割引計算」の部分(コメント以降の5行)を独立した関数にします。
2. 新しい関数の作成
抽出する部分を新しい関数にコピーします。
function calculateDiscount(total, customerType) {
let discount = 0;
if (customerType === 'regular') {
discount = total * 0.05;
} else if (customerType === 'gold') {
discount = total * 0.1;
} else if (customerType === 'platinum') {
discount = total * 0.15;
}
return discount;
}
Code language: JavaScript (javascript)
3. 必要なパラメータの特定
新しい関数が必要とするパラメータを確認します。この場合は:
total: 合計金額customerType: 顧客タイプ
4. 元の関数を修正
元の関数から抽出した部分を、新しい関数の呼び出しに置き換えます。
function calculateTotal(items, customer) {
let total = 0;
// 各商品の合計を計算
for (const item of items) {
total += item.price * item.quantity;
}
// 割引を計算
const discount = calculateDiscount(total, customer.type);
// 最終金額を計算して返す
return total - discount;
}
Code language: JavaScript (javascript)
5. テスト
リファクタリングが正しく行われたか、元の動作が維持されているかテストします。
変数の整理と名前付けのコツ
変数のスコープを最小化する
変数は使う場所の近くで宣言し、使用範囲を狭めましょう。
改善前:
function processOrder() {
const tax = 0.1;
// 50行のコード...
// ここでやっとtaxを使う
const totalWithTax = subtotal * (1 + tax);
}
Code language: JavaScript (javascript)
改善後:
function processOrder() {
// 50行のコード...
const tax = 0.1;
const totalWithTax = subtotal * (1 + tax);
}
Code language: JavaScript (javascript)
変数名を明確にする
変数名は略語ではなく、目的がはっきりわかる名前をつけましょう。
改善前:
const d = new Date();
const m = d.getMonth();
const res = calculateResult(m);
Code language: JavaScript (javascript)
改善後:
const currentDate = new Date();
const currentMonth = currentDate.getMonth();
const monthlyResult = calculateResult(currentMonth);
Code language: JavaScript (javascript)
一時変数を関数呼び出しに置き換える
何度も使う場合を除いて、一時変数より関数呼び出しの方が明確になることがあります。
改善前:
const basePrice = quantity * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
Code language: JavaScript (javascript)
改善後:
function getBasePrice() {
return quantity * itemPrice;
}
if (getBasePrice() > 1000) {
return getBasePrice() * 0.95;
} else {
return getBasePrice() * 0.98;
}
Code language: JavaScript (javascript)
ただし、この例では関数が複数回呼ばれるので、パフォーマンス上は一時変数を使った方が良い場合もあります。状況に応じて判断しましょう。
パラメータの整理と最適化
多すぎるパラメータを避ける
パラメータが多い関数は理解しにくく使いにくいです。一般的に3〜4個までが理想です。
改善前:
function createUser(name, email, age, address, phone, company, position, startDate) {
// ...
}
Code language: JavaScript (javascript)
改善後:
function createUser(userInfo) {
// userInfo.name, userInfo.email などとしてアクセスする
}
// 呼び出し側
createUser({
name: "田中太郎",
email: "tanaka@example.com",
age: 28,
// ...他のプロパティ
});
Code language: JavaScript (javascript)
フラグパラメータを使わない
真偽値によって関数の振る舞いを変えるフラグパラメータは避け、代わりに明示的に別の関数を作りましょう。
改善前:
function processOrder(order, isRush) {
if (isRush) {
// 特急処理
} else {
// 通常処理
}
}
Code language: JavaScript (javascript)
改善後:
function processOrder(order) {
// 通常処理
}
function processRushOrder(order) {
// 特急処理
}
Code language: JavaScript (javascript)
関数の副作用を減らす
関数は1つのことだけをするのが理想です。データを変更する副作用を持つ関数は、その目的を名前に反映させましょう。
改善前:
function calculateTotal(cart) {
let total = 0;
for (const item of cart.items) {
total += item.price * item.quantity;
}
cart.total = total; // 副作用!
return total;
}
Code language: JavaScript (javascript)
改善後:
function calculateTotal(cart) {
let total = 0;
for (const item of cart.items) {
total += item.price * item.quantity;
}
return total;
}
function updateCartTotal(cart) {
cart.total = calculateTotal(cart);
}
Code language: JavaScript (javascript)
実習:長い関数を意味のあるまとまりに分割する
それでは実際に長い関数を分割する練習をしましょう。以下は注文処理を行う長い関数です:
function processOrder(order) {
// 商品の在庫確認
let allInStock = true;
let outOfStockItems = [];
for (const item of order.items) {
const stockCount = getStockCount(item.productId);
if (stockCount < item.quantity) {
allInStock = false;
outOfStockItems.push(item.productName);
}
}
if (!allInStock) {
return {
success: false,
message: `以下の商品が在庫不足です: ${outOfStockItems.join(', ')}`,
outOfStockItems: outOfStockItems
};
}
// 合計金額の計算
let subtotal = 0;
for (const item of order.items) {
subtotal += item.price * item.quantity;
}
// 割引適用
let discount = 0;
if (order.customer.membershipLevel === 'gold') {
discount = subtotal * 0.1;
} else if (order.customer.membershipLevel === 'silver') {
discount = subtotal * 0.05;
} else if (subtotal > 10000) {
discount = subtotal * 0.03;
}
// 税金の計算
const taxRate = 0.1;
const tax = (subtotal - discount) * taxRate;
// 最終金額
const total = subtotal - discount + tax;
// 在庫数の更新
for (const item of order.items) {
updateStock(item.productId, item.quantity);
}
// 注文情報の保存
const orderRecord = {
id: generateOrderId(),
customerId: order.customer.id,
items: order.items,
subtotal: subtotal,
discount: discount,
tax: tax,
total: total,
date: new Date()
};
saveOrderToDatabase(orderRecord);
// メール送信
const emailContent = `
${order.customer.name} 様、ご注文ありがとうございます。
注文番号: ${orderRecord.id}
合計金額: ${total}円
発送まで今しばらくお待ちください。
`;
sendEmail(order.customer.email, '注文確認', emailContent);
return {
success: true,
message: '注文が完了しました',
orderId: orderRecord.id,
total: total
};
}
Code language: JavaScript (javascript)
ステップ1:機能のまとまりを特定する
この関数には以下の機能のまとまりがあります:
- 在庫確認
- 金額計算(小計、割引、税金、合計)
- 在庫更新
- 注文情報の保存
- 確認メールの送信
ステップ2:各機能を独立した関数に抽出する
まず、在庫確認の部分を抽出します:
function checkStock(items) {
let allInStock = true;
let outOfStockItems = [];
for (const item of items) {
const stockCount = getStockCount(item.productId);
if (stockCount < item.quantity) {
allInStock = false;
outOfStockItems.push(item.productName);
}
}
return {
allInStock,
outOfStockItems
};
}
Code language: JavaScript (javascript)
次に、金額計算の部分を抽出します:
function calculateOrderAmounts(items, membershipLevel) {
// 小計の計算
let subtotal = 0;
for (const item of items) {
subtotal += item.price * item.quantity;
}
// 割引の計算
let discount = 0;
if (membershipLevel === 'gold') {
discount = subtotal * 0.1;
} else if (membershipLevel === 'silver') {
discount = subtotal * 0.05;
} else if (subtotal > 10000) {
discount = subtotal * 0.03;
}
// 税金の計算
const taxRate = 0.1;
const tax = (subtotal - discount) * taxRate;
// 最終金額
const total = subtotal - discount + tax;
return {
subtotal,
discount,
tax,
total
};
}
Code language: JavaScript (javascript)
在庫更新の部分を抽出します:
function updateStockLevels(items) {
for (const item of items) {
updateStock(item.productId, item.quantity);
}
}
Code language: JavaScript (javascript)
注文情報の保存部分を抽出します:
function saveOrder(order, amounts) {
const orderRecord = {
id: generateOrderId(),
customerId: order.customer.id,
items: order.items,
subtotal: amounts.subtotal,
discount: amounts.discount,
tax: amounts.tax,
total: amounts.total,
date: new Date()
};
saveOrderToDatabase(orderRecord);
return orderRecord;
}
Code language: JavaScript (javascript)
確認メール送信部分を抽出します:
function sendOrderConfirmationEmail(customer, orderId, total) {
const emailContent = `
${customer.name} 様、ご注文ありがとうございます。
注文番号: ${orderId}
合計金額: ${total}円
発送まで今しばらくお待ちください。
`;
sendEmail(customer.email, '注文確認', emailContent);
}
Code language: JavaScript (javascript)
ステップ3:元の関数を修正して新しい関数を呼び出す
function processOrder(order) {
// 在庫確認
const stockCheck = checkStock(order.items);
if (!stockCheck.allInStock) {
return {
success: false,
message: `以下の商品が在庫不足です: ${stockCheck.outOfStockItems.join(', ')}`,
outOfStockItems: stockCheck.outOfStockItems
};
}
// 金額計算
const amounts = calculateOrderAmounts(order.items, order.customer.membershipLevel);
// 在庫更新
updateStockLevels(order.items);
// 注文情報の保存
const orderRecord = saveOrder(order, amounts);
// メール送信
sendOrderConfirmationEmail(order.customer, orderRecord.id, amounts.total);
return {
success: true,
message: '注文が完了しました',
orderId: orderRecord.id,
total: amounts.total
};
}
Code language: JavaScript (javascript)
リファクタリング後の効果
リファクタリングによって得られた効果は以下の通りです:
- 読みやすさの向上: 元の関数は60行以上ありましたが、リファクタリング後は20行程度になりました。
- 意図の明確化: 各関数の名前が、その部分が何をするのかを明確に示しています。
- テストのしやすさ: 各機能を独立してテストできるようになりました。
- 再利用性: 例えば
calculateOrderAmountsは他の場所(請求書画面など)でも再利用できます。 - 変更のしやすさ: 例えば割引計算のルールが変わった場合、
calculateOrderAmounts関数だけを修正すれば良いです。
まとめ
今日は関数レベルのリファクタリングについて学びました:
- 関数抽出の手順:
- 抽出すべきコードの特定
- 新しい関数の作成
- 必要なパラメータの特定
- 元の関数の修正
- テスト
- 変数の整理と名前付け:
- スコープを最小化する
- 明確な名前をつける
- 一時変数を関数呼び出しに置き換える
- パラメータの整理:
- 多すぎるパラメータを避ける
- フラグパラメータを使わない
- 関数の副作用を減らす
これらの技術を使うことで、大きな関数を適切なサイズに分割し、コードをより理解しやすく保守しやすくすることができます。
次回の講義では「クラスとモジュールの分割技法」について学びます。関数レベルから一歩進んで、より大きな構造の整理方法を見ていきましょう。
質問はありますか?
ここまでの内容について質問や疑問があれば、ぜひ聞いてください。
皆さん、第3回の講義内容についての質問をありがとうございます。3人の意欲的な学生さんからの質問に答えていきましょう。
学生A(山田さん)の質問
山田さん: 「関数を抽出する際に、その関数が使う変数が多すぎる場合はどうすればいいですか?パラメータがたくさん必要になって、かえって使いにくくなることもありますよね?」
回答: 山田さん、とても鋭い質問ですね。確かに変数が多い部分を抽出すると、パラメータも多くなってしまう問題があります。この場合、いくつかの解決策があります:
- オブジェクトにまとめる: 関連する変数をひとつのオブジェクトにまとめることで、パラメータの数を減らせます。例えば、顧客情報に関する変数が5つあるなら、
customerInfoというオブジェクトにまとめて1つのパラメータにできます。 - クラスのメソッドにする: もし関数が特定のデータに強く依存しているなら、それをクラスのメソッドにすることを検討しましょう。クラスのメソッドならインスタンス変数にアクセスできるので、パラメータを減らせます。
- 関数を分割する: もし関数が異なる目的のために多くの変数を使っているなら、それ自体がひとつの関数にすべきではないサインかもしれません。さらに小さな関数に分割すると、各関数のパラメータ数も減ります。
- クロージャを活用する: 特にJavaScriptでは、外側の関数のスコープにある変数にアクセスできるクロージャを使って、パラメータの受け渡しを減らせることもあります。
例えば、最初のアプローチをコードで示すと:
// 変更前: パラメータが多い
function calculateShipping(weight, height, width, depth, destination, isExpress) {
// ...
}
// 変更後: 関連するものをオブジェクトにまとめる
function calculateShipping(packageDimensions, destination, isExpress) {
// packageDimensions.weight, packageDimensions.height などとして使用
}
Code language: JavaScript (javascript)
大切なのはバランスです。パラメータが多すぎると使いにくいですが、少なすぎるとオブジェクトの中身を理解するのに時間がかかることもあります。経験を積むと、このバランス感覚が身についてきますよ。
学生B(中村さん)の質問
中村さん: 「リファクタリングの最中に機能が壊れないかどうか確認するには、テストが必要だと言われましたが、もともとテストがないコードをリファクタリングする場合はどうすればいいでしょうか?」
回答: 中村さん、実践的な質問をありがとうございます。これは多くの開発者が直面する現実的な問題です。テストがないコードをリファクタリングする場合、次のようなアプローチがあります:
- 先にテストを書く: 理想的には、リファクタリング前にテストを追加することです。これを「レガシーコード救済」と呼びます。まずは現在の動作を保証するテストを書いてから、リファクタリングに取り組みましょう。
- 小さなステップで進める: テストがない場合は特に、一度に大きな変更をするのではなく、小さなステップで進めることが重要です。1つの関数を抽出したら、すぐに動作確認をしましょう。
- 手動テストのチェックリストを作る: 自動テストがなくても、手動で確認すべき項目のリストを作り、各変更後にそれを確認することができます。
- リファクタリング前後の出力比較: 同じ入力に対して、リファクタリング前と後で同じ出力が得られるか確認します。これをいくつかのケースで試してみましょう。
- デバッガーを活用する: デバッガーを使って、リファクタリング前後の変数の変化を追跡し、同じ動作をしているか確認します。
- 段階的なデプロイ: 可能であれば、リファクタリングしたコードを一部のユーザーだけに提供して、問題がないか確認してから全体に適用することも考えられます。
私の経験では、テストがない場合でも「抽出」操作は比較的安全です。元のコードはそのままで、一部を関数化するだけなので、壊れにくいのです。ただし、変数のスコープや副作用には注意が必要です。
長期的には、リファクタリングしながら少しずつテストも追加していくことをお勧めします。これは「テストでコードを覆う」という戦略と呼ばれ、時間をかけて徐々にテスト可能なコードベースに移行する方法です。
学生C(鈴木さん)の質問
鈴木さん: 「関数を小さく分割することの利点はわかりましたが、分割しすぎると関数が多くなりすぎて、かえってコードの流れを追いにくくなることはありませんか?どこまで分割するのが適切なのでしょうか?」
回答: 素晴らしい質問です、鈴木さん。関数分割の「適切な粒度」は、多くのプログラマーが悩む問題です。確かに分割しすぎると、関数の数が増えて全体の流れが見えにくくなる可能性があります。
関数分割の適切な粒度を判断するためのガイドラインをいくつか紹介します:
- 単一責任の原則: 1つの関数は1つの責任だけを持つべきです。「この関数は何をするものか」という問いに、「AとBとCをする」と答えるなら分割候補です。「Aをする」と一言で答えられるなら適切な粒度かもしれません。
- 抽象化レベルの一貫性: 1つの関数内の処理は同じ抽象度のものであるべきです。例えば「顧客情報の取得」と「XMLのパース」のような異なるレベルの処理が混ざっていたら分割すべきです。
- 再利用の可能性: その処理が他の場所でも使われる可能性があるなら、独立した関数にするメリットがあります。
- 読みやすさの向上: 分割後に元のコードが読みやすくなるかどうかが重要な判断基準です。分割してもコードが理解しやすくならないなら、分割する意味が薄いです。
- 名前付けの明確さ: 切り出す関数に明確で簡潔な名前がつけられないなら、それは適切な分割ポイントではないかもしれません。
実践的な目安としては、以下のようなものがあります:
- 関数は通常、画面に収まるサイズ(20〜30行程度)が読みやすい
- 1クラスあたり10〜20個程度の関数が管理しやすい目安
- 深い入れ子になった条件分岐は、独立した関数に抽出すると読みやすくなる
また、関数が多くなった場合の対処法として:
- 関連する関数をクラスやモジュールにまとめる
- 高レベルの関数(何をするか)と低レベルの関数(どうやるか)を分ける
- 良い命名規則を使って関数の目的を明確にする
- ドキュメントやコメントで全体の流れを説明する
最終的には経験とチームの好みによる部分もあります。マーティン・ファウラーは「リファクタリング」の中で「短い関数のほうが長い関数より優れている」と述べていますが、極端に細かく分割するのも避けるべきです。バランスが大切です。
まとめ
皆さん、素晴らしい質問をありがとうございました。関数分割には技術的な側面だけでなく、判断や経験も必要です。今日議論したポイントをまとめると:
- 多くのパラメータが必要な場合は、オブジェクトにまとめたり、クラスのメソッドにすることを検討する
- テストがないコードでは、小さなステップで進め、各ステップで動作確認をする
- 関数分割は単一責任の原則に従い、読みやすさを向上させる目的で行う
- 分割しすぎないよう、全体の構造と流れを意識する
次回の「クラスとモジュールの分割技法」では、より大きな単位での構造化について学びます。関数を適切にグループ化することで、今回議論したような「関数が多すぎる」問題も解決できるでしょう。
何か他に質問があれば、いつでも聞いてくださいね。