リファクタリング はプログラムの機能を変えずにプログラムの構造を変えることです。リファクタリングは強力な技法ですが、注意して使う必要があります。一番危険性が高いのは、特に手動でリファクタリングを行う際、不注意でエラーが入り込みやすいことです。こうした危険性があるので、リファクタリングはよく批判されるわけです。「壊れてもいないコードをなぜ直すのか」と。
コードをリファクターしたくなる理由にはいくつかあると思います。まずは伝説的要因:古く神々しき製品用に古式蒼然たるコードが引き継がれた。そう思わない限りこのおかしな振る舞いが理解できない。元の開発チームが解散してしまった。新しいフィーチャーを持った新バージョンを作らなければいけないのに、コードがもはや理解不能になっている。そうしたコードを新しい開発チームが日夜解読し、マッピングし、散々設計計画を立てた挙句、コードを細断し始める。そして最後に、悪戦苦闘しつつ、新しい目標に従ってすべてを組み立てなおす。これが英雄的規模でのリファクタリングであり、生きてこれを語れる人は多くないでしょう。
もっと現実的な話としては、プロジェクトに設計変更を要するような要求事項が新たに加わったことなどがあるでしょう。この変更が元の計画に見落としがあったためなのか、それとも反復的手法(アジャイル(俊敏な)またはテスト主導型開発など、開発過程全体に渡って随時要求項目を出してくる手法)を使っているためなのかはどうでも良いことです。これはリファクタリングとしてはずっと小規模なもので、一般にインターフェースや抽象クラスの導入、クラスの分割、再配置などによるクラス階層の変更を伴います。
リファクタリングを使う理由の最後のものとしては、自動リファクタリング・ツールがあるなら初めからコード生成の早道として使ってしまうというものです。綴りが分からない時にスペルチェッカーを使って単語をタイプするようなものです。こうした日常的な使い方、例えばセッター・メソッド、ゲッター・メソッドの生成などにリファクタリングを使うのは、一旦使い方が分かってしまえば時間の節約に有効です。
Eclipseのリファクタリング・ツールは英雄的規模でのリファクタリング用途に使えるようなものではありません。・・・そんな規模に使えるようなツールはほとんどありません。ただ、普通のプログラマーが(日々の仕事にアジャイル開発手法を含むか否かによらず)日々経験するようなレベルでのコード変更のツールとしては貴重なものです。いずれにせよ、自動化できるような複雑な操作はどんなものであれ退屈なものです。Eclipseにどんな自動化ツールがあるかを知り、どう使われるように意図されているのかを理解することで仕事の生産性を高めることができるのです。
コードが分断してしまう危険性を減らすためには2つの重要な方法があります。一つはコードに対して完璧なユニット・テストを行うことです。コードはリファクタリングの前と後の両方でテストにパスする必要があります。2番目の方法はEclipseのリファクタリング・フィーチャーのような、自動ツールでリファクタリングを行うことです。
完璧なテストと自動リファクタリングの組み合わせは特に強力で、かつては神秘的とも思えた作業を、実用的な日々のツールに変えてしまいます。機能追加や保守性向上のためのこうした構造変更機能、つまりコードの機能を変えずに迅速且つ安全に構造を変えられる機能のおかげで、コードの設計開発に劇的なほどの変化が生まれます。それは公式にアジャイル手法を取り入れるか否かによりません。
Eclipseのリファクタリング・ツールは大きく3つのグループに分けられます(これがRefactoringメニューに現れる順番にもなっています)。
- 名前とコードの物理構成の変更で、フィールド、変数、クラス、インターフェースなどのリネームとパッケージやクラスの移動などです。
- クラスレベルでの論理構成の変更で、匿名クラスのネスト・クラスへの変更、ネスト・クラスの最上位クラスへの変更、具象クラスからインターフェースの生成、メソッドやフィールドのクラスからサブクラスまたはスーパークラスへの移動などです。
- クラス内でのコードの変更で、ローカル変数のクラス・フィールドへの変更、メソッド中で選択されたコードの別メソッドへの変更、フィールドへのゲッター、セッターメソッドの生成などです。
この3つの範疇にはうまく分類できないようなリファクタリングもいくつかあります。特にChange Method Signature(メソッド・シグニチャの変更)をここでは3番目の範疇に含めていますが、うまく分類できません。この例外を除き、以下の説明では上の順に従ってEclipseのリファクタリング・ツールを説明します。
特別なツール無しでも、ファイルシステムの中でファイルをリネームしたり移動したりすることはもちろんできますが、Javaソースファイルでそういうことをすると、importやpackage記述の更新に多くのファイルを編集する必要が出てきます。同様にクラスやメソッド、変数のリネームにテキスト・エディターの検索・置換機能を使うことはできますが、注意しないと、異なるクラスに似た名前のメソッドや変数があるかもしれません。プロジェクト中のすべてのファイルを見て、すべてのインスタンスが正しく認識され、変更されたことを確認していくのは退屈至極と言えます。
EclipseのRename と Moveはこうした変更をユーザーの手を煩わすことなく、全工程を高度な方法で行います。これはEclipseがコードの意味を理解し、特定のメソッドや変数、クラス名への参照を認識できるためです。こうした作業が簡単にできればメソッドや変数、クラス名などが確実にその内容に沿ったものになります。
元々の予定と違った働きをするように変更されたため、コードの名前が不適当だったり間違えやすいものであったりするのはごく普通のことです。例えば、ファイル中のある特定の単語を検索するプログラムがWebページを検索するように拡張され、URLクラスを使ってInputStreamを取得するように変えられたかもしれません。この入力ストリームが元々fileと呼ばれていたのであれば、新しい、より一般的な機能、sourceStreamのようなものに変えるべきでしょう。こうした変更は面倒で退屈な作業なので、開発者は変更せずに済ましてしまいがちなものです。もちろんそのままにしておくと、次の開発者がそのコードで作業するときには混乱させられる羽目になります。
Java要素をリネームするには、単純にPackage Explorerビューか、Javaソースファイルでその要素をクリックし、次にRefactor > Renameを選択します。ダイアログボックスで新しい名前を選択し、Eclipseにその名前への参照も変更させるべきかを選択します。具体的に表示されるフィールドは選択した要素のタイプによって異なります。例えばゲッター・メソッドとセッター・メソッドを持つフィールドを選択した場合、新しいフィールドを反映してこれらメソッドの名前も更新することができます。
図1. ローカル変数をリネームする
Eclipseのリファクタリングではすべて、比較ダイアログでEclipseが推奨する変更をプレビューし、影響を受けるファイル一つ一つの変更を拒否するか受け入れるかを選択できます。プレビューはリファクタリングに必要なものすべてを指定した後、Previewを押すことで行えます。またはプレビューはせず、Eclipseが正しく変更できると信頼して単にOKを押すこともできます。リファクタリングが何をするか良く分からない時には当然まずプレビューしたくなるでしょうが、リネームや移動のようなリファクタリングでは普通、プレビューは必要ありません。
MoveはRenameとほとんど同じように動作します。Java要素(通常はクラス)を選択し、新しい位置を指定し、参照を更新する必要があるかどうかを指定します。そして変更を確認するためにPreviewを選ぶか、OKを押してリファクタリングを即実行します。これを図2に示します。
図2. あるパッケージから別のパッケージにクラスを移動する
プラットフォームによっては(特にwindowsの場合)、クラスをあるパッケージやフォルダーから別のパッケージやフォルダーに、単にPackage Explorer(ビュー上のドラッグ・アンド・ドロップで移動することができます。
Eclipseの大掛かりなリファクタリングで、クラス相互の関係を自動的に変更することができます。こうしたリファクタリングは普通、Eclipseが提供する他のリファクタリングほど有用ではありませんが、かなり複雑なこともできるので貴重と言えます。使えるときには、非常に便利なのです。
2つのリファクタリング、Convert Anonymous Class to NestedとConvert Nested Type to Top Level はクラスを現在のスコープから出し、それを包含するスコープに移動するという意味で似ていると言えます。
匿名クラスは言ってみれば構文的な簡略表現で、これにより抽象クラスまたはインターフェースを実装するクラスを、クラス名を明示的に与えずにインスタンス化できます。これはユーザー・インターフェースでリスナーを生成するときなどに普通に使われます。リスト1でBagはどこか他で定義されたインターフェースで、get()とset()という2つのメソッドを宣言します。
リスト1. Bagクラス
public class BagExample
{
void processMessage(String msg)
{
Bag bag = new Bag()
{
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
};
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
}
|
匿名クラスがあまりにも大きくなりすぎてコードを読むのが困難になったら、その匿名クラスを適当なクラスにすることを考慮すべきでしょう。カプセル化を保つには(言い換えれば、そのクラスを知る必要のない外部のクラスから見えないように隠すには)最上位クラスにするよりもネスト・クラスにします。匿名クラス内でクリックし、Refactor > Convert Anonymous Class to Nestedを選択することでこれが行えます。指示されたときにクラスの名前、例えばBagImplを入力し、PreviewかOKいずれかを選択します。こうするとリスト2に示すようにコードが変わります。
リスト2. リファクターされたBagクラス
public class BagExample
{
private final class BagImpl implements Bag
{
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
}
void processMessage(String msg)
{
Bag bag = new BagImpl();
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
} |
Convert Nested Type to Top Levelはネスト・クラスを他のクラスから使えるようにするのに便利です。例えば上に示したBagImplクラスのように、クラス内部で値オブジェクトを使っているとしましょう。後からこのデータをクラス間で共有することにした場合には、リファクタリングがネスト・クラスから新しいクラスファイルを生成します。これはソースファイルのクラス名をハイライトし(またはOutlineビューでクラス名をクリックし)、Refactor > Convert Nested Type to Top Levelを選択することで行えます。
このリファクタリングはエンクロージング・インスタンスに名前をつけるように要求してきます。例えばexampleのような候補をあげてくるので、それに決めることもできます。その意味がこのすぐ後の説明で分かります。OKを押すと、エンクロージング・クラスBagExampleのコードがリスト3のように変わります。
リスト3. リファクターされたBagクラス
public class BagExample
{
void processMessage(String msg)
{
Bag bag = new BagImpl(this);
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
} |
クラスがネストされているときには、そのクラスは外部クラスのメンバーにアクセスできることには注意してください。この機能を保つため、リファクタリングではエンクロージング・クラスBagExampleのインスタンスを、リファクタリング前にネスト・クラスであったクラスに追加します。これはその前に、名前をつけるように要求されたインスタンス変数であり、このインスタンス変数を設定するコンストラクターも生成します。リファクタリングが生成するBagImplクラスをリスト4に示します。
リスト4. BagImplクラス
final class BagImpl implements Bag
{
private final BagExample example;
/**
* @paramBagExample
*/
BagImpl(BagExample example)
{
this.example = example;
// TODO Auto-generated constructor stub
}
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
} |
BagExampleクラスへのアクセスを保つ必要が無ければ(このケースの場合がそうですが)、インスタンス変数とコンストラクターを安全に取り去り、BagExampleクラスのコードをデフォルトの引数なしコンストラクターに変更することができます。
別の2つのリファクタリング、プッシュ・ダウンとプル・アップはクラス・メソッド、クラス・フィールドをそれぞれサブクラスまたはスーパークラスに移動します。リスト5で定義されるような抽象クラスVehicleがあるとしましょう。
リスト5. 抽象Vehicleクラス
public abstract class Vehicle
{
protected int passengers;
protected String motor;
public int getPassengers()
{
return passengers;
}
public void setPassengers(int i)
{
passengers = i;
}
public String getMotor()
{
return motor;
}
public void setMotor(String string)
{
motor = string;
}
}
|
リスト6に示すようにAutomobileと呼ばれる、Vehicleのサブクラスもあるとします。
リスト6. Automobileクラス
public class Automobile extends Vehicle
{
private String make;
private String model;
public String getMake()
{
return make;
}
public String getModel()
{
return model;
}
public void setMake(String string)
{
make = string;
}
public void setModel(String string)
{
model = string;
}
}
|
Vehicleの属性の一つにmotorがあることに注意してください。エンジン付きの車両のみを対象にするならこれでも構いませんが、手漕ぎのボートのようなものも含もうとするなら、motor属性をVehicleクラスからAutomobileクラスに下げるべきでしょう。これをするにはアウトラインビューでmotorを選び、次にRefactor > Push Downを選びます。
Eclipseは優秀でフィールドをフィールドだけで移動することは必ずしもできないことを分かっており、Add Requiredボタンを用意しています。ただ、これはEclipse 2.1では常に正しく動作するとは限りません。このフィールドに依存するメソッドがどれもプッシュダウンされたかどうか確認する必要があります。この例ではリスト3に示すように、2つのメソッド、ゲッター・メソッドとセッター・メソッドがmotorフィールドに付随しています。
図3. 必要なメンバーを追加する
OKを押すとmotorフィールドはgetMotor()、setMotor()メソッドと共にAutomobileクラスに移動します。リスト7はこのリファクタリングの後、Automobileクラスがどう見えるかを示します。
リスト7. リファクターされたAutomobileクラス
public class Automobile extends Vehicle
{
private String make;
private String model;
protected String motor;
public String getMake()
{
return make;
}
public String getModel()
{
return model;
}
public void setMake(String string)
{
make = string;
}
public void setModel(String string)
{
model = string;
}
public String getMotor()
{
return motor;
}
public void setMotor(String string)
{
motor = string;
}
} |
プルアップリファクタリングはプッシュダウンとほとんど同じですが、当然ながらクラスのメンバーをサブクラスではなくスーパークラスに移動します。後で気が変わり、motorをVehicleクラスに移動し直したくなったような時などにこれを使うことができます。必要なメンバーを確実に選ぶように注意するのはプッシュダウンと同じです。
AutomobileクラスにmotorがあるということはVehicleのサブクラスで別のもの、例えばBusを生成した場合には、Busクラスにもmotor(と、motorに関連したメソッド)を追加する必要があります。こうした関係を表す方法の一つは(AutomobileやBusが実装し、RowBoatは実装しない)Motorizedのようなインターフェースを作ることです。
Motorizedインターフェースを生成する一番簡単な方法は、Automobileにインターフェース抽出リファクタリングを使うことです。これをするにはアウトラインビューでAutomobileクラスを選び、次にメニューからRefactor > Extract Interfaceを選びます。このダイアログで、どのメソッドをインターフェースに含めたいか選択することができます。これを図4に示します。
図4. Motorizedインターフェースを抽出する
OKを押すと、リスト8のようにインターフェースが生成されます。
リスト8. Motorizedインターフェース
public interface Motorized
{
public abstract String getMotor();
public abstract void setMotor(String string);
}
|
するとAutomobileに対するクラス宣言は次のように変更されます。
public class Automobile extends Vehicle implements Motorized |
この範疇最後のリファクタリングは”Use Supertype Where Possible”です。自動車の在庫を管理するアプリケーションを考えてください。このアプリケーションでは一貫してAutomobile型のオブジェクトを使います。すべての乗り物を扱いたいと思ったらこのリファクタリングを使ってAutomobileへの参照をVehicleへの参照に変更することができます(図5)。instanceofオペレーターを使ってコード内の型チェックを行う場合にはどんな型チェックであれ、特定の型またはスーパータイプを使うのが適当かどうか判断する必要があり、それによって適宜最初のオプション、”Use the selected supertype in 'instanceof' expressions”をチェックする必要があります。
図5. Automobileをそのスーパータイプ、Vehicleに変更する
Java言語では、特にファクトリー・メソッドのパターンが使われる場合には、スーパータイプを使う必要のある場合が頻繁に出てきます。典型的には、これは(抽象クラスを実装する具象オブジェクトを返す)静的なcreate()メソッドを持つ抽象クラスを持つことで実装されます。これは生成されるべき具象オブジェクトの型がクライアント・クラスには関係の無い、実装の詳細に依存するようなときには便利です。
リファクタリングで一番大掛かりなものがクラス内でコードを組み直すリファクタリングです。なによりもこのリファクタリングでは中間変数の追加(または削除)ができ、また古いメソッドの一部から新しいメソッドを生成したり、フィールドのゲッター、セッター・メソッドを生成したりすることができるのです。
リファクタリングにはExtractで始まるものがいくつかあります。Extract Method(メソッド)、Extract Local Variable (ローカル変数抽出)、Extract Constants(定数抽出)などです。最初のExtract Methodはご想像の通り、選択したコードから新しいメソッドを生成します。例えばリスト8にあるクラスのmain()メソッドを見てください。これはコマンドライン・オプションを評価し、-Dで始まるオプションを見つけたら名前と値の対としてPropertiesオブジェクトに保存します。
リスト8. main()
import java.util.Properties;
import java.util.StringTokenizer;
public class StartApp
{
public static void main(String[] args)
{
Properties props = new Properties();
for (int i= 0; i < args.length; i++)
{
if(args[i].startsWith("-D"))
{
String s = args[i].substring(2);
StringTokenizer st = new StringTokenizer(s, "=");
if(st.countTokens() == 2)
{
props.setProperty(st.nextToken(), st.nextToken());
}
}
}
//continue...
}
}
|
あるメソッドからあるコードを取り出して別のメソッドに入れたくなる場合としては主に2つの例が挙げられます。1番目の例としてはメソッドが長すぎ、論理的に別な2つ以上の操作をしている場合です(ここでのmain()メソッドが他に何をするのかは分かりませんが、見ての通り、他に何かをしているからここでメソッドを抽出するというわけではありません)。2番目の例としては他のメソッドで再利用できるような、論理的にはっきりと分かれる部分がコードの中にある場合です。さらに別の例として例えば、いくつか別のメソッドに同じ何行かのコードを書くような場合に応用することも考えられなくはありません。ただそういう場合にはそのコードを実際に再利用する必要があるまで、このリファクタリングを行うことはないでしょう。
別の部分で名前と値の対を構文解析して、Propertiesオブジェクトに追加する必要がある場合には、if文を追うことでStringTokenizer宣言を含むコードの部分を抽出できます。これをするには、このコードをハイライトし、メニューからRefactor > Extract Methodを選択します。メソッド名を入れるように指示されるのでaddPropertyを入力し、次にこのメソッドにProperties prop とStrings という2つのパラメーターがあることを確認します。リスト9はEclipseがaddProp()メソッドを抽出した後のクラスを示します。
リスト9. addProp()が抽出される
import java.util.Properties;
import java.util.StringTokenizer;
public class Extract
{
public static void main(String[] args)
{
Properties props = new Properties();
for (int i = 0; i < args.length; i++)
{
if (args[i].startsWith("-D"))
{
String s = args[i].substring(2);
addProp(props, s);
}
}
}
private static void addProp(Properties props, String s)
{
StringTokenizer st = new StringTokenizer(s, "=");
if (st.countTokens() == 2)
{
props.setProperty(st.nextToken(), st.nextToken());
}
}
} |
Extract Local Variable(ローカル変数抽出)リファクタリングは直接使用されている式を取り出し、まずローカル変数に割り当てます。この変数は次にその式が元あった場所で使われます。例えば上のaddProp()メソッドでは、st.nextToken()への最初のコールをハイライトしRefactor > Extract Local Variableを選択します。変数を入力するように指示されるので、keyを入力します。選択された式と同じ式をすべて新しい変数への参照で置き換えるオプションがあることに注意してください。このオプションは普通使って問題ないのですが、ここでのnextToken()メソッドは(明らかに)呼ばれる度に異なる値を返すので問題があります。このオプションが選択されていないことを確認します。図6を見てください。
図6. 選択された式すべてを置き換えることはしない
次にこのリファクタリングをst.nextToken()への2番目のコールに対して繰り返しますが、今度は新しいローカル変数valueを呼びます。リスト10はこの2つのリファクタリング後のコードを示します。
リスト10. リファクターされたコード
private static void addProp(Properties props, String s)
{
StringTokenizer st = new StringTokenizer(s, "=");
if(st.countTokens() == 2)
{
String key = st.nextToken();
String value = st.nextToken();
props.setProperty(key, value);
}
} |
変数をこうした方法で導入するのにはいくつか利点があります。第一に式に意味のある名前をつけることで、そのコードが何をしているかが明確になります。第二に式が返す値を容易に調べられるので、コードのデバッグが簡単になります。最後に、一つの式が何度も出てきて、それを一つの変数で置き換える場合にはずっと効率的なことです。
Extract Constant(定数抽出)はExtract Local Variable(ローカル変数抽出)と似ていますが、静的な定数式を選ぶ必要があります。これをリファクタリングが静的な最終式に変換します。これはコードの中からハード・コーディングされた数字や文字列を取り除くには有効です。例えば上の例では名前と値の対を定義するコマンドライン・オプションとして-D" を使いました。コード中の-D" をハイライトし、Refactor > Extract Constantを選び、定数の名前としてDEFINEを入力します。このリファクタリングでコードがリスト11のように変わります。
リスト11. リファクターされたコード
public class Extract
{
private static final String DEFINE = "-D";
public static void main(String[] args)
{
Properties props = new Properties();
for (int i = 0; i < args.length; i++)
{
if (args[i].startsWith(DEFINE))
{
String s = args[i].substring(2);
addProp(props, s);
}
}
}
// ... |
各Extract(抽出)リファクタリングに対して、対応する逆の操作をするInline(インライン)リファクタリングがあります。例えば上のコードで変数 s をハイライトし、Refactor > Inline...を選び、次にOKを押すと、EclipseはaddProp()へのコールの中で次のように直接、式args[i].substring(2)を使います。
if(args[i].startsWith(DEFINE))
{
addProp(props,args[i].substring(2));
} |
これは一時変数を使うよりは多少効率的で、コードが簡潔になるので読みやすくなります(見方によっては読みにくくもなります)。ただ一般的に言って、こうしたインライン化はあまりお勧めできるものではありません。
変数をインライン化した式で置き換えられるのと同様に、メソッド名や静的最終定数をハイライトすることもできます。メニューからRefactor > Inline...を選択するとEclipseはメソッドへのコールを、それぞれメソッド・コードまたは定数値を持つ定数への参照に置き換えます。
普通、オブジェクトの内部構造を見えるようにするのは良いやり方とは言えません。Vehicleクラスとそのサブクラスがプライベート・フィールドまたは保護フィールドどちらかを持ち、またアクセスにパブリックのセッター、ゲッター・メソッドを持つのはこのためです。こうしたメソッドは2つの異なる方法で自動的に生成されます。
一つの方法は、これらのメソッドを生成するのにSource > Generate Getter and Setterを使うものです。この方法では(まだゲッター、セッターの無い)各フィールドに対するゲッター、セッター・メソッドの候補を挙げたダイアログ・ボックスを表示します。ただしこれは新しいメソッドを使用するフィールドへの参照は更新しないので、リファクタリングではありません。更新が必要な場合は自分でする必要があります。このオプションは強力な時間節約ですが、クラスを最初に作る時またはクラスに新しいフィールドを追加する時に使うのが一番です。こういう時にはこれらのフィールドを参照するようなコードはまだ無いので、他のコードを変更する必要が無いからです。
ゲッター、セッターを生成する二番目の方法は、フィールドを選択し、次にメニューからRefactor > Encapsulate Fieldを選択するものです。この方法では一度に一つのフィールドに対するゲッターとセッターのみを生成しますが、Source > Generate Getter and Setterとは対照的に、フィールドへの参照も新しいメソッドへのコールに変更します。
例えば、リスト12に示すように、まったく新しい、簡単なAutomobileクラスから始めてみましょう。
リスト12. 簡単なAutomobileクラス
public class Automobile extends Vehicle{public String make;public String model;} |
次にAutomobileをインスタンス化し、makeフィールドに直接アクセスするクラスを作ります。これをリスト13に示します。
リスト13. Automobileをインスタンス化する
public class AutomobileTest
{
public void race()
{
Automobilecar1 = new Automobile();
car1.make= "Austin Healy";
car1.model= "Sprite";
// ...
}
} |
今度はフィールド名をハイライトし、Refactor > Encapsulate Fieldを選択することでmakeフィールドをカプセル化します。ダイアログにゲッター、セッター・メソッドの名前を入れます・・・予期した通りデフォルトがgetMake()、setMake()です。またフィールドと同じクラスにあるメソッドがフィールドを直接アクセスし続けるのか、これらの参照がすべての他のクラスと同じアクセス方法を使うように変更するかを選択することができます。(二つのうち絶対こちらを使うべきだと言い張る人もいますが、ここではAutomobileのmakeフィールドへの参照は無いのでどちらでも変わりありません。)図7を見てください。
図7. フィールドをカプセル化する
OKを押した後は、Automobileクラスのmakeフィールドはプライベートになり、リスト14に示すようにgetMake()、setMake()メソッドを持ちます。
リスト14. リファクターされたAutomobileクラス
public class Automobile extends Vehicle
{
private String make;
public String model;
public void setMake(String make)
{
this.make = make;
}
public String getMake()
{
return make;
}
}
|
新しいアクセス方法を使うように、AutomobileTestクラスも更新されます(リスト15)。
リスト15. AutomobileTestクラス
public class AutomobileTest
{
public void race()
{
Automobilecar1 = new Automobile();
car1.setMake("Austin Healy");
car1.model= "Sprite";
// ...
}
} |
ここで議論するリファクタリングの最後は一番使うのが難しいもの、Change Method Signature(メソッド・シグニチャの変更)です。これがすることは明確で、メソッドのパラメーター、可視性、戻り値を変更します。こうした変更がメソッドや、そのメソッドを呼ぶコードに与える影響についてはそれほど明確ではありません。ここには特別魔法があるわけではありません。リファクターされたメソッドに(未定義の変数や型の不一致が残ったりして)問題が起きた場合には、リファクタリング操作がフラグを上げます。それに対するオプションとしてリファクタリングをとりあえず受け入れ、問題を後で修正するか、リファクタリングを中止するかを選択できます。リファクタリングが他のメソッドに問題を起こしている場合には、それらの問題は無視されてしまうので、リファクタリング後に自分でその問題を修正する必要があります。
これをはっきりさせるため、次のリスト16に示すクラスとメソッドを見てください。
リスト16. MethodSigExampleクラス
public class MethodSigExample
{
public int test(String s, int i)
{
int x = i + s.length();
return x;
}
}
|
上記のクラスでのtest()メソッドは他のクラスにあるメソッドに呼ばれます(リスト17)。
リスト17. callTestメソッド
public void callTest()
{
MethodSigExample eg = new MethodSigExample();
int r = eg.test("hello", 10);
}
|
最初のクラスのtestをハイライトし、Refactor > Change Method Signatureを選択します。図8のダイアログボックスが現れます。
図8. Change Method Signature(メソッド・シグニチャの変更オプション)
最初のオプションはメソッドの可視性を変更するものです。この例では可視性をprotectedまたはprivateに変えることで2番目のcallTest()メソッドからアクセスできなくなります。(もし2つのメソッドが別のパッケージの場合は、アクセスをデフォルトに変えてもこの問題が起きます。)Eclipseはリファクタリング中のこのエラーに対してはフラグを上げません。適当なオプションを選択します。
次のオプションは戻り型を変更するものです。例えば戻り型をfloatに変えたとしても、test()メソッドの戻り記述中のintは自動的にfloatにプロモートされるので、エラーとしてのフラグは上がりません。それでも、floatをintに変更することはできないので、この変更は2番目のクラスのcallTest()では問題を起こすことになります。test()が返す戻り値をintにキャストするか、callTest()中のrの型をfloatに変更する必要があります。
最初のパラメーターの型をStringからintに変更する場合も同様の考慮が必要になります。この変更はリファクターを受けているメソッドに問題を起こす(intはlength()メソッドを持ちません)ので、リファクタリング中にフラグが上がります。ところがStringBufferに変更する場合は、StringBufferがlength()メソッドを持つので、問題ありのフラグが立ちません。もちろんこれはcallTest()メソッドがtest()を呼ぶ時にやはりStringをパスしてしまうので、問題を起こすことになります。
前に説明したように、フラグが上がるか否かによらずリファクタリングでエラーが起きる場合でも、ケース・バイ・ケースでエラーを修正しながら続けることができます。別のやり方としてはエラーを事前防止してしまうのも一つです。パラメーター iが不必要なので取り除きたい場合、リファクタリングを受けているメソッド中の、そのパラメーター iへの参照を取り除くことから始めるわけです。そうすればパラメーターの除去はもっとスムーズに行くことになります。
最後にDefault Value(デフォルト値)オプションについて説明しておかなければなりません。このオプションはメソッド・シグニチャにパラメーターを追加する場合にのみ使われ、呼び出し元に追加するそのパラメーターの値を入れるのに使われます。例えばString型で、nの名前、デフォルト値worldのパラメーターを追加する場合、callTest()メソッド中のtest()へのコールは次のように変更されます。
public void callTest()
{
MethodSigExample eg = new MethodSigExample();
int r = eg.test("hello", 10, "world");
} |
Change Method Signature(メソッド・シグニチャの変更)リファクタリングに関する説明はちょっと見ると非常に面倒そうですが、要点としては、問題が多いということではありません。うまく使うには計画的によく考える必要はありますが、むしろ非常に強力で時間の節約になるリファクタリングであるということです。
Eclipseのツールでリファクタリングが簡単になりますし、使い方に慣れれば仕事の生産性を上げることができます。プログラムのフィーチャーを反復的に追加する、アジャイル(俊敏な)手法では、リファクタリングでプログラム設計を変更したり拡張したりします。リファクタリングを必要とする正式手法を使わない場合でも、Eclipseのリファクタリング・ツールを使うことで、よくある普通のコード変更の時間が節約できるようになります。ちょっと時間をかけてEclipseのツールに慣れ、そうしたツールがどこで使えるかを考えるだけでも時間の使い方としては有効だと思います。
書籍
- リファクタリングに関する重要な本としては、Martin Fowler、Kent Beck、John Brant、William Opdyke、Don Roberts共著のRefactoring: Improving the Design of Existing Code (1999年Addison-Wesley刊)があります。
- David Gallardo、Ed Burnette、Robert McGovern共著によるEclipse in Eclipse In Action: A Guide for Java Developers (2003年Manning刊)では、Eclipseでのプロジェクト設計、開発の観点から、進行中過程としてのリファクタリングが説明されています。
- (この記事でも取り上げたファクトリー・メソッドのような)パターンはオブジェクト指向設計を理解、議論する上で重要なツールです。古典的なテキストとして、Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides共著によるDesign Patterns: Elements of Reusable Object-Oriented Software (1995年Addison-Wesley刊)があります。
- JavaプログラマーにとってDesign Patterns の欠点は、例がC++で書かれていることです。パターンをJava言語で説明した本としては、Mark Grand著のPatterns in Java, Volume One: A Catalog of Reusable Design Patterns Illustrated with UML (1998年Wiley刊)があります。
- アジャイル・プログラミングの一種の入門としてはKent Beck著のExtreme Programming Explained: Embrace Change (1999年Addison-Wesley刊)があります。
Webサイト
-
Martin Fowler's Web siteはWeb上でリファクタリングの中心になっています。
- JUnitでのユニット・テストについて更に詳しくはJUnit Web siteを見てください。
developerWorks にある記事・チュートリアル
- Daniel H. Steinbergによる「Refactoring with Eclipse」では裏で何が行われているかを説明しています(developerWorks 2001年11月)。
- Davidによる「Java design patterns 101」はパターンの入門編です(developerWorks 2002年1月)。
- Paul Mondayによる「Java design patterns 201」はパターンについての、もう少し上級のチュートリアルです(developerWorks 2002年4月)。
- Dan Kehnによる「EclipseでのJava Development Toolsの拡張」では自分のリファクタリングでEclipseをどう拡張するかを説明しています(developerWorks 2003年7月)。
- Eclipseをより詳しく知るための出発点としてDavidが「Eclipse Platform入門」を説明しています(developerWorks 2002年11月)。
- 「エクストリーム・プログラミングの神秘を解く」でRoy W. MillerがEclipseでJUnitをどう使うか説明しています(developerWorks 2003年5月)。
- developerWorksOpen source projectsゾーンのarticles for Eclipse usersにはさらに記事があります。
David Gallardoはソフトウェアの国際化対応、Java Webアプリケーション、データベース開発を専門とする独立系のソフトウェア・コンサルタントおよび著述家です。15年以上の間、彼はプロのソフトウェア・エンジニアであり、多くのオペレーティング・システム、プログラム言語、およびネットワーク・プロトコルについての経験があります。彼の最近の経験には、企業間 (B2B) e-commerce企業TradeAccess, Inc.における先進的なデータベースおよび国際化対応の開発が含まれています。その前まで、彼はLotus Development CorporationのProduct Developmentグループにおけるシニア・エンジニアであり、Dominoを含むLotus製品にUnicodeおよび国際言語サポートを提供するクロス・プラットフォーム・ライブラリーの開発に貢献しました。