Perl Hackers Hub

第8回 Perlによる大規模システム開発・設計のツボ(1)

この記事を読むのに必要な時間:およそ 4 分

関数とオブジェクト

オブジェクト指向や関数の切り出し,そのほかさまざまなテクニックは「関心の分離」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;

このように条件に適応しない場合にすぐに戻り値を返したり,例外を飛ばしたりするテクニックを「ガード節」と言います。このようなテクニックを使って,関数の複雑さを分解するよう心がけましょう。

注2)
!(P || Q) == !P && !Qになるという論理学の法則です。
破壊的代入を避ける

次の関数のように,同一の変数に何度も値を代入しているコードは,ラインごとの処理内容が不明瞭になりがちです。

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つの責務を持つようにすべきだという「単一責務原則」があります。

著者プロフィール

広木大地(ひろきだいち)

筑波大学大学院卒業後,2008年度に新卒として株式会社ミクシィに入社。

広告システムの開発に従事したのち,システム本部たんぽぽ開発グループに所属。現在は「刺身の上にたんぽぽを乗せる仕事」を撲滅するべく,サービスアーキテクチャの設計/開発や技術者教育などを担当している。

YAPC::Asia 2010にて,mixiのアーキテクチャについての発表を行った。