一時変数は、ソフトウェアコードにおける不必要な複雑さの最も一般的な原因の一つです。長いメソッドに蓄積され、計算された値に曖昧な名前が付けられ、保持されているロジックの抽出、テスト、再利用が困難になります。Martin Fowler が著書『Refactoring: Improving the Design of Existing Code』で紹介している「Replace Temp with Query」リファクタリングは、この問題に直接対処します。計算された値をローカル変数に格納する代わりに、計算を名前付きメソッドであるクエリに抽出し、値が必要な場所でそれを呼び出します。
その結果、意図を隠すのではなく、意図を伝えるコードが生まれます。計算処理はもはや長いメソッドの先頭にある変数代入の中に埋もれることなく、名前と場所を持ち、単独でテストできるようになります。この記事では、このテクニック全体、一時変数とは何か、一時変数が問題となる場合、Java、Python、TypeScriptでリファクタリングを段階的に実行する方法、適用すべき場合とそうでない場合、そしてリファクタリングカタログの関連テクニックとの関連性について解説します。
プログラミングにおける一時変数(Temp)とは何ですか?
一時変数(一般にtempと呼ばれる)は、関数またはメソッド内のローカル変数であり、同じスコープ内で使用する中間結果を格納します。これは一度計算され、名前付き変数に格納され、同じ関数内で後で参照されます。この変数は関数呼び出しの有効期間中のみ存在し、関数外からはアクセスできず、オブジェクトの状態にも格納されません。
パイソン
# Python: base_price is a temp variable
def calculate_total(quantity, item_price):
base_price = quantity * item_price # temp: computed once, used below
if base_price > 1000:
return base_price * 0.95
return base_price * 0.98
ジャワ
// Java: basePrice is a temp variable
double basePrice = quantity * itemPrice; // temp
if (basePrice > 1000) {
return basePrice * 0.95;
}
return basePrice * 0.98;
タイスクリプト
// TypeScript: basePrice is a temp variable
const basePrice = quantity * itemPrice; // temp
if (basePrice > 1000) return basePrice * 0.95;
return basePrice * 0.98;
一時変数自体は必ずしも悪いものではありません。正当な用途もあります。例えば、繰り返し実行するのが無駄な高コストな処理の結果を取得したり、複雑な複数ステップの計算を読みやすい段階に分割したり、ループの繰り返し処理で蓄積される値を保持したりする場合などです。問題となるのは、名前付きメソッドとして記述した方が分かりやすい単純な派生値に対して一時変数が反射的に使用された場合、あるいは長いメソッド内で一時変数が蓄積され、読者が同時に複数のアクティブな中間値を追跡する必要が生じた場合です。
ソフトウェアエンジニアリングにおけるリファクタリングとは何か?
リファクタリングとは、既存のコードの目に見える動作を変更せずに、その構造を再構築するプロセスです。その目的は、コードの内部品質、つまり可読性、テスト容易性、保守性、モジュール性を向上させることです。リファクタリングは機能を追加したり、バグを修正したりするものではなく、コードの動作を維持しながら構造を変更するものです。
「Temp を Query に置き換える」は、マーティン・ファウラーが解説した数十種類のテクニックのうちの 1 つです。これは、長くなりすぎたり複雑になりすぎたりしたメソッドに対処するテクニック群に属します。
| リファクタリング手法 | それは何をする |
|---|---|
| TempをQueryに置き換える | 一時変数の計算を名前付きメソッドに抽出します。 |
| 抽出方法 | コードブロックを新しい名前付きメソッドに抽出します。 |
| インライン温度 | 単純なtempをその式に直接置き換えます |
| 一時変数を分割する | さまざまな目的で再利用される一時変数を個別の変数に分割します。 |
| ループをパイプラインに置き換える | 命令型ループを関数型パイプライン(マップ、フィルタ、リデュース)に置き換える |
| 説明変数の導入 | 複雑な式を明確にするために、名前付き一時変数を導入します。 |
これらの手法は単独で使用されるものではありません。Fowlerは、メソッド抽出の前に重要なステップとして「一時変数をクエリに置き換える」ことを説明しています。メソッドに一時変数がある場合、抽出対象のセクションの前後両方でこれらの一時変数が使用される可能性があるため、メソッドの一部を新しいメソッドに抽出することが困難になります。一時変数をクエリに変換することで、抽出の道が開かれます。
「一時変数をクエリに置き換える」とはどういう意味ですか?
「一時変数をクエリに置き換える」は、ローカルの一時変数をメソッド呼び出しに変換するリファクタリング手法です。値を計算してローカル変数に代入する代わりに、計算処理をプライベートメソッドであるクエリに抽出し、呼び出し時に計算結果を返します。一時変数が使用されていた箇所はすべて、クエリメソッドへの呼び出しに置き換えます。
ファウラーの『リファクタリング』からの典型的な例:
前:
ジャワ
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
return basePrice * 0.95;
else
return basePrice * 0.98;
後:
ジャワ
if (basePrice() > 1000)
return basePrice() * 0.95;
else
return basePrice() * 0.98;
private double basePrice() {
return _quantity * _itemPrice;
}
クエリメソッド basePrice() これは、名前付きの自己完結型計算処理になりました。クラス内の他のどのメソッドからも呼び出すことができ、独立してテストでき、サブクラスでオーバーライドでき、呼び出し元のメソッドを事前に読まなくても理解できます。
一時変数の問題
メソッド全体にわたってロジックを断片化する
一時変数を使うと、計算処理が代入(値が計算される箇所)と使用(値が読み取られる箇所)の2つの部分に分割されます。短いメソッドであれば、この分割は問題ありません。しかし、30行や50行にも及ぶメソッドでは、代入と使用の間には多くのロジックが挟まれている可能性があります。読者は代入箇所を見つけるために上にスクロールし、その意味をワーキングメモリに保持し、使用箇所までスクロールバックしなければなりません。一時変数が増えるごとに、この認知的負担は増大します。
彼らは抽出方法をブロックする
temp の最も重大な実用上の問題点は、他のリファクタリングを阻害することです。複雑な条件分岐を持つメソッドを考えてみましょう。このメソッドは、独立したメソッドに抽出することでメリットが得られます。分岐でメソッド内で既に割り当てられた temp を使用している場合、抽出するには、temp をパラメータとして渡すか、temp をインスタンス変数にするか、抽出されたメソッド内で再度値を計算するかのいずれかが必要になります。これらの方法はいずれもクリーンではありません。temp をクエリに置き換えることで、この障害を完全に解消できます。
それらは再利用と変異を促す
一時変数は、同じメソッド内で異なる目的で再利用されることがあり、ファウラーはこの慣習を「一時変数の絡まり」と呼んでいます。 temp or result 複数回再割り当てされる一時変数は、意味的な情報を一切提供せず、特定の時点におけるその変数の意味について読者を誤解させる可能性があります。単一目的の一時変数であっても、蓄積されると、メソッドのスコープが中間値で溢れかえり、読者はそれらを同時に追跡しなければならなくなります。
ステップバイステップ:一時変数をクエリに置き換える方法
この変換は、どの言語にも安全に適用できる4つのステップで構成されています。
ステップ1:temp変数が一度だけ代入され、変更されないことを確認します。メソッド内でtemp変数が後で再代入される場合は、まずSplit Temporary Variableを使用して分割してください。
ステップ2:代入式の右辺をプライベートメソッドに抽出します。 メソッドには、計算方法ではなく、計算内容を表す名前を付けましょう。 basePrice() よりも優れている calculateQuantityTimesPrice().
ステップ3:tempへのすべての参照を新しいメソッドの呼び出しに置き換えます。ほとんどのIDEではこれを自動的に行うことができます。tempを右クリックして「リファクタリング」→「変数のインライン化」を選択し、インライン化された式に対して「メソッドの抽出」を選択します。
ステップ4:temp変数の宣言を削除します。抽出が完了していれば、tempには参照が残っていないため、削除できます。
Java: 完全な動作例
ジャワ
// Before: Order class with temporary variables
public class Order {
private int quantity;
private double itemPrice;
public double getPrice() {
double basePrice = quantity * itemPrice; // temp 1
double discountFactor; // temp 2
if (basePrice > 1000)
discountFactor = 0.95;
else
discountFactor = 0.98;
return basePrice * discountFactor;
}
}
ジャワ
// After: temps extracted to query methods
public class Order {
private int quantity;
private double itemPrice;
public double getPrice() {
return basePrice() * discountFactor();
}
private double basePrice() {
return quantity * itemPrice;
}
private double discountFactor() {
return basePrice() > 1000 ? 0.95 : 0.98;
}
}
方法 getPrice() これで、計算内容を明確に伝える単一の式として読み取れるようになりました。抽出された各クエリは、個別に読み取り、テスト、拡張できます。 discountFactor() 呼び出し basePrice()これは正しいです。 basePrice() これは副作用のない純粋な計算なので、2回呼び出してもリスクはありません。
Python: Temp を Property に置き換える
Python では、クエリ メソッドの自然な同等物は @propertyこれにより、括弧なしでメソッドを呼び出すことができ、属性アクセスとまったく同じように読み取れます。
パイソン
# Before: temporary variables in a method
class Order:
def __init__(self, quantity, item_price):
self.quantity = quantity
self.item_price = item_price
def get_price(self):
base_price = self.quantity * self.item_price # temp
discount = 0.95 if base_price > 1000 else 0.98 # temp
return base_price * discount
パイソン
# After: temps replaced with properties (query methods in Python)
class Order:
def __init__(self, quantity, item_price):
self.quantity = quantity
self.item_price = item_price
def get_price(self):
return self.base_price * self.discount_factor
@property
def base_price(self):
return self.quantity * self.item_price
@property
def discount_factor(self):
return 0.95 if self.base_price > 1000 else 0.98
使い方 @property 手段 self.base_price インスタンス変数と全く同じように読み取られるため、呼び出しコードは self.base_price * self.discount_factor 完全に自然な状態。各特性は個別に検証可能です。
パイソン
def test_base_price():
order = Order(10, 150)
assert order.base_price == 1500
def test_discount_factor_high_value():
order = Order(10, 150) # base_price = 1500 > 1000
assert order.discount_factor == 0.95
def test_get_price():
order = Order(10, 150)
assert order.get_price() == 1500 * 0.95
このレベルのテスト可能性は、一時ベースのバージョンでは不可能です。内部計算 base_price (NAIST) と discount_factor メソッド外部からはアクセスできません。
TypeScript: クエリメソッドとゲッター
TypeScriptは、メソッドベースのクエリとプロパティゲッターの両方をサポートしており、それぞれJavaとPythonで利用可能なパターンに対応しています。
タイスクリプト
// Before: temporary variables
class Order {
constructor(private quantity: number, private itemPrice: number) {}
getPrice(): number {
const basePrice = this.quantity * this.itemPrice; // temp
const discount = basePrice > 1000 ? 0.95 : 0.98; // temp
return basePrice * discount;
}
}
タイスクリプト
// After: TypeScript getters replace temps
class Order {
constructor(private quantity: number, private itemPrice: number) {}
getPrice(): number {
return this.basePrice * this.discountFactor;
}
private get basePrice(): number {
return this.quantity * this.itemPrice;
}
private get discountFactor(): number {
return this.basePrice > 1000 ? 0.95 : 0.98;
}
}
クエリメソッドの適切な命名
クエリメソッドの名前は、最も重要な役割を担っています。名前が不適切な抽出メソッドは、置き換えられた一時メソッドよりもさらに悪い結果をもたらします。なぜなら、不透明な間接参照が発生し、呼び出し元はメソッド定義まで移動してその動作を理解する必要があるため、本来の目的が損なわれてしまうからです。
優れたクエリメソッド名は、以下の原則に従います。
それが何を表しているのかを明記し、どのように計算されたのかを明記しないでください。 basePrice() ビジネスコンセプトを伝える。 getQuantityTimesItemPrice() 概念ではなく、計算式を説明する。計算式や概念名が変わると、この区別が重要になる。 basePrice() 式が変更されても安定性は維持される。
値を表すには名詞句を使用してください。 クエリメソッドは値を返すものであり、コマンドではありません。 discountFactor(), totalAmount(), isEligible() 返されるものには、命名規則に従って名前を付けてください。 calculateDiscount(), processAmount() 命令型の慣例に従うため、純粋に計算して値を返すだけのメソッドにとっては混乱を招く。
ブールクエリは質問として読むべきです。 isHighValue(), hasDiscount(), meetsThreshold() 戻り値がブール値であり、呼び出し元がはい/いいえの質問をしていることを伝える。 bool 変数名 (ブール変数の命名規則、Search Console データのクエリ)はまさにこの懸念を反映しています。ブール変数とメソッドには、使用時に意味が明確になるような名前が必要です。
関連するメソッド間で名前を統一する。 If basePrice() によって使用されます discountFactor()命名の一貫性は読者に discountFactor に依存します basePrice命名規則の不統一は、この暗黙のドキュメントを損ないます。
適用するタイミング:TempをQueryに置き換える
このリファクタリングは、以下の場合に適用してください。
- 一時変数は一度だけ割り当てられ、再割り当てされることはありません。
- この計算は純粋な式であり、フィールドやパラメータから読み取りますが、外部状態を変更したり、ネットワークサービスを呼び出したり、時間やランダム性に依存したりすることはありません。
- 計算は、名前を付けることで可読性が向上するほど複雑か、あるいは単にテンポが邪魔になるほど単純かのどちらかである。
- 一時データを使用するブロックに抽出メソッドを適用しようとしています
最も一般的な理想的なシナリオは、派生値です。例えば、価格、合計金額、割引額、書式設定された文字列、条件付き分類などです。これらは、副作用がなく、オブジェクトのフィールドから完全に派生した値であり、メソッド内の中間計算ではなく、オブジェクトのプロパティとして自然に扱われるべきものです。
適用しない場合:Temp を Query に置き換える
パフォーマンスに影響する操作。計算コストが高い場合(データベースクエリ、ネットワーク呼び出し、O(n²)ループなど)、クエリメソッドを2回呼び出すとコストが2倍になります。tempはまさにこれを回避するために存在するものです。このような場合は、tempをそのままにしておくか、クエリメソッドをメモ化(最初の呼び出し後に結果をキャッシュする)してください。
パイソン
# Memoized property: computed once, cached
from functools import cached_property
class Order:
@cached_property
def expensive_validation(self):
return self.external_service.validate(self.data) # called once, cached
副作用のある操作。一時変数に、一度だけ実行されるべき操作(一意のIDの生成、ログ記録、ファイルへの書き込みなど)の結果が格納されている場合、それをクエリに変換すると、呼び出しごとにその操作が実行されることになります。これはプログラムの構造だけでなく、動作も変更します。副作用のある一時変数には、このリファクタリングを適用しないでください。
ループの繰り返し処理を通じて蓄積される温度。 温度は蓄積器であり、 for ループ、 total += item.priceは、Replace Temp with Query の候補ではありません。これは派生値ではなく、反復処理を通じて蓄積される状態です。ループが問題の場合は、代わりに Replace Loop with Pipeline を検討してください。
関連するリファクタリング手法
「一時ファイルをクエリに置き換える」は、メソッド内の不要な複雑さをまとめて排除する一連の手法の一つです。この手法群を理解することで、開発者は目の前の問題に最適な手法を選択できるようになります。
抽出メソッドは最も一般的な組み合わせです。多くの場合、一時変数をクエリに置き換えることで、抽出部分とメソッドの残りの部分との間で煩雑なパラメータの受け渡しが必要となる変数をクリアできるため、抽出メソッドが可能になります。
インラインテンポラリは、テンポラリを導入するのとは逆の操作です。テンポラリをコード内で直接、その式に置き換えます。テンポラリがコードの明確化に貢献せず、その式が既に読みやすい場合にインラインテンポラリを使用してください。
一時変数の分割は、同じメソッド内で単一の一時変数を複数の用途に再利用する場合に適用されます。各用途を反映した名前を持つ個別の変数に分割し、結果として得られる単一用途の一時変数のいずれかに「クエリによる一時変数の置換」を適用します。
「説明変数の導入」は逆方向のアプローチです。複雑な式が読みにくい場合、説明的な名前の一時変数を導入することで、可読性を向上させることができます。この手法と「一時変数をクエリに置き換える」は相反するものであり、開発者はどちらの方向が対象となるコードを改善するかを判断する必要があります。
ループをパイプラインに置き換える 一時アキュムレータを使用したループを連鎖パイプライン操作に置き換えることができる一般的なパターンに対処します(map, filter, reduce)は、より宣言的で読みやすい。
認定条件 SMART TS XL 大規模なリファクタリングをサポート
TempをQueryに置き換えるのは局所的なリファクタリングです。つまり、1つのメソッド内の1つの変数を変換するだけです。ある程度の規模のコードベースでは、「このリファクタリングをどのように適用するか?」ではなく、「コードベース全体のどこに適用すべきか、そして適用すると何に影響が出るのか?」という問いの方がより重要です。
SMART TS XL この質問に体系的に答えることができる、コードベースを横断した構造分析を提供します。複数の場所で同じ計算が一時変数として実行されている箇所を特定します。これは、Replace Temp with Query が単一の名前付きクエリ メソッドに統合するように設計されているパターンです。抽出されたリファクタリングされたクエリ メソッドがどのように使用されるかを追跡し、リファクタリングを行う前にその範囲を可視化します。また、言語を横断して動作します。COBOL プログラム、Java サービス、Python パイプラインがすべて同じデータを操作するエンタープライズ システムの場合、 静的コード分析 (NAIST) と 影響分析 同じ論理計算が異なる言語間で異なる形式で現れる箇所を特定すること。これは、Replace Temp with Query が単一言語レベルで対処する問題のより深い形態である。
作業中のチーム向け レガシーの近代化, SMART TS XLさん 依存関係の可視化 これにより、リファクタリングされたコンポーネントを変更する前に、それらがどこで使用されているかを確認できるため、計算をクエリメソッドに抽出しても、元の構造を期待していた呼び出し元が壊れることがなくなります。
一時変数と自己文書化コード
一時変数をクエリに置き換えるという決定は、最終的にはコードが何を伝えるべきかという決定です。一時変数は実装を伝えます。つまり、この値を取得するために実行した計算です。クエリ メソッドはドメインを伝えます。つまり、この値が何を意味するかです。Order クラスでは、 basePrice() この概念がその領域に存在することを読者に伝える。 double x = quantity * itemPrice 読者に算術演算について説明する。
コードが進化するにつれて、ドメイン概念には安定した場所が必要になります。一時変数に埋め込まれた計算は変更されたり、複数のメソッドで重複したり、次にそれを読む開発者に誤解されたりする可能性があります。一方、名前付きクエリメソッドは、見つけやすく、テストしやすく、ドキュメント化しやすく、意図的に進化させることができます。この安定性は、計算が必要とされるすべての場所、そしてそれを扱うすべての開発者にわたって維持されるため、「一時変数をクエリに置き換える」という取り組みは、単なる構文変更以上の意味を持ちます。これは、コードベースが解決する問題をどのように伝えるかという決定なのです。