Eclipse には様々なリファクタリング機能が備わっていますが,正直,実行するとどうなるのかよくわからないものが多かったのでまとめてみました.
リファクタリングとは
リファクタリングとは、ソフトウェアの外部の振る舞いを保ったままで、内部の構造を改善していく作業を指します。
リファクタリングを行う理由
ソフトウェアの設計を改善する
完璧な設計は存在しません.厳密には設計当時は完璧に思えても時間経過とともに完璧ではなくなります.
さらに,今後どんな要望が出てきて,誰が,どこに機能追加することになるかは誰にも分かりません.
設計が完璧ではなくなる例
- 決済方法として現金,クレジットカードのみを考えていたが,他のキャッシュレス決済に対応する必要が出てきた
- 納期が短かったので,影響範囲を限定的にするために場当たり的な対応をした
- 新たに参画した人が,既存コードの理解が足らないままコードを追加し,スパゲッティコードになる
時間経過で設計が劣化していくので,定期的にメンテナンスしてやる必要があります.
ソフトウェアを理解しやすくする
きれいに整理されたコードは可読性が高く,開発者がコードを読む時間を短縮できます.
さらに,バグを見つけやすくなります.
長期的に開発スピードが向上する
設計がよくないと技術的負債が蓄積され,同じ変更を何箇所にも適用する,影響範囲がわからず調査に時間がかかるようになります.
継続的にリファクタリングすることで負債を解消し,機能が増えても開発にかかる工数が少なくて済みます.
リファクタリングを避けるとき
残念ながらリファクタリングは銀の弾丸ではありません.
今後もコードを塩漬けにする
既存のコードに変更を加えることがなければ,リファクタリングしても効果は薄く,リスクだけが残ります.
そこに,経済的合理性はなく,ただの自己満足に過ぎません.
外部公開されたインタフェース
API の開発をしていて,その API を誰が使っているかわからない場合は変更することができません.
上司を説得できない
リファクタリングはその性質上,システムの機能は増えません.
上司がリファクタリングの重要性を理解できていない場合は全力で説得してみましょう.
どうしても説得できない場合は,責任を負わないために潔く諦めましょう.(そして裏で,ソフトウェアのプロフェッショナルとして黙ってリファクタリングしましょう.)
納期が厳しいときや変更箇所を全て手動テストしなければならない時は避けるべきかもしれません.
Eclipse でのリファクタリング
リファクタリングと言ってもローカル変数の名前を変更するような小さな変更から,switch 文をクラスのポリモーフィズムに置き換えるような大掛かりな変更まであります.
リファクタリング対象のコードがテストで保護されているのであれば,自信を持ってリファクタリングできますが,レガシーコードにはテストがありませんので,安心してリファクタリングできません.
しかし,Eclipse などの統合開発環境に備わっているリファクタリングツールに限って言えば,全世界で何千何万回と実行(テスト)されているので,自動テストがない環境でも比較的安全にリファクタリングできます.
⚠️ 100%安全ではありません.
確認環境
Version: 2019-06 (4.12.0)
Build id: 20190614-1200
リファクタリングの方法
リファクタリングしたい箇所を選択して右クリック → リファクタリングのメニューから実行可能です.
ショートカットの場合はAlt
+Ctrl
+T
です.(Mac の場合はAlt
+⌘
+T
)
リファクタリング実行時の注意点
バックアップは頻繁にとる
リファクタリングは複数のクラスに影響する可能性があり,何度か実行するとctrl + z
で戻せない可能性があります.
変更を戻せるようにこまめにバックアップを取得しましょう.
自動テストが存在するのであれば,頻繁に実行する
IDE のリファクタリングは基本的に安全ですが,リファクタリングにより振る舞いが変わっていないことを確認するために,できる限り頻繁に自動テストを実行しましょう.
リフレクションが使われている場合は影響範囲について十分調査する
Java にはリフレクションという強力な仕組みが導入されていますが,コンパイラの型チェックなどの恩恵を受けることができません.
IDE のリファクタリングツールでも対象外となってしまいますので,コードの中で使っている場合は実行するまでコードを壊したことを検知できません.
プログラム中で利用している場合は,影響範囲について十分調査しましょう.
フレームワークやプラグインを開発しているのであれば,リフレクションを利用している可能性があります.
テストコード中でアクセス修飾子を破壊するために使っている?誰ですかそんなことしたのは?
各リファクタリング
やっと本題です.
独断と偏見で,よく使うもの,使えそうなものの順に並べてみました.
名前変更
適用対象
パッケージ,クラス,メソッド,変数
効能
名前を変更する.
どうしてその略称を使った?やa
とかinstanse2
とか命名を放棄した怠慢のような変数名,メソッド名,クラス名を,意味のわかる名前に変えることができる.
オプション(メソッドに適用した場合)
名前変更されたメソッドへの委譲として元のメソッドを保持 + 非推奨としてマーク
外部公開しているメソッドなどを互換性維持のため元メソッドを残したまま変更することも可能です.
非推奨としてマークを選択した場合は@deprecated
が付与されます.
String getCpyNm() {
return cpyNm;
}
String getCompanyName() {
return companyName;
}
/**
* @deprecated {@link #getCompanyName()} の代用
*/
public String getCpyNm() {
return getCompanyName();
}
public String getCompanyName() {
return companyName;
}
オプション(クラスに適用した場合)
以下のオプションが利用可能です.
- 類似の名前の変数とメソッドの更新
- コメントおよびストリング内のテキスト出現箇所の更新
- 非 Java テキストファイル内の完全修飾名の更新
参照している箇所ではなく,コメントや似たメソッド名を変更し安全とは言い切れないため,プレビューでの確認が必須です.
メソッドの抽出
適用対象
メソッド内の選択した行
効能
選択箇所を別メソッドとして分離する.
コード内に重複が存在すれば,複数の重複箇所もまとめて置き換えることが可能.
public void printAverage(int a, int b, int c) {
int sum = a + b+c;
int average = sum/3;
System.out.println(average);
}
public void printAverage(int a, int b, int c) {
int average = getAverage(a, b, c);
System.out.println(average);
}
private int getAverage(int a, int b, int c) {
int sum = a + b+c;
int average = sum/3;
return average;
}
ローカル変数の抽出
適用対象
メソッド内の選択した計算式,条件式
効能
長い式などを分割,または,説明変数の導入
インライン化の逆
public int getTotal(int price) {
int result = (int) (price * 0.8 * 1.10);
return result;
}
public int getTotal(int price) {
double discountPrice = price * 0.8;
int result = (int) (discountPrice * 1.10);
return result;
}
インライン化
適用対象
変数
効能
冗長な変数宣言をその値で置き換える
ローカル変数の抽出の逆
public int getTotal(int price) {
double discountPrice = price * 0.8;
int result = (int) (discountPrice * 1.10);
return result;
}
public int getTotal(int price) {
int result = (int) ((price * 0.8) * 1.10);
return result;
}
定数の抽出
適用対象
定数
効能
定数をメンバ変数に変更する.
static な値のみ利用可能.
public int getTotal(int price) {
int result = (int) (price * 0.8 * 1.10);
return result;
}
public static final double TAX_RATE = 1.10;
public int getTotal(int price) {
int result = (int) (price * 0.8 * TAX_RATE);
return result;
}
フィールドのカプセル化
適用対象
メンバ変数
効能
メンバ変数の getter/setter を生成し,参照方法を getter/setter を利用したものに置き換える.
オプション
宣言型でのフィールドアクセス
をフィールド参照の保持
にすると,直接フィールドの値を参照したままにできる.
public class Refactoring {
private String companyName = "";
private int companyId = 0;
public Refactoring(String companyName, int companyId) {
this.companyName = companyName;
this.companyId = companyId;
}
}
public class Refactoring {
private String companyName = "";
private int companyId = 0;
public Refactoring(String companyName, int companyId) {
this.companyName = companyName;
this.setCompanyId(companyId);
}
/**
* @return companyId
*/
private int getCompanyId() {
return companyId;
}
/**
* @param companyId セットする companyId
*/
private void setCompanyId(int companyId) {
this.companyId = companyId;
}
}
ローカル変数をフィールドに変換
適用対象
ローカル変数
効能
ローカル変数をメンバ変数に変更する.
定数の抽出と似てる
オプション
フィールドのアクセス修飾子や初期化位置を変更可能
public class Refactoring {
public int getTotal(int price) {
double discountRate = 0.8;
int result = (int) (price * discountRate * 1.10);
return result;
}
}
public class Refactoring {
private double discountRate = 0.8;
public int getTotal(int price) {
int result = (int) (price * discountRate * 1.10);
return result;
}
}
移動
適用対象
パッケージ,クラス
効能
クラスを別パッケージに移動する,パッケージを移動する/名称変更する.
パッケージを移動した場合はサブパッケージもまとめて変更できる.
package com.example.refactoring;
package com.example.refactoring.util;
適用対象
static 変数,static メソッド
効能
別のクラスに static 変数や static メソッドを移動することができる.
メンバ変数にも使うことができるが,参照を保持できないので高確率でコンパイルエラーになる...(リファクタリングと言えるのか?)
public class Refactoring {
public static final String staticString = "s";
public static String getStaticString() {
return staticString;
}
}
public class OtherClass {
}
public class Refactoring {
public static String getStaticString() {
return OtherClass.staticString;
}
}
public class OtherClass {
public static final String staticString = "s";
}
メソッド・シグネチャーの変更
適用対象
メソッド
効能
メソッドの引数を追加,削除,変更する
コピペでは面倒な並び替えも可能
オプション
変更されたメソッドへの委譲として元のメソッドを保持
にチェックすると元のシグネチャーのメソッドを保持できる.
public Refactoring(String companyName) {
this.companyName = companyName;
}
public Refactoring(String companyName, String newParam) {
this.companyName = companyName;
}
/**
* @deprecated {@link #Refactoring(String,String)} の代用
*/
public Refactoring(String companyName) {
this(companyName, null);
}
public Refactoring(String companyName, String newParam) {
this.companyName = companyName;
}
インタフェースの抽出
適用対象
クラス
効能
既存クラスからインタフェースを作成する.
既存のクラスの任意の public メソッドを選択して,インターフェースを作成でき,作成したインターフェースが自動的にimplements
に指定され,メソッドには@Override
アノテーションが付与される.
public class Refactoring {
private String companyName = "";
public String getCompanyName() {
return companyName;
}
}
public interface RefactoringInterface {
String getCompanyName();
}
public class Refactoring implements RefactoringInterface {
private String companyName = "";
@Override
public String getCompanyName() {
return companyName;
}
}
クラスの抽出
適用対象
クラス(実質メンバ変数)
効能
メンバ変数を別クラスにまとめる.
クラスが肥大化したとき,または,IP アドレスとポート番号など関連するものを一つにまとめる時に使う.
メンバ変数だけで,メソッドは抽出できない.
オプション
抽出先をトップレベルクラス,匿名クラスから選択可能.
トップレベルクラスの場合は,別のクラスとして抽出され,
匿名クラスの場合は,同一クラス内にクラスが抽出される.
public class Refactoring {
private String companyName = "";
private String postNo = "";
public void main() {}
}
public class Refactoring {
private RefactoringData data = new RefactoringData("", "");
public void main() {}
}
public class RefactoringData {
public String companyName;
public String postNo;
public RefactoringData(String companyName, String postNo) {
this.companyName = companyName;
this.postNo = postNo;
}
}
public class Refactoring {
public static class RefactoringData {
public String companyName;
public String postNo;
public RefactoringData(String companyName, String postNo) {
this.companyName = companyName;
this.postNo = postNo;
}
}
private RefactoringData data = new RefactoringData("", "");
public void main() {}
}
パラメーターオブジェクトの導入
適用対象
メソッド
効能
任意の引数を1つのオブジェクトとしてまとめる
オプション
変更されたメソッドへの委譲として元のメソッドを保持
にチェックすると元のシグネチャーのメソッドを保持できる.
生成するパラメータオブジェクトをトップレベルクラス
or 匿名クラス
のどちらかから選択できる.
public class Refactoring {
public Refactoring(String companyName, int companyId) {
this.companyName = companyName;
this.companyId = companyId;
}
}
public class Refactoring {
public Refactoring(RefactoringParameter parameterObject) {
this.companyName = parameterObject.companyName;
this.companyId = parameterObject.companyId;
}
}
public class RefactoringParameter {
public String companyName;
public int companyId;
public RefactoringParameter(String companyName, int companyId) {
this.companyName = companyName;
this.companyId = companyId;
}
}
public class Refactoring {
/**
* @deprecated {@link #Refactoring(RefactoringParameter)} の代用
*/
public Refactoring(String companyName, int companyId) {
this(new RefactoringParameter(companyName, companyId));
}
public Refactoring(RefactoringParameter parameterObject) {
this.companyName = parameterObject.companyName;
this.companyId = parameterObject.companyId;
}
}
public class RefactoringParameter {
// パラメータオブジェクトは同じ
}
パラメーターの導入
適用対象
メソッド内の変数,定数
効能
メソッド内の変数,定数をメソッドの引数に変更する
public String getCompanyName() {
return "prefix_" + companyName;
}
public String getCompanyName(String prefix) {
return prefix + companyName;
}
スーパークラスの抽出
適用対象
クラス
効能
既存のクラスからスーパークラスを作成する.
既存のクラスから任意のメンバ変数,メソッドを指定してスーパークラスを作成でき,自動的にextends
指定される.
public class Refactoring {
private String companyName = "";
public String getCompanyName() {
return companyName;
}
}
public class RefactoringSuper {
private String companyName = "";
public String getCompanyName() {
return companyName;
}
}
public class Refactoring extends RefactoringSuper {
}
プルアップ/プッシュダウン
適用対象
スーパークラス/サブクラスのメンバ変数,メソッド
効能
スーパークラス/サブクラス間でメンバ変数,メソッドを移動する
プルアップがサブクラス → スーパークラスへの移動,
プッシュダウンがスーパークラス → サブクラスへの移動.
オプション(プッシュダウン時)
abstract宣言を残す
を選択するとスーパークラスに移動したメソッドを abstruct として残すことができる.
public class SuperClass {
private String companyName = "";
public String getCompanyName() {
return companyName;
}
}
public class Refactoring extends SuperClass {
}
public class SuperClass {
}
public class Refactoring extends SuperClass {
private String companyName = "";
public String getCompanyName() {
return companyName;
}
}
public abstract class SuperClass {
public abstract String getCompanyName();
}
public class Refactoring extends SuperClass {
private String companyName = "";
public String getCompanyName() {
return companyName;
}
}
ファクトリーの導入
適用対象
コンストラクタ
効能
コンストラクタを static なファクトリメソッドに置き換える.
対象のクラスをシングルトンクラスにしたい場合や,生成方法を柔軟に変更したい場合に利用する.
public class Refactoring {
private String companyName = "";
public Refactoring(String companyName) {
this.companyName = companyName;
}
}
public class Refactoring {
public static Refactoring createRefactoring(String companyName) {
return new Refactoring(companyName);
}
private String companyName = "";
private Refactoring(String companyName) {
this.companyName = companyName;
}
}
使用可能な場合にスーパータイプを使用
適用対象
クラス(実質他のメソッド内での参照)
効能
スーパークラスで代用可能な場合に,スーパークラスに置き換える.
以下の例では Use クラスの宣言が置き換わっているが,リファクタリングを実行するのは SubClass です.
※ リファクタリングを実行したクラスに変化はない.
サブクラスを削除する前の処理として使うんだと思います.(滅多に使わない)
public class SuperClass {
protected String companyName = "";
public String getCompanyName() {
return companyName;
}
}
public class Refactoring extends SuperClass {
}
public class Use {
public void main() {
Refactoring instance = new Refactoring("", 0);
System.out.println(instance.getCompanyName());
}
}
public class Use {
public void main() {
SuperClass instance = new Refactoring("", 0);
System.out.println(instance.getCompanyName());
}
}
型を新規ファイルに移動
適用対象
匿名クラス(インナークラス)
効能
匿名クラスを別ファイルに移動する
public class Refactoring {
private String companyName = "";
private int companyId = 0;
public static class RefactoringParameter {
public String companyName;
public int companyId;
public RefactoringParameter(String companyName, int companyId) {
this.companyName = companyName;
this.companyId = companyId;
}
}
public Refactoring(RefactoringParameter parameterObject) {
this.companyName = parameterObject.companyName;
this.companyId = parameterObject.companyId;
}
}
public class Refactoring {
private String companyName = "";
private int companyId = 0;
public Refactoring(RefactoringParameter parameterObject) {
this.companyName = parameterObject.companyName;
this.companyId = parameterObject.companyId;
}
}
public class RefactoringParameter {
public String companyName;
public int companyId;
public RefactoringParameter(String companyName, int companyId) {
this.companyName = companyName;
this.companyId = companyId;
}
}
宣言された型の一般化
適用対象
メンバ変数の型,メソッドの戻り値の型,変数の型
効能
型をスーパークラスの型に置き換える.
String 型 →Object 型のようにスーパークラスに変更できる.
ただし,他のクラスで String 型として参照していて互換性がない場合は変更できない.
public String getCompanyName() {
return companyName;
}
public Object getCompanyName() {
return companyName;
}
最後に
Eclipseのリファクタリングについてまとめて,初めて知る機能がたくさんありました.
個人的に使いどころが分からなかったものはの数が少なくなっています.便利な使いどころがあれば教えてください.
100%安全とは言えませんが,変更してみてエラーの出た箇所を手で直す方法よりは圧倒的に速く,かつ,安全にリファクタリングできるのでどんどんリファクタリングしていきましょう.