
Page | 1 | 2 |
誰でもわかる リファクタリング入門
|
目的を決めてリファクタリングしよう
「リファクタリングの目的も,その方法もだいたいわかった。でも,いつリファクタリングを行うべきか?」――そうですね。前述のようにリファクタリングはユーザーのためではなく,プログラマのために行うものです。ユーザーから「今からリファクタリングをしてほしい」なんていう要望はけっして出てきません。いつリファクタリングするかの判断は,プログラマ次第というわけです。 ところがデバッグ,機能追加,パフォーマンス改善に比べると,プログラムの構造をきれいにして,拡張性や再利用性を高めることは実際にはかなりアイマイです。「きれいかどうか」という判断には多分に主観が入りますし,拡張性や再利用性も,何をどこまで考慮するかによってずいぶん基準が変わります。こうした作業はある意味キリがありません(注9)。
したがって,リファクタリングをいつ行うか,どこまでするかという判断は案外難しいものと言えます。そこで筆者がお勧めするのが「目的を決めて」リファクタリングをすることです。特に有効なのは,機能拡張をする必要が出た時にリファクタリングをすることです。すでに動いているプログラムに機能を追加しようとしたときに「そのままでは修正がしにくい」「プログラムのここがこうなっていたら良かったのに」と思ったときが,リファクタリングの絶好のタイミングなのです(注10)。
実践!リファクタリング
それでは,実際にJavaのサンプル・プログラムを使ってリファクタリングを体験してみましょう。取り上げるのは,バイオリズムのプログラムです。 バイオリズムとは,人間の身体,感情,知性の三つの要素について,生まれてからの周期的な変化を表したものです。それぞれの要素の数値(ポイント)と,その組み合わせで「調子が良いのか悪いのか」を判断します(注11)。 ある日のバイオリズムを計算するには,まず誕生日からその日までの日数を計算します。次に三つの要素ごとに異なる周期の変化を(図4)の式で算出します。
|
身体 :p=sin(term÷23×2π) 感情 :s=sin(term÷28×2π) 知性 :i=sin(term÷33×2π) |
(図4) termは誕生日からその日までの日数 |
サンプル・プログラムでは,誕生日を入力するとプログラム実行日のバイオリズムを計算する仕様になっています。
●バイオリズムを計算するSimpleBiorhythmクラスコード(SimpleBiorhythm.java)
リスト1
import java.util.Calendar;
public class SimpleBiorhythm {
public static void main(String[] args) {
int year0 = Integer.parseInt(args[0]);
int month0 = Integer.parseInt(args[1]);
int day0 = Integer.parseInt(args[2]);
Calendar calendar = Calendar.getInstance();
int year1 = calendar.get(Calendar.YEAR);
int month1 = calendar.get(Calendar.MONTH) + 1;
int day1 = calendar.get(Calendar.DAY_OF_MONTH);
// 日数計算
int days0 = calculateDaysInAD(year0, month0, day0);
int days1 = calculateDaysInAD(year1, month1, day1);
int term = days1 - days0;
// バイオリズムを計算
double bio0 = Math.sin(
(double) term / 23 * 2 * Math.PI);
double bio1 = Math.sin(
(double) term / 28 * 2 * Math.PI);
double bio2 = Math.sin(
(double) term / 33 * 2 * Math.PI);
// 判定
System.out.println("今日(" + year1 + "/" + month1 +
"/" + day1 + ")のバイオリズム:");
System.out.println("身体リズム:" + (int) (bio0 * 100));
System.out.println("感情リズム:" + (int) (bio1 * 100));
System.out.println("知性リズムl:" + (int) (bio2 * 100));
}
private static int calculateDaysInAD(int year, int month, int day) {
// 月ごとの日数
int[] monthTerm = new int[] {
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// それぞれ西暦1年1月1日からの日数を得る
int days = (year - 1) * 365 + (year - 1) / 4 - (year - 1) / 100 +
(year - 1) / 400;
// 前月までの日数を加算
for (int i = 0; i < month - 2; i++) {
days += monthTerm[i];
}
if ((month > 2) && ((year % 4 == 0) &&
(year % 100 != 0) || (year % 400 == 0))) {
days++;
}
// 日を加算
days += day;
return days;
}
}
リスト1がリファクタリング前のJavaプログラム(SimpleBiorhythm.java)です。(1)mainメソッドと(2)calculateDaysInADメソッドの二つで構成しています。日付,sin関数,円周率(π)の計算には,java.util.Calendarクラスやjava.lang.Mathクラスを利用しています。このプログラムの実行時引数に誕生日を指定すると,今日のバイオリズムの得点が表示されるというわけです(注12)。例えば1975年4月22日を引数とすれば,実行結果は(図5)のようになります。プログラムの動作には,何の問題もありません。

しかしリスト1のプログラムを眺めていたら,気になる点がいくつか見つかりました。また,どうせ計算するなら,毎日のバイオリズムの数値をデスクトップ画面に表示したり,数日分のバイオリズムをグラフで見たい,といったさまざまなアイデアも沸いてきました。
そう,先に述べましたね。「機能拡張したいと思ったときが,リファクタリングの絶好のタイミング」です。そこで,こうした対応が簡単にできることを目標にリファクタリングを行っていきましょう。
リファクタリング1 理解しやすい変数名に変更する
リスト1を眺めて最初に気付くのは,mainメソッドの中に変数がたくさんあることでしょう。year, month, dayについては,それぞれ末尾に0と1がつく変数があります。ほかにも日数を表すdays0とdays1や,バイオリズムの三つの要素を表すbio0,bio1,bio2などがあり,全体的にごちゃごちゃした感じです。
そこで,少しでもわかりやすくなるように,こうした変数の名前を変更してみましょう(注13)。まずは最初に登場する三つの変数year0,month0,day0から手をつけましょう。これらの変数は実行時の引数として渡されますが,ロジックを追いかけてみると,誕生日を表していることがわかります。そこでそれぞれの名前を,birthYear,birthMonth,birthDay に変更します(リスト2)。
リスト2
public class SimpleBiorhythm {
public static void main(String[] args) {
int birthYear = Integer.parseInt(args[0]);
int birthMonth = Integer.parseInt(args[1]);
int birthDay = Integer.parseInt(args[2]);
Calendar calendar = Calendar.getInstance();
int year1= calendar.get(Calendar.YEAR);
int month1= calendar.get(Calendar.MONTH) + 1;
int day1= calendar.get(Calendar.DAY_OF_MONTH);
// 日数計算
int days0= calculateDaysInAD(birthYear, birthMonth, birthDay);
int days1 = calculateDaysInAD(year1, month1, day1);
~ 略 ~
この修正が終わった段階で,SimpleBiorhythm.javaをコンパイルしてテストを実行します。テストは図5と同じ条件でプログラムを実行し,同じ結果になればOKと判断します。この例では特にJUnitなどのテスティング・フレームワークを用いませんが,お持ちの方はぜひ使ってみてください。引き続いて,次の変数についても同様の手順を繰り返して変更します。
・ year1,month1,day1の名前を,それぞれcurrentYear,currentMonth,currentDayに変更する
・ days0,days1の名前を,それぞれbirthDays,currentDaysに変更する
・ bio0,bio1,bio2の名前を,それぞれphysicalPoint,emotionalPoint,intellectualPointに変更する
リファクタリング2 複雑な条件判定をわかりやすくする
もう少しリスト1を眺めてみましょう。(2)のcalculateDaysInADメソッドはどうでしょうか。これは指定された年月日について西暦1年1月1日からの日数を求める計算を行っています。このうち,わかりにくいのが(3)の条件判定です。
この条件判定の後半部分では西暦の年を4,100,400でわり算したときの余りを調べていますね。これが何を意味しているかわかりますか?
そう,ここではその年がうるう年かどうかを調べているのです。ちょっと見ただけではわかりませんね。そこで,そのものズバリのisLeapYear(うるう年かどうかを判定する)という名前のメソッド(注14)に変更してしまいましょう。これは「条件記述の分解」と呼ばれるリファクタリングです。 まずは,うるう年を判定するisLeapYearメソッドを新たに作ります。判定するだけですから,戻り値はboolean型にして,うるう年なら真,違うなら偽となるようにします。判定ロジックは,リスト1のうるう年判定の部分をそのままコピーすればいいでしょう(リスト3)。
リスト3
//うるう年かどうかを判定する
private static boolean isLeapYear(int year) {
return ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0));
}
この修正が終わった段階で,SimpleBiorhythm.javaをコンパイルしてテストを実行します。テストは図5と同じ条件でプログラムを実行し,同じ結果になればOKと判断します。この例では特にJUnitなどのテスティング・フレームワークを用いませんが,お持ちの方はぜひ使ってみてください。引き続いて,次の変数についても同様の手順を繰り返して変更します。
リスト4
private static int calculateDaysInAD(int year, int month, int day) {
~ 略 ~
if ((month > 2) && isLeapYear(year)) {
days++;
}
~ 略 ~
}
とてもすっきりしてわかりやすくなりましたね。リスト4は,2月を過ぎてその年がうるう年ならば,2月29日の1日分を日数として加算することを表しています。これならコメントを書かなくてもプログラムを読むだけで意味が伝わります。コメントを軽視してはいけませんが,コメントがなくても理解できるようなプログラムが「わかりやすい」プログラムですからね。それでは,再びコンパイルとテストを実行して,結果が同じになることを確認します。
リファクタリング3 主要な処理をメソッドとして抽出する
ここまでのリファクタリングを行った結果,プログラムはだいぶ理解しやすくなりました。でもまだ「それだけ」です。今回のリファクタリングの目標は,バイオリズムの値をデスクトップ画面に表示したり,グラフィックス表示をしたりすることです。それを達成するまで,もう少しがんばらなくてはいけません。
これらの目標を達成するには,どうしたらいいでしょう。ポイントとなるのは,バイオリズムの値を単純に取り出せるようにすることです。そうすれば,後はグラフィックス表示などを行う別のプログラムにバイオリズムの結果を渡せます。ところが現時点のプログラムでは,バイオリズムの計算とコンソールへの表示(System.out.println~の部分)の両方がmainメソッドの中で行われています。これでは,計算結果だけ取り出したいのに,プログラムを実行するたびにいちいちコンソールに結果が表示されてしまいます。そこでmainメソッドから,バイオリズムの計算処理だけを取り出して,別のメソッドとして独立させましょう。これは「メソッドの抽出」と呼ばれる最も代表的なリファクタリングです。 まず先ほどと同様に,バイオリズムの計算処理だけを行うメソッドを新しく追加します。名前はcalculateBiorhythmメソッドでいいでしょう。そのものズバリですよね。バイオリズムの三つの値を返す必要があるため,戻り値はdouble型の配列とします。ロジックの中身は,mainメソッドから使えそうな部分をコピーして作ります(リスト5)。
リスト5
// バイオリズムの値を計算する
private static double[] calculateBiorhythm(
int birthYear, int birthMonth, int birthDay,
int currentYear, int currentMonth, int currentDay) {
// 日数計算
int birthDays = calculateDaysInAD(birthYear, birthMonth, birthDay);
int currentDays = calculateDaysInAD(
currentYear, currentMonth, currentDay);
int term = currentDays - birthDays;
// バイオリズムを計算
double physicalPoint = Math.sin(
(double) term / 23 * 2 * Math.PI);
double emotionalPoint = Math.sin(
(double) term / 28 * 2 * Math.PI);
double intellectualPoint = Math.sin(
(double) term / 33 * 2 * Math.PI);
return new double[] {
physicalPoint, emotionalPoint, intellectualPoint};
}
ここでもコンパイルとテストを実行してきちんと動くかどうかを確かめましょう。次に,新しく追加したcalculateBiorhythmメソッドを利用するように,mainメソッドを書き換えます(リスト6)。もちろんコンパイルとテストの実行を忘れてはいけません。
リスト6
public static void main(String[] args) {
// 誕生日
int birthYear = Integer.parseInt(args[0]);
int birthMonth = Integer.parseInt(args[1]);
int birthDay = Integer.parseInt(args[2]);
// 今日
Calendar calendar = Calendar.getInstance();
int currentYear = calendar.get(Calendar.YEAR);
int currentMonth = calendar.get(Calendar.MONTH) + 1;
int currentDay = calendar.get(Calendar.DAY_OF_MONTH);
// バイオリズムを計算
double[] points = calculateBiorhythm(birthYear, birthMonth, birthDay,
currentYear, currentMonth, currentDay);
// 判定
System.out.println("今日(" + currentYear + "/" + currentMonth +
"/" + currentDay + ")のバイオリズム:");
System.out.println("身体リズム:" + (int) (points[0] * 100));
System.out.println("感情リズム:" + (int) (points[1] * 100));
System.out.println("知性リズムl:" + (int) (points[2] * 100));
}
リファクタリング4 戻り値をオブジェクトに置き換える
ここまでで,バイオリズムの計算部分をメソッドとして抽出できました。これで,グラフィックス表示などにも対応できるようになったと言えそうです。でも,もう少し改善したいところがあります。それは,新たに作ったcalculateBiorhythmメソッドの戻り値です。リスト5ではdouble型の配列を戻しています。この配列には,0番目の要素に「身体」,1番目に「感情」,2番目に「知性」を格納しています。しかし,こうした配列の順序に意味を持たせる方法は,プログラマが順番を間違えてもコンパイル時にチェックできないため好ましくありません。 そこでこの部分もリファクタリングしてしまいましょう。こういうときには,受け渡し専用のクラスを新たに作るというテクニックが役に立ちます。これは,「引数オブジェクトの導入」というリファクタリングの変形です。
具体的には,バイオリズムの三つの値を保持するクラスを別に作り,それをcalculateBiorhythmメソッドの戻り値として利用します。 それでは,まずバイオリズムの値を保持するクラスを新しく作りましょう。クラスの名前はバイオリズムの情報を保持するという意味でBiorhythmInfo(BiorhythmInfo.java)とします。バイオリズムの三つの値をフィールドに持って,それを取得するメソッド(getPhysicalPoint( ),getEmotionalPoint( ),getIntellecturalPoint( ))を定義するだけのクラスです(リスト7)
リスト7
public class BiorhythmInfo {
private final double physicalPoint;
private final double emotionalPoint;
private final double intellectualPoint;
public BiorhythmInfo(double physicalPoint, double emotionalPoint,
double intellectualPoint) {
this.physicalPoint = physicalPoint;
this.emotionalPoint = emotionalPoint;
this.intellectualPoint = intellectualPoint;
}
// 身体リズムのポイントを取得
public double getPhysicalPoint() {
return this.physicalPoint;
}
// 感情リズムのポイントを取得
public double getEmotionalPoint() {
return this.emotionalPoint;
}
// 知性リズムのポイントを取得
public double getIntellectualPoint() {
return this.intellectualPoint;
}
}
だいぶしつこくなってきましたが,新たに作ったBiorhythmInfo.javaもコンパイルし,コンパイル・エラーが起きないことを確認します。そして,BiorhythmInfoクラスをcalculateBiorhythmメソッドの戻り値に変更し(リスト8),コンパイルとテストを実行します。
リスト8
public class SimpleBiorhythm {
private static BiorhythmInfo calculateBiorhythm(
int birthYear, int birthMonth, int birthDay,
int currentYear, int currentMonth, int currentDay) {
~ 略 ~
return new BiorhythmInfo(
physicalPoint, emotionalPoint, intellectualPoint);
}
public static void main(String[] args) {
~ 略 ~
// バイオリズムを計算
BiorhythmInfo[] points = calculateBiorhythm(birthYear, birthMonth, birthDay,
currentYear, currentMonth, currentDay);
// 表示
System.out.println("今日(" + currentYear + "/" + currentMonth +
"/" + currentDay + ")のバイオリズム:");
System.out.println("身体リズム:" +
(int) (biorhythmInfo.getPhysicalPoint() * 100));
System.out.println("感情リズム:" +
(int) (biorhythmInfo.getEmotionalPoint() * 100));
System.out.println("知性リズムl:" +
(int) (biorhythmInfo.getIntellectualPoint() * 100));
~ 略 ~
リファクタリング5 クラスの責任を切り分ける
バイオリズム計算処理を独立したメソッドにし,戻り値も使いやすくなりました。しかしまだバイオリズム計算処理が,コンソールに値を表示するSimpleBiorhythmクラスの一部になっているため,やや中途半端な感じです。最後の仕上げとして,バイオリズム計算処理の部分を専用のクラスとして独立させてしまいましょう。これは「クラスの抽出」というリファクタリングです(注15)。 新しいクラスの名前は,バイオリズムを計算するわけですから,そのものズバリのBiorhythmCalculator(BiorhythmCalculator.java)としましょう。ここにSimpleBiorhythmクラスからバイオリズム計算に必要なロジックをコピーします。また,初期化の際に誕生日を引数として受け取るようにします(リスト9)。また,このBiorhytmCalculatorを使うように,SimpleBiorhythmクラスも変更します(リスト10)。
リスト9
public class BiorhythmCalculator {
private final int birthYear;
private final int birthMonth;
private final int birthDay;
// コンストラクタ
public BiorhythmCalculator(int birthYear, int birthMonth, int birthDay) {
this.birthYear = birthYear;
this.birthMonth = birthMonth;
this.birthDay = birthDay;
}
// 日数計算
private static int calculateDaysInAD(int year, int month, int day) {
int[] monthTerm = new int[] {
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int days = (year - 1) * 365 + (year - 1) / 4 - (year - 1) / 100 +
(year - 1) / 400;
for (int i = 0; i < month - 2; i++) {
days += monthTerm[i];
}
if ((month > 2) && isLeapYear(year)) {
days++;
}
days += day;
return days;
}
private static boolean isLeapYear(int year) {
return ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0));
}
// バイオリズム計算
public BiorhythmInfo calculateBiorhythm(int year, int month, int day) {
int birthDays = calculateDaysInAD(
this.birthYear, this.birthMonth, this.birthDay);
int currentDays = calculateDaysInAD(
year, month, day);
int term = currentDays - birthDays;
double physicalPoint = Math.sin(
(double) term / 23 * 2 * Math.PI);
double emotionalPoint = Math.sin(
(double) term / 28 * 2 * Math.PI);
double intellectualPoint = Math.sin(
(double) term / 33 * 2 * Math.PI);
return new BiorhythmInfo(
physicalPoint, emotionalPoint, intellectualPoint);
}
}
リスト10
import java.util.Calendar;
public class SimpleBiorhythm {
public static void main(String[] args) {
int birthYear = Integer.parseInt(args[0]);
int birthMonth = Integer.parseInt(args[1]);
int birthDay = Integer.parseInt(args[2]);
Calendar calendar = Calendar.getInstance();
int currentYear = calendar.get(Calendar.YEAR);
int currentMonth = calendar.get(Calendar.MONTH) + 1;
int currentDay = calendar.get(Calendar.DAY_OF_MONTH);
// バイオリズム計算
BiorhythmCalculator calculator =
new BiorhythmCalculator(birthYear, birthMonth, birthDay);
BiorhythmInfo biorhythmInfo = calculator.calculateBiorhythm(
currentYear, currentMonth, currentDay);
// 判定
System.out.println("今日(" + currentYear + "/" + currentMonth +
"/" + currentDay + ")のバイオリズム:");
System.out.println("身体リズム:" +
(int) (biorhythmInfo.getPhysicalPoint() * 100));
System.out.println("感情リズム:" +
(int) (biorhythmInfo.getEmotionalPoint() * 100));
System.out.println("知性リズムl:" +
(int) (biorhythmInfo.getIntellectualPoint() * 100));
}
}
もちろんここでもコンパイルとテストを行って,最初と同じ結果になることを確認します。これで,最終的にクラスは当初の一つだけから,SimpleBiorhythmクラス,BiorhythmInfoクラス,BiorhytmCalculatorクラスの三つになりました。リスト10の(1)の部分を見ると,バイオリズムの計算がシンプルかつスマートに実現されていることがわかりますね。コメントがなくてもわかりやすいですし,これならデスクトップ表示やグラフィックス表示のためのプログラムも簡単に作れそうです。拡張性や再利用性というリファクタリングの目的は,達成できたと言っていいでしょう(注16)。
ではこの辺で,リファクタリング作業は一段落しましょう。まだ直したいところも見つかりそうですが,プログラムを拡張する準備もできたようですから,そろそろそちらの作業に取りかかりましょう。もし後からリファクタリングをする必要性が出てきたら,そのときの状況に応じてまた手をつければいいのです。
リファクタリングで既存プログラムを有効活用しよう
いかがだったでしょうか? 「プログラムの設計は,最初に作るときに行い,後から変えるのはよくない」「正しく動いているコードには触るな」――従来はそれが常識でした。リファクタリングは,そうしたタブーを破る考え方と言ってもいいでしょう。
リファクタリングのテクニックは,一つひとつは単純なものです。しかし既存のコードに影響を及ぼさないように少しずつ修正を加えていくスタイルは,意外に強力な武器となります。動いているプログラムを有効活用することも重要なのです。
プログラムを長持ちさせるためには,お肌と同じように日頃の手入れが大切です。プログラムの手入れを怠るとそのうちに荒れ放題になってしまいます。リファクタリングのテクニックを身に付けると,皆さんのプログラミング・スキルもきっとワンランク上になると思いますよ。
(注9)せっかくリファクタリングをしても,将来その部分に修正が入らなければ,あまり意味がない。
(注10)もちろん特別なタイミングがなくても,何かのきっかけでリファクタリングを行うのは悪いことではない。
(注11)要素の数値(ポイント)が0に近い日は,調子が不安定となるため要注意日と言われている。
(注12)本来ならば,実行時引数が正しく設定されているかどうかを判定するロジックを組み込む必要があるが,今回は省略した。
(注13)このリファクタリングは,前述のカタログには載っていない。しかしこれとよく似た「メソッド名の変更」というリファクタリングがある。
(注14)グレゴリオ暦を表すjava.util.GregorianCalendarクラスに同名のメソッドがあるが,今回のサンプルでは利用していない。
(注15)Javaではクラスとして独立させることにより,一般的にファイルも別ファイルになるため,再利用しやすくなる。
(注16)記事で紹介しているバイオリズムのJavaプログラムは,日経ソフトウエアのWebサイト(http://software.nikkeibp.co.jp/)からダウンロードできる。リファクタリング以前のものから,五つのリファクタリングそれぞれの段階のコードもすべて収録しているので,ぜひリファクタリングの過程を実感してほしい。
Page | 1 | 2 |
