本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはmixiの広木大地さんで、テーマは「大規模システム開発・設計のツボ」です。
仕事やOSS(Open Source Software )プロジェクトでPerlを用いた多人数開発をするにあたって気をつけるべきことや、品質を維持するためのノウハウを、国内最大級のPerlシステムであるmixiの事例をベースに紹介します。コーディング上の命名に関する考え方から、大規模アーキテクチャの設計や品質の数値化まで、ミクロからマクロに至るポリシーやテクニックを駆け足で解説します。
なお、今回の内容は( 株) ミクシィの2010年度の新卒エンジニア技術教育メニューからの抜粋になります。これからPerl をはじめとするLL(Lightweight Language 、軽量言語)を仕事で使うというフレッシュエンジニアのみなさんにも、ぜひご一読いただけたらと思います。
コードはコミュニケーションツール
「コミュニケーション能力」は社会人にとって重要な資質とされています。プログラマにとって重要なコミュニケーション能力の一つに、コードの意図を正確に同僚や未来の自分に伝えることがあります。意図のわかりづらいコードや入り組んだコードは、それだけで周囲のプログラマやプロジェクトに迷惑をかけてしまうかもしれません。
コードの命名
コードの意図を正しく伝えるためには、何よりも命名するという行為に注意を払う必要があります。Perlスクリプト中に存在する「あなたが命名するもの」には、次のようなものがあります。
パッケージ名/クラス名/メソッド名
DBカラム名/テーブル名
サービスのURI
グローバル/レキシカル変数名
このように見ると予約語、CPANモジュール、同僚のメンテしているライブラリなどを除いて、ほぼすべてがあなたが付けた名前です。プログラミングという行為は、そのほとんどが名前を付けることと言っても過言ではないかもしれません。
不明瞭な名前、マジックナンバーを避ける
適切に名付けられていないコードは意図が読み取りづらいものになりがちです。
sub calculate {
my ( $ctgry,$t ) = @_;
if( $ctgry == 1 ) {
return $t * hourly($ctgry) * ( 1 + 0.23);
} else {
return $t * hourly($ctgry);
}
}
上記のコードはロジックとしてはとてもシンプルでわかりやすいのですが、何をするためにどんな意図で作られたのかがわかりません。具体的にはたとえば次のような不明確な点があります。
1や0.23など裸の数字が何を意味しているかわからない
引数がどのようなものかよくわからない
関数名が一般的過ぎて何をしたいのかよくわからない
1点目のようなプログラム中の裸の数字は「マジックナンバー」と呼ばれています。文脈が不明瞭になるため基本的に避けたほうがよいです。
上記のコードを文脈が明らかになるように書き換えてみましょう。
use constant WHITE_WORKER => 1;
use constant BLUE_WORKER => 2;
use constant INCREASING_RATE => 1 + 0.23;
sub get_wages {
my ( $user_category,$working_hours ) = @_;
if( $user_category == WHITE_WORKER ) {
return $working_hours *
wages_an_hour($user_category) *
INCREASING_RATE;
} else {
return $working_hours *
wages_on_hour($user_category);
}
}
このようにすると、一見するだけで時給の計算をしている関数だと理解できます。プログラム中に含まれる数字、フラグなどには必ず意味があります。意味のあるものにはその意味を名前として与えましょう。
バグを誘発しやすい変数名
関数スコープ内のレキシカル変数も重要な名前です。この名前の付け方一つで、バグを引き起こしたり読解が困難になるなど、さまざまな悪影響が生じます。
my $item_1; my $item_2; my $item_3;
上記のように連番で定義された変数は、リストとして定義するか、名前で明瞭な意味を表しましょう。
my @items;
my ($head,$body,$tail);
ほかにも、次のような名前の変数は明快さに欠けています。
my $item;
my %item;
my @item;
それぞれ異なるシジル[1] で、同じ名前の変数が定義されています。Perlではこれらは別の変数と認識されますが、同一の関数の中で異なるデータ構造が同じ名前で同居している状態は、ミスタイプや勘違い、誤読を誘発します。
次のように、それぞれのデータ構造に合った名前にすることで、区別が付きやすくなります。
my $item;
my %name_to_item;
my @items;
my @item_list;
ぱっと良い名前が思いつかないときに先のような変数名を付けてしまいがちですが、多少長くなっても意図のはっきりした名前を付けることで、メンテナンス性が格段に向上します。
コメントとドキュメント
コメントやドキュメントも重要なコミュニケーション手段です。
悪いコメント
よく見かける悪いコメントは次のようなものです。
# ここはこんな感じにしておく
# 絶対に変更しないコト!!
上記のように事情や意味がよくわからない文章を書いてはいけません。
# parsed_queryをsetupする
$parsed_query->setup;
上記のように自明なコメントは、コードの一覧性が下がるだけなので必要ありません。
# 終了したのでコメントアウト
# call_some_func(@array_of_arguments);
# TODO : あとで実装する
上記のようにコメントアウトやTODOをメモするのではなく、バージョン管理システムやチケット管理システムを利用して、コード上には現在動いているもののみを残しましょう。
# deprecated : 廃止予定
上記のようにコメントで伝えるのではなく、デバッグ時のエラーメッセージや警告などを利用して伝えましょう。
表明による予防的コーディング
次のようなコメントを目にすることがあります。
# $minは0<= * <= 59の間
my $min = shift;
こういった入力や出力が満たすべき性質についてのコメントは、アサーションなどを利用して書き換えましょう。
my $min = shift;
assert 0 <= $min && $min <=60;
Carp::Assertなどを用いて上記のように記述しておけば、実行時にassert文が検証されバグを追跡しやすくなります。
コードをコメントで分割しない
機能追加や仕様変更が多くなると、次のような関数を見かけます。
sub add_items {
my ($self,$items) = @_;
# ここは○○をしている
:
# ここは○○をしている
:
if( ! *** && ! ***) {
# ○○の場合
:
} else {
# ○○が出ない場合
:
}
}
上記ではコメントの記述で機能を分割しています。このようなコメントを記述するのではなく、意味が明瞭でテストしやすい関数に機能を分割しましょう。
コメントとドキュメントの違い
Perlでは、#によるコメントとpodによるドキュメントをコード上に情報として記述できます。これらは次のように、前提とする読者が違う点を意識しましょう。
コメントは、内部実装の理解のために必要な情報
ドキュメントは、ライブラリ利用者のために必要な情報
コメントの役割は補助的なものです。できるだけ、コメントは書かなくとも意図が伝わるようにしましょう。コード中の名前付けや関数の粒度を小さくすることをまず考え、それでも意図が伝わりにくい部分についてのみコメントを記述します。
ドキュメントには、主にモジュールの役割や公開メソッドなどのインタフェースの情報を記述します。インタフェースが変更されたら修正する必要があります。
コメントやドキュメントを記述しても、プログラムの動作を保証することはできません。動作を保証するためではなく、コードの読者をサポートする正しい情報を伝えるために、最新の状態を保ちましょう。
関数とオブジェクト
オブジェクト指向や関数の切り出し、そのほかさまざまなテクニックは「関心の分離」( SoC : Separation of Concerns )を実現するためのものです。関心の分離とは、プログラムの目的や機能、意味をできる限り分解し、切り離してとらえることです。これは、書き手、読み手を問わず「今考えるべきこと」を最小化することを意味しています。
論理式を明快にする
コード上に分岐条件として現れる論理式は、コードの可読性に影響を与えます。
if( !A && !B ) {
:
}
上記のような論理条件があった場合、ド・モルガンの法則 [2] を利用して次のように書き換えることができます。
unless(A or B ) {
:
}
条件が複雑過ぎる場合は、次のように関数に切り出してもよいでしょう。
if( is_enable($a) ) {
:
}
ポイントは、できるだけ論理記号や条件を小さく見せることです。
また、関数を大きく覆うif文のネストも、論理条件を反転させることで無駄にネストを増やすことを避けられます。
if ( $self->has_extra_space ) {
:
}
上記は次のように書き換えることができます。
return unless $self->has_extra_space;
このように条件に適応しない場合にすぐに戻り値を返したり、例外を飛ばしたりするテクニックを「ガード節」と言います。このようなテクニックを使って、関数の複雑さを分解するよう心がけましょう。
破壊的代入を避ける
次の関数のように、同一の変数に何度も値を代入しているコードは、ラインごとの処理内容が不明瞭になりがちです。
sub hoge {
my $x = shift;
:
$x = _get_fuga();
:
for my $item ( @list ) {
$x.= _get_moga( $item );
}
my $y;
if( is_enable($x) ) {
$y = "hello";
} else {
$y = "world";
}
}
こういった処理は代入ごとに意味合いが変わっていることが多いので、別の名前を付与してあげるのがよいでしょう。また、if/else内でそれぞれ代入するよりは、値を返すための処理であれば三項演算子を使うほうが明快になります。
sub hoge {
my $query_string = shift;
my $query_object = _parse_query( $query );
my $api_request =
join '/',
map{$_->key.'='.$_->value}
@{$query_object->params};
return ( is_enable( $mogaed_array) ) ?
'hello':
'world';
}
このように変数への代入をできるだけ一度に制約することで、関数内部の処理一つ一つを、小さな関数に分割しやすい明快な処理にできます。ifやelse、forがたくさん登場する関数を書いてしまいやすい場合は、こういった点に注目するとよいでしょう。
副作用と状態
関数の一つ一つを丁寧に記述しても、現実的なアプリケーションではどうしても副作用を持ってしまいます。副作用とは、関数の引数に含まれない入力や、関数の戻り値に含まれない出力のことです。
# 副作用のない関数
sub add {
my ( $a , $b ) = @_;
return $a + $b;
}
# 引数に含まれない入力の例
sub div_with_env {
my ( $a , $b ) = @_;
return ( $a / $b ) unless ( $ENV{USE_RATIONAL} );
return Rational->new( $a , $b );
}
# 引数に含まれない入出力を持つ関数
my $register = 0;
sub add {
my ( $a , $b ) = @_;
my $b ||= $register;
return $register = $a+b;
}
こういった隠れた入出力を持つ関数は、テスト作成時に隠れた入出力を意識して設計する必要があります。
オブジェクトとカプセル化
副作用・状態を持つ処理は、そのままでは取り扱いが難しく、また、完全になくすことは難しいです。このように隠れた性質である副作用や状態を日の当たる場所に連れ出して、名前を与え、分割統治する手法がオブジェクト指向です。先ほどの例であれば、
package RegisterCalc;
sub new {
my ( $class,%option ) = @_;
return bless { register => 0} ,$class;
}
sub set_register {}
sub get_register {}
sub add {}
sub div {}
のように、レジスタを持つ計算機クラスを定義し、その上で計算を行うようにします。すると、副作用はRegisterCalcインスタンス内に閉じ込められるため、テストしやすく、どのような状態を持っているのかも明瞭になります。
求めよ、聞くな
オブジェクト指向では、あるデータを持つクラスに、そのデータを利用した処理を紐づけることが望ましいです。副作用と状態を分割統治する手法がオブジェクト指向ですから当たり前のことに感じますが、このことは時に忘れられてしまいます。
たとえば次のコードを見てみましょう。
if( $user->type == User::OFFICIAL ) {
return get_entries_for_official($user);
}
if( $user->type == User::FRIEND ) {
return get_entries_friend($user);
}
if( $user->type == User::OTHER ) {
return get_entries_other($user);
}
この場合、typeという属性はuserに紐づいているので、その判断もUserが持つべきです。
if( $user->is_official );
if( $user->is_friend );
if( $user->is_other );
さらに、get_entries_*などのメソッドは$userというデータに紐づいた処理なので、
$user->get_entries;
のように処理が隠ぺいされている状態がより良いと言えます。
このようなデータと処理の関係に関する経験的な教訓として、「 求めよ、聞くな」という言葉があります。オブジェクトに内部データを問い合わせるのではなく、やってもらいたい処理そのものをオブジェクトに担当してもらおうという意味です。
オブジェクトが持つ状態とそれに関連した処理を「責務」と言いますが、責務はすなわち「コードに機能追加するときの単位」を表しています。オブジェクト指向設計には、1つのモジュールが1つの責務を持つようにすべきだという「単一責務原則」があります。