プログラムが複雑になる一番の理由は条件分岐 (if 文など) です。
条件分岐がなければ、一本道で読み下していけばいいのでバグが入り込む余地は大変少なくなります。
ということで、
- 条件分岐を書かなくていいように書く
- 条件分岐を書くなら、わかりやすく局所化して書く
というのがなにより大切です。
条件分岐は、主に
- バリデーションチェック
- ルール
- フロー制御
の三種類の役割にわけられます。
それぞれ混ぜずに書くことで、ロジックをきれいに保ちやすくなります。
これは書籍「リファクタリング」でガード節として処理しろと載っています。
パラメータに対するバリデーションチェックは先にすませ、早いうちに結果を出すことで後続のロジックではイレギュラーケースの判定を考慮から外すことができます。通常、メソッドにとっての事前条件をチェックします。
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;
}
👍