早期 return でネスト解消
まぁ、これは書くまでもなく、わかる。
というか、例示コード書くのも辛い・・・
if ( $a == 1 ) { if ( $b == 2 ) { if ( $c == 3 ) { } } }
的な。
これに else や elsif が入ってくるとさらにカオスになるという。
ネストの深淵を見つめる時、深淵もまたお前を見つめているのだ・・・的な。
解決策としては、「早期 return」。
早期 return によって見通しが良くなり・・・
条件ロジックと実行ロジックを分離できること
なるほど。ロジックの追加も怖くない、と。
switch 文の重複
まぁ、Perl には switch 文がない(厳密にはあるけど実験的機能&推奨されていない)。
ということで軽く読み飛ばす。
ここでは、魔法の名前、消費魔法力、攻撃力ごとに switch 文を書いてる(リスト6.11,リスト 6.12, リスト6.13)・・・けど、普通そんなに小分けにして条件分岐というか switch 分書かないよね?
まぁ、当然そんな書き方をしていれば、追加漏れなんかも出てくるだろう。
例示を優先して執筆したんかな?
ただまぁ、伝えたいことはわかる。
のちに、魔法ごとに名前、消費魔法力、攻撃力をまとめて switch 文で切り替えるコード(リスト6.18)になっている。
こちらはわかる。
そしてここで、巨大な switch 文を解消する手段として interface が出てくる。
おおっと、さすがに Perl ではないかな・・・近そうなのは role ?
うーん、ここはわからないなぁ。
インターフェース自体、Go をちょっと勉強したときに「同じメソッドがあれば共通化できるのねー、ふーん、そういうものかー」程度の浅い理解しかない。
とりあえず、例示コードの四角形クラスと円クラスを書いてこう。
リスト6.19 四角形クラスと円クラス
ここクリックして展開
#!/usr/bin/env perl use strict; use warnings; use feature qw/say/; use Function::Parameters; package Rectangle { use Carp qw/croak/; use Mouse; use Readonly; use constant { MIN => 0 }; has width => ( is => 'ro', isa => 'Num', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); has height => ( is => 'ro', isa => 'Num', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); fun area($self) { return $self->width * $self->height; } __PACKAGE__->meta->make_immutable(); } package Circle { use Carp qw/croak/; use Mouse; use Readonly; use constant { MIN => 0, PI => 3.1415 }; has radius => ( is => 'ro', isa => 'Num', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); fun area($self) { return $self->radius * $self->radius * PI; } __PACKAGE__->meta->make_immutable(); } package main; my $rectangle = Rectangle->new( width => 4, height => 5 ); say $rectangle->area; # 20 my $circle = Circle->new( radius => 1 ); say $circle->area; # 3.1415
ここで作った rectangle と circle 、両方にある(面積を求める)area というメソッド。
この area メソッドを備えたオブジェクトなら同じインターフェースで扱うことができる・・・
いちいち、中に入っているのが rectangle か circle かという条件分岐をせずとも、area というメソッドを呼べる。
・・・いまの Perl のコードでもできてますがな。
まぁ、 Perl には(この本に書いてあるような用途での)型ないからな!!
リスト6.25 同じ Shape 型として利用可能
というわけで・・・ここは Perl でちゃんと実装できませんでした。
やりたかったのは、Shape というインターフェース型を用意し、Rectangle と Circle という型をそこに含める。
Shape->area とすることで、Shape の中身が Rectangle でも Circle でも面積を求めることができる・・・
ということを Perl でもやりたかったんだけど、いや別にインターフェース型用意しなくとも普通にメソッド使える(ダックタイピングできる)んだよなぁ。
ただ、「引数を特定のクラスのオブジェクトに限定することで、堅牢なクラスをつくる」というこの本のこれまでの趣旨とは合わない。
うーん。
で、Mouse::Role とか使ってみたのだけどなんか違う。
というわけで、インターフェース型じゃなくて、こういう型を作ってみた。
# Shape 型 declare 'Shape', as Object, where { ref $_ eq 'Rectangle' || ref $_ eq 'Circle' }, where { $_->can('area') }, message { 'Rectangle か Circle クラスかつ area メソッドのあるもののみ受け付けます' };
これで、
- Rectangle 型か Circle 型 しか受け付けず
- area メソッドを持っている
ものしか受け取らない型ができた。もうこれはインターフェースと言っていいんじゃないか!!
いややっぱ違う?
違うけど、こっから先に進めなくなってしまったので、とりあえずこれで済ませよう。
なお、せっかく書いた、そして初めて Mouse::Role 使ったコードも置いておこう。
ちなみに、Role は初めて使いました。
ここクリックして展開
#!/usr/bin/env perl use strict; use warnings; use feature qw/say/; use Function::Parameters; # role を インターフェース代わりに使う package Shape { # use Mouse; use Mouse::Role; # ロールを使うための便利モジュール # このロールを継承(?)するオブジェクトが持っているべきメソッド requires qw(area_of); # 継承したオブジェクトで area メソッドを呼ぶと、 # 各オブジェクトの area_of メソッドの結果を返す sub area { my $self = shift; return $self->area_of(); } } package Rectangle { use Carp qw/croak/; use Mouse; with qw/Shape/; has width => ( is => 'ro', isa => 'Num', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); has height => ( is => 'ro', isa => 'Num', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); fun area_of($self) { return $self->width * $self->height; } __PACKAGE__->meta->make_immutable(); } package Circle { use Carp qw/croak/; use Mouse; use constant { PI => 3.1415 }; with qw/Shape/; has radius => ( is => 'ro', isa => 'Num', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); fun area_of($self) { return $self->radius * $self->radius * PI; } __PACKAGE__->meta->make_immutable(); } package main; my $rectangle = Rectangle->new( width => 4, height => 5 ); say $rectangle->area; # 20 my $circle = Circle->new( radius => 1 ); say $circle->area; # 3.1415
リスト6.37 魔法 interface の値オブジェクト導入版
・・・というのが例示コードのタイトル。
しかし、Perl で実装をこころみましたが実力及ばず、インターフェースをそのまま再現することができませんでした。
残念。
ただまぁ、それでもできるところまでやってみるべ、ということで
- 型を使って、指定したメソッドが含まれていることを確認
- if 文を使わず、魔法の切り替えができるよう実装
- 一部のプロパティは型オブジェクトで返す
ところまでやってみました。
interface の代わりに、MagicAttack クラスの中に各魔法のプロパティを書くことで代用しています。
もう少し、良い手がありそうな気がするのですが、とりあえずここまでとします。
ここクリックして展開
# Magic 型 declare 'Magic', as Object, where { ref $_ eq 'Fire' || ref $_ eq 'Shiden' || ref $_ eq 'HellFire' }, where { $_->can('name') && $_->can('cost_magic_point') && $_->can('attack_power') && $_->can('cost_technical_point') }, message { 'Magic クラスのみ受け付けます' };本体
#!/usr/bin/env perl use strict; use warnings; use feature qw/say/; use Function::Parameters qw/method/; package Member { use Carp qw/croak/; use Mouse; use constant { MIN => 0 }; has level => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); has agility => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); has magic_attack => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); has vitality => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); __PACKAGE__->meta->make_immutable(); } package Fire { use Carp qw/croak/; use Mouse; has member => ( is => 'ro', isa => 'Member', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); method name() { return "ファイア"; } method cost_magic_point() { return MagicPoint->new( amount => 2 ); } method attack_power() { return AttackPower->new( amount => 20 + $self->member->level * 0.5 ); } method cost_technical_point() { return TechnicalPoint->new( amount => 0 ) } __PACKAGE__->meta->make_immutable(); } package Shiden { use Carp qw/croak/; use Mouse; has member => ( is => 'ro', isa => 'Member', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); method name() { return "紫電"; } method cost_magic_point() { return MagicPoint->new( amount => 5 * $self->member->level * 0.2 ); } method attack_power() { return AttackPower->new( amount => 50 + $self->member->agility * 0.5 ); } method cost_technical_point() { return TechnicalPoint->new( amount => 5 ); } __PACKAGE__->meta->make_immutable(); } package HellFire { use Carp qw/croak/; use Mouse; has member => ( is => 'ro', isa => 'Member', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); method name() { return "地獄の業火"; } method cost_magic_point() { return MagicPoint->new( amount => 16 ); } method attack_power() { return AttackPower->new( amount => 200 + $self->member->magic_attack * 0.5 + $self->member->agility * 2 ); } method cost_technical_point() { return TechnicalPoint->new( amount => 20 + $self->member->level * 0.4 ); } __PACKAGE__->meta->make_immutable(); } package MagicAttack { use Carp qw/croak/; use Mouse; use lib qw/./; use MyType; has member => ( is => 'ro', isa => 'Member', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); has Fire => ( is => 'ro', isa => MyType::Magic, required => 0, default => sub { Fire->new( member => $_[0]->member ) } ); has Shiden => ( is => 'ro', isa => MyType::Magic, required => 0, default => sub { Shiden->new( member => $_[0]->member ) } ); has HellFire => ( is => 'ro', isa => MyType::Magic, required => 0, default => sub { HellFire->new( member => $_[0]->member ) } ); __PACKAGE__->meta->make_immutable(); } package MagicPoint { use Carp qw/croak/; use Mouse; has amount => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); __PACKAGE__->meta->make_immutable(); } package AttackPower { use Carp qw/croak/; use Mouse; has amount => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); __PACKAGE__->meta->make_immutable(); } package TechnicalPoint { use Carp qw/croak/; use Mouse; has amount => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); __PACKAGE__->meta->make_immutable(); } package main; my $member = Member->new( level => 10, agility => 10, magic_attack => 10, vitality => 10, ); my $magic_attack = MagicAttack->new( member => $member ); my $magic = $magic_attack->Fire; say $magic->name; # ファイア say $magic->cost_magic_point->amount; # 2 say $magic->attack_power->amount; # 25 say $magic->cost_technical_point->amount; # 0 $magic = $magic_attack->Shiden; say $magic->name; # 紫電 say $magic->cost_magic_point->amount; # 10 say $magic->attack_power->amount; # 55 say $magic->cost_technical_point->amount; # 5 $magic = $magic_attack->HellFire; say $magic->name; # 地獄の業火 say $magic->cost_magic_point->amount; # 16 say $magic->attack_power->amount; # 225 say $magic->cost_technical_point->amount; # 24