前回の話 では、変数の生存期間をできるだけ短くする話をしましたが、今回はメソッドの話です。同じリファクタリングのなかで指摘したお話です。
例1(メソッドのリファクタリング直前の例)
public static void DoSomething()
{
var a = 0;
var b = 1;
var c = 0;
c = a + b; // 何かの計算
a = b;
b = c;
Console.WriteLine($"c = {c}"); // 計算した値を使用する。
c = a + b; // 何かの計算
a = b;
b = c;
Console.WriteLine($"c = {c}"); // 計算した値を使用する。
// 数回繰り返す(省略)
}
この例では、あるメソッドのなかで繰り返し同じ計算をしています。実際のコードではもっと複雑な条件分岐や、他の機能との組み合わせの中で呼ばれる少し複雑な計算でしょうが、このメソッドの中で何度も実行できる計算であることがわかっています。これをリファクタリングする場合を考えてみます。
例2(Visual Studio 標準のリファクタリングを使用した、あまりよくない例)
private static int NewMethod(ref int a, ref int b)
{
int c;
c = a + b; // 何かの計算
a = b;
b = c;
return c;
}
public static void DoSomething()
{
var a = 0;
var b = 1;
var c = 0;
c = NewMethod(ref a, ref b); // 何かの計算
Console.WriteLine($"c = {c}"); // 計算した値を使用する。
c = NewMethod(ref a, ref b); // 何かの計算
Console.WriteLine($"c = {c}"); // 計算した値を使用する。
// 数回繰り返す(省略)
}
Visual Studio の機能で「メソッドの抽出」というリファクタリング機能が使えます。最近のVisual Studio では、メソッドにしたいコードを選択した後、Ctrl + . のショートカットで呼び出すことができます。この例では NewMethod を作成しました。最初の1つのメソッド呼び出しは、リファクタリング機能によって自動的に作成されるので、他の行もメソッドを呼び出すようにコピペで変更します。
これは良くみられるリファクタリングですが、よくない点が2つあります。
1つ目の良くない点は、引数が複数で少し複雑なことです。この例では2つしか引数がありませんが、きっと実際のメソッドの抽出では、引数が5つ以上、10や20になることもあるでしょう。これは非常に扱いづらく、順番を間違えるだけでバグになるので、何度も同じ引数を指定させるのは不便です。また ref を使ってメソッドの呼び出し元の変数の値を書き換えるのは、静的コード分析では非推奨です。これも同様に扱いが難しく、バグになりやすいためです。ref はTryParseなどそれ以外では実装が難しく、かつ、多くの人がよく知っている使われ方に限って使用されます。この計算には、もともとのメソッドで使用している変数の値を使うことが必須であるため、新しいメソッドが引数を持つのはやむを得ない様に感じるかもしれませんが、これは単にメソッドを作るだけでなく、クラスを作ることで解決できます。(後述します)
2つ目の良くない点は、新しく作られたメソッドが、もともとのメソッドから遠く離れてしまうということです。つまり、もともとのメソッドからだけでなく、他のメソッドからも使うことができます。もし、本当にそうしたい場合にはそれでもかまいません。そのための適切な準備(単体テストを作ったり、もっと適切な名前を付けたり、XMLコメントでメソッドの使い方を書いたり)をして、新しいメソッドをいろんなところから使えるようにします。これはなかなかにハードルの高い(コストのかかる)作業です。もしそうではなくて、単にもともとのメソッドで何度か繰り返して同じ処理をしたいだけであれば、新しいメソッドを作る代わりに、ラムダ式を作ることでこの問題を解決できます。ラムダ式では、引数を作ることも不要なので1つ目の問題を解決することもできます。(後述します)
例3(クラスを作る例)
public class SomethingValue
{
private int a = 0;
private int b = 1;
public int GetNewValue()
{
var c = this.a + this.b; // 何かの計算
this.a = this.b;
this.b = c;
return c;
}
}
public static void DoSomething()
{
var s = new SomethingValue();
var c = 0;
c = s.GetNewValue(); // 何かの計算
Console.WriteLine($"c = {c}"); // 計算した値を使用する。
c = s.GetNewValue(); // 何かの計算
Console.WriteLine($"c = {c}"); // 計算した値を使用する。
// 数回繰り返す(省略)
}
メソッドが複数の引数を必要としていて、かつそれが ref で呼び出し元の値を書き換える必要があるということは、そのメソッドは特定の変数を自分で持っていなければならない。つまり、クラスのプロパティ(あるいはメンバ変数)に状態を保持して、それを使用して動作るするメソッドを作るべきということがわかります。
クラスには引数の代わりに2つの変数を用意し、新しいメソッド GetNewValue は自身の変数を使用して動作するようにします。引数がなくなったため、メソッドの使い方も非常にシンプルになりました。また、クラスのメンバ変数は private で宣言されているので、他のコードのよって値を壊される心配がありません。誤った値を引数に指定する危険性もないので、バグになりにくと言えます。
実際のコードではたくさんの値をプロパティやメンバ変数として扱う必要があり、またそれらの値が本当にこのクラスの持ち物か、あるいは別のクラスのプロパティとすべきなのか?複雑な機能の中で十分に考えなければならないため、クラスの設計はとても難しいと言えます。また、前述の通り、クラスとして作成したからには、今回の使用箇所以外でも、適切に使用できるように十分にテストを実施(単体テストを実装)しなければならないでしょう。クラスの名前ももっと具体的に扱う値と実行したい計算を表す名前に変更します。もちろんクラス専用のファイルを作成します。
エンタープライズの開発現場では、十分なクラスの設計をすることができずに、実際のリファクタリングはメソッドの抽出にとどまることが多いでしょう。そのため、どうしても引数の多いメソッドが乱立し、また単独の目的にしか使われない(使えない)ケースが多くみられるようになってしまいます。数回しか呼ばれないメソッドが数十から数百も作られてしまうと、コードの保守にも非常にコストがかかるようになってしまいます。単にメソッドを作るだけでなく、適切にクラス分けできるかどうか?は非常に重要なポイントです。もしこのようなクラス設計が難しければ、ラムダ式を作る方がお勧めです。
例4(ラムダ式を作る例)
public static void DoSomething4()
{
var a = 0;
var b = 1;
var c = 0;
Func<int> getNewValue = () => {
int r = a + b; // 何かの計算
a = b;
b = r;
return r;
};
c = getNewValue(); // 何かの計算
Console.WriteLine($"c = {c}"); // 計算した値を使用する。
c = getNewValue(); // 何かの計算
Console.WriteLine($"c = {c}"); // 計算した値を使用する。
// 数回繰り返す(省略)
}
この例ではクラスやメソッドを作る代わりに、もっとシンプルに、ラムダ式を作成しています。ラムダ式は、変数に代入できるメソッドと考えて問題ありません。ただし、メソッドを宣言するときと違って、メソッドの名前を付けないので、代入された変数を扱える範囲でだけ使用することができます。変数の型は Action<T> や Func<T> などです。 ここでは int を戻り値に返すラムダ式を宣言しているので Func<int> の変数に代入しています。
このラムダ式の特徴として、引数を使わなくても、ラムダ式が宣言されているブロック(もともとのメソッド)で宣言されている変数をそのまま直接使用することができます。この例ではラムダ式のメソッド部分の外にある a, b を直接使用しています。書き換えた点としては、ラムダ式の中で使用する変数(もともとはc)を、外の変数 c と区別するために、名前を r に変えたことだけです。このおかげで、とてもシンプルにラムダ式を記述することができます。ラムダ式を実行するコードも引数を指定しないので、単に変数名に () を付けて呼び出すだけです。
またこのラムダ式の良い点として、このラムダ式を宣言したメソッドの外では使用できない。つまり、ラムダ式のコードが呼び出し元のメソッドから遠く離れていかないということが、とても重要なポイントです。エンタープライズ開発の現場では、メソッドの順番を明確に管理しない場合も多く、単純に2つのメソッドの定義されている個所がファイルの中で離れてしまって保守しずらくなるということがあります。たとえそうでなくても、ラムダ式は他のメソッドから呼び出されることが無いので、当初の目的の範囲を超えて、再利用可能になるようなテストをみっちり行う必要もありませんし、使い方のコメントをたくさん書く必要もありません。これは大きなコスト削減でありリスク回避となります。
まず最初は、ラムダ式を作成することを考え、後で、本当に必要になったときにだけ、他のメソッドからも呼び出せるようにクラスやメソッドを作るようにしてほしいのです。