あなたはとある業務用アプリケーションの開発・保守を任されています。
このアプリケーションはC#で記述されており、
とある企業におけるプロジェクト(Project
)の管理を主目的としています。
プロジェクトには何名かの社員がアサインされており(AssignedStaffs
)、
プロジェクト内には必ずマネージャーが1名存在します(ManagerStaffId
)。
大まかなイメージとしては以下のようなコードになっています:
public class Staff
{
public String Id {get; set;}
public String Name {get; set;}
...
}
public class Project
{
public ArrayList AssignedStaffs {get; set;}
public String ManagerStaffId {get; set;}
...
public Project()
{
this.AssignedStaffs = ...;
}
}
さて、とある事情により、プロジェクトのマネージャーの名前を使う場面があちこちで出てきました。
現状の構成ではマネージャーの名前を得ることにも一苦労しそうです。
なのでマネージャーの名前を返す Project.GetManagerName
を追加することにしましょう。
public String GetManagerName()
{
// TODO: 実装する。
return "";
}
これは簡単なタスクです。
せっかくなので、改修作業の一連の流れを把握してもらうための例題として、
このタスクは新しく開発チームに加わったメンバーに担当してもらうことにしました。
30分後 ──
新メンバーがコードレビューを要請してきました。
できあがったコードを見てみましょう:
public String GetManagerName()
{
var i = 0;
while (i < this.AssignedStaffs.Count)
{
if (((Staff)(this.AssignedStaffs[i])).Id == this.ManagerStaffId)
break;
i++;
}
var managerStaff = (Staff)(this.AssignedStaffs[i]);
return managerStaff.Name;
}
これはリファクタリングのしがいがあるコードです。
ちょっと本腰を入れて直すことにしましょう。
AssignedStaffs
の型は何故か ArrayList
になっています。ArrayList
以外にも IList
などが使用されている場合もあります。
これは歴史のあるソフトウェアではよく見かける光景です。
本来ならば AssignedStaffs
の型を IEnumerable<Staff>
のような要素の型が明示された型に変更すべきです。ArrayList
は要素の型が不明なので、(0)のように、
要素を参照するときにその都度キャストする必要があります。
そのため、公開APIでこのような型を使うことは通常ならばありえません
(ソース: .NETのクラスライブラリ設計の8.3節)。
キャストは時と場合によっては必要になりますが、GetManagerName
のようなメソッド中のあちこちで使われると、
コードを読む際にキャストの箇所で思考が分断されてしまうため、
とてもよろしくありません(左から右へ読み進めているところに右から左へ戻らなければならない)。
それなのに何故 AssignedStaffs
の型が ArrayList
になっているのかというと、
このプロジェクトはジェネリックスが導入される C# 2.0 のリリースよりも早く始まったからです。
ですから AssignedStaffs
が ArrayList
であることを仮定しているコードは既にプロジェクト内に蔓延しており、
いますぐリファクタリングできる状況ではありません。
仕方がないので GetManagerName
内でどうにか対処することにしましょう。
幸い、C# 3.0 / .NET Framework 3.5以降であればEnumerable.Cast<TResult>
があります。
これを使えば ArrayList
のような要素の型が明示されていないシーケンスをIEnumerable<T>
のような要素の型が明示されたシーケンスに変換することができます。
これで this.AssignedStaffs
を IEnumerable<Staff>
に変換すれば、
あちこちでキャストをする必要はなくなります。
ただ、 Enumerable.Cast<TResult>
の戻り値は IEnumerable<TResult>
になります。IEnumerable<T>
は配列のようなものを表す型なのですが、
配列のような添字参照はできません。
ですので添字参照ができるように、Enumerable.ToList<TSource>
で戻り値を IList<Staff>
に変換しています
(本当は添字参照なんてしたくないのですが、
一度に変更する量を減らしてリファクタリングの過程を分かり易くするために敢えて Enumerable.ToList<TSource>
を使っています。
後述するように、最終的には Enumerable.ToList<TSource>
は使わなくなります)。
なお、最初のうちは
「IEnumerable<T>
なんてデータコンテナーとしては抽象的すぎて実用性ないんじゃないの」
「IEnumerable<T>
を返すぐらいなら最初から配列や IList<T>
のような添字参照ができる型で値を返してよ」
などと思われるかも知れませんが、
実のところ IEnumerable<T>
は非常に重要な意味を持っています。
これについては後日詳細を述べることにしましょう。
以上のことから、(0)のコードは以下の形にリファクタリングできます:
public String GetManagerName()
{
var assignedStaffs = this.AssignedStaffs.Cast<Staff>().ToList();
var i = 0;
while (i < assignedStaffs.Count)
{
if (assignedStaffs[i].Id == this.ManagerStaffId)
break;
i++;
}
var managerStaff = assignedStaffs[i];
return managerStaff.Name;
}
foreach
を使って添字の管理を無くすさて、キャストを消し去って多少はすっきりした GetManagerName
のコード(1)ですが、
まだまだ読み辛いものとなっています。
その原因は i
── 添字です。
本当にやりたいことは「assignedStaffs
から条件に合致する要素を見つける」ことなのですが、
(1)ではそのために添字を使って assignedStaffs
の各要素を調べています。
添字を使っていると、
ループの終了条件や添字のインクリメントで単純なミスが入り込む可能性が出てくるため、
常に注意深くコードを書く羽目になってしまいます。
そして常に注意深くコードを書くことができる人は存在しません。
という訳で添字の管理を止めましょう。
添字を管理しなければしょうもないミスが入り込む余地は自然となくなります。
これには foreach
を使います。
以上のことから、(1)のコードは以下の形にリファクタリングできます:
public String GetManagerName()
{
var assignedStaffs = this.AssignedStaffs.Cast<Staff>().ToList();
var i = 0;
foreach (var s in assignedStaffs)
{
if (s.Id == this.ManagerStaffId)
break;
i++;
}
var managerStaff = assignedStaffs[i];
return managerStaff.Name;
}
(2)で assignedStaffs
の各要素を調べるために添字を管理することを止めました。
しかし添字を使っているところがまだ残っています。
それは条件に合致した要素を後続のコードへ渡すところです。
(2)で述べたことと同様の理由で、
やはり添字を使うと不要なミスが入り込む可能性ができてしまいます。
そもそも、条件に合致した要素を伝達するために、
添字を使って間接的に行なう必然性はありません。
見つかった要素への参照を直接使えば良いのです。
また、添字の使用を止めれば Enumerable.ToList<TSource>
を使う必要性もなくなります。
以上のことから、(2)のコードは以下の形にリファクタリングできます:
public String GetManagerName()
{
var assignedStaffs = this.AssignedStaffs.Cast<Staff>();
Staff managerStaff;
foreach (var s in assignedStaffs)
{
if (s.Id == this.ManagerStaffId)
{
managerStaff = s;
break;
}
}
return managerStaff.Name;
}
Where
を使ってループを無くす(3)で添字の管理と使用を完全に排することができました。
しかしまだ読み辛さは残っています。何故でしょうか。
それは本当にやりたいことが「条件に合致する要素を探す」ことなのに、
(3)までのコードでは本当にやりたいことを どうやるか が前面に出ているからです。
そのため、コードを見ても 何がしたいか すぐに理解できません。
例えば(2)と(3)では「 添字を使って 条件に合致する要素を探す」コードから
「添字を使って」という「どうやるか」の詳細を消し去りました。
しかしまだ改善の余地があります。
(3)の結果は「 ループを使って各要素を調べて 条件に合致する要素を探す」コードになっています。
つまり「ループを使って各要素を調べて」という「どうやるか」の詳細がまだコード上に残っているのです。
幸い、C# 3.0 / .NET Framework 3.5以降であればEnumerable.Where<TSource>
があります。
これを使えば IEnumerable<T>
から指定した条件に合致する要素のみを取り出すことができます。
ただ、条件に合致する要素は0個かも知れませんし、1個だけかも知れませんし、30個あるかも知れません。
なので Where
の戻り値は IEnumerable<T>
になっています。
ですが GetManagerName
では必ず条件に合致する要素が1つだけ存在すると仮定しています。
なので Where
で絞り込んだ結果のうち最初の要素だけを取り出せば十分です。
これには Enumerable.First<TSource>
を使います。
また、 Where
は要素を絞り込むための「条件」を引数に取ります。
この「条件」はメソッドとして表現します
(なお、このようなメソッドのことを述語(predicate)と呼ぶことがあります)。
今回の場合は isManagerStaff
というメソッドを作って、
それを Where
の引数として渡すことにしましょう。
以上のことから、(3)のコードは以下の形にリファクタリングできます:
public String GetManagerName()
{
var managerStaff = (
this.AssignedStaffs
.Cast<Staff>()
.Where(isManagerStaff)
.First()
);
return managerStaff.Name;
}
private bool isManagerStaff(Staff s)
{
return s.Id == this.ManagerStaffId;
}
さて、(4)にまで至るとコード上から「どうやるか」の詳細は消え去り、
(0)と比較するとかなり読み易くなりました。
ところが(4)では逆に問題が1つ増えてしまいました。isManagerStaff
というメソッドを定義して、
これを Where
で要素を絞り込む「条件」として渡すようにしました。
しかし isManagerStaff
なんてメソッドはGetManagerName
の中でしか使われません。
さらに言えば GetManagerName
の中でも使用箇所は1箇所のみです。isManagerStaff
が他のメソッドからも使われる「条件」ならメソッドとして定義しておく価値はありますし、
仮に1箇所でしか使われないメソッドであっても「条件」の中味が複雑ならばメソッドとして定義して意味のある名前を付けておくことに価値はあります。
しかし isManagerStaff
はどちらにも該当しません。
このようなメソッドを定義していては、
あっというまにクラスの中味が「ちょっとした」メソッドの山で埋もれてしまいます。
幸い、C# 3.0 / .NET Framework 3.5以降であれば
ラムダ式
が使えます。
ラムダ式を使えば isManagerStaff
のような「ちょっとした」メソッドをその場で記述することができます。
以上のことから、(4)のコードは以下の形にリファクタリングできます:
public String GetManagerName()
{
var managerStaff = (
this.AssignedStaffs
.Cast<Staff>()
.Where(x => x.Id == this.ManagerStaffId)
.First()
);
return managerStaff.Name;
}
(5)でかなり読み易いコードにリファクタリングできましたが、
まだ1点だけ問題があります。
それは managerStaff
です。
(5)で述べた isManagerStaff
と同様に、
この managerStaff
も一度しか参照されない変数です。managerStaff
以外にもデータを検索していたり、managerStaff
の算出過程が複雑であれば、
変数を用いて値に名前を付けることで「何がやりたいか」を黙示できます。
しかし GetManagerName
の場合はどちらにも該当しませんし、
そもそも GetManagerName
というメソッド名から何をしたいかや何を探しているかは自明です。
また、変数を使うということは名前を考えなければなりません。
一見すると些細なことのように思えますが、名前を考えるということは非常に重労働です。
そのような労力はもっと重要なコードに対して使うべきであって、managerStaff
のようなどうでもいい一時変数のために使うものではありません。
以上のことから、(5)のコードは以下の形にリファクタリングできます:
public String GetManagerName()
{
return (
this.AssignedStaffs
.Cast<Staff>()
.Where(x => x.Id == this.ManagerStaffId)
.First()
.Name
);
}
foreach
などの構文が用意されています。Where
などのライブラリ関数が用意されています。foreach
のような明示的なループは「どうやるか」が前面に出ている可能性が高いです。Where
のようなライブラリ関数で対応できます。(0)のようなコードは古式ゆかしいものです。
私も昔は(0)のようなコードを量産していたものです。
しかし量産していくうちに嫌気が差してきました。
注意深く書かなければ簡単に動かないコードができあがるし、
苦労して書いたところでちっとも読み易くないからです。
徐々にレベルアップしていき、今では最初から(6)のようなコードを書いています。
プログラミングそのものに慣れていないうちはいきなり(6)のようなコードを書くことは難しいかと思います。
何故なら、上述の通り、(0)から(6)に至るまでには様々な壁があるからです。
初めのうちは(0)でも構いませんが、徐々にレベルアップして(6)をすらすら書けるようになりましょう。
その方が色々と幸せになれます。
次回は「美しいプログラムを書く(リスト処理ライブラリ編)」です。