Skip to content

Instantly share code, notes, and snippets.

@sunaot
Last active October 2, 2019 15:15
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sunaot/5592103 to your computer and use it in GitHub Desktop.
Save sunaot/5592103 to your computer and use it in GitHub Desktop.
条件分岐とのつきあい方

条件分岐とのつきあい方

プログラムが複雑になる一番の理由は条件分岐 (if 文など) です。

条件分岐がなければ、一本道で読み下していけばいいのでバグが入り込む余地は大変少なくなります。

ということで、

  • 条件分岐を書かなくていいように書く
  • 条件分岐を書くなら、わかりやすく局所化して書く

というのがなにより大切です。

条件分岐の 3 つの役割

条件分岐は、主に

  • バリデーションチェック
  • ルール
  • フロー制御

の三種類の役割にわけられます。

それぞれ混ぜずに書くことで、ロジックをきれいに保ちやすくなります。

バリデーションチェック

これは書籍「リファクタリング」でガード節として処理しろと載っています。

パラメータに対するバリデーションチェックは先にすませ、早いうちに結果を出すことで後続のロジックではイレギュラーケースの判定を考慮から外すことができます。通常、メソッドにとっての事前条件をチェックします。

sub do_something {
    my ($self, $foo, $bar, @baz) = @_;
    return unless defined $foo;             # 必須チェック
    return unless ref $bar eq 'Bar::Class'; # 型チェック
    return unless @baz == 4;                # 要素数のチェック

    # main logic
    my $name = $baz[2]; # 要素不足への考慮が不要になっている
    $foo->foo();        # $foo が undef であるケースの考慮が不要になっている
}

ここで main logic の前にチェックをして return しているのがガード節です。

ルール

機能要件を条件分岐として表現したものです。一般に「この条件だったらこの結果になる」という式として表現できます。

use Scalar::Util qw(looks_like_number);
use Carp qw(croak);

my $price_table = {
    free => 0,
    child => 22,
    junior => 32,
    adult => 40,
};

sub ticket_price {
    my ($age) = @_;
    croak('$age must be number') unless looks_like_number($age);
    croak('$age cannot be negative value') if $age < 0;

    if ($age < 6) {
        return $price_table->{free};
    } elsif ($age >= 6 and $age < 12) {
        return $price_table->{child};
    } elsif ($age >= 12 and $age < 18) {
        return $price_table->{junior};
    } elsif ($age >= 18) {
        return $price_table->{adult};
    }
}

フロー制御

フラグによって処理の流れを制御します。ディスパッチルーチンとして切り出すと扱いやすくなります。たいてい状態の表現としてのフラグを受けて、それぞれの状態によってやるべきことを呼びだす形になります。

sub process {
    my ($self) = @_;

    if ($self->has_ordered() and !$self->has_arrived()) { # is_receivable() として切り出すのもあり
        $self->receive();
    } elsif ($self->has_arrived() and !$self->has_shipped()) { # is_shippable() として切り出すのもあり
        $self->ship();
    } elsif ($self->has_shipped()) {
        $self->complete();
    } else {
        Carp::croak('invalid status');
    }
}

大切なのは、バリデーションチェックとルールとフロー制御を混ぜないこと

package BookOrder;

use Carp qw(croak);

use constant {
    ORDERED => 1,
    ARRIVED => 2,
    SHIPPED => 3,
};

sub process {
    my ($self) = @_;

    if ($self->has_ordered() and !$self->has_arrived()) { # is_receivable() として切り出すのもあり
        $self->receive();
    } elsif ($self->has_arrived() and !$self->has_shipped()) { # is_shippable() として切り出すのもあり
        $self->ship();
    } elsif ($self->has_shipped()) {
        $self->complete();
    } else {
       croak('invalid status'); # メソッドにとって事前条件ではなく例外動作を明示しているだけなのでガード節での表現をしない
    }
}

sub receive {
    my ($self) = @_;

    my $warehouse;
    if ($self->is_comic_order()) {  # ルール。フロー制御と混ぜない
        $warehouse = ComicWarehouse->new();
    } else {
        $warehouse = MagazineWarehouse->new();
    }
    $warehouse->receive_new_items();
}

sub ship {
    my ($self) = @_;

    my $transportation = $self->_shipment_transportation($self->destination, $self->weight);
    $transportation->deliver($self);
}

# ルール。購入時の指定など実際は条件が複雑化しそうなのでクラスとして切り出して
# 輸送手段のルールだけでテストしやすくするだろう。
sub _shipment_transportation {
    my ($self, $destination, $weight) = @_;

    if ($destination->very_far()) {
        return Transportation::Airplane->new();
    } else {
        if ($weight->very_heavy()) {
            return Transportation::Train->new();
        } else {
            return Transportation::Truck->new();
        }
    }    
}

sub has_ordered {
    my ($self) = @_;

    return $self->{status} == ORDERED;
}

sub has_arrived {
    my ($self) = @_;

    return $self->{status} == ARRIVED;
}

sub has_shipped {
    my ($self) = @_;

    return $self->{status} == SHIPPED;
}
@kotaroito
Copy link

👍

@sunaot
Copy link
Author

sunaot commented May 20, 2013

元ネタは @makotan で、要件の IF 仕様の IF 実装の IF というのがあって...という話が 3 つの IF というところに書かれています。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment