リスト5.19 C# の出力引数
ここでサンプルコードが C# になってますが、まぁなんとか読める気がします。
関数に参照渡しを行って宣言元の変数を変えるというコードです。
Perl だとこんな感じですかね。
#!/usr/bin/env perl use strict; use warnings; sub set { my $arg = shift; ${$arg} = 10; return $arg; } my $value; set( \$value ); print $value; # 画面に10が表示される
これはまぁ、例示コードを Perl に落とし込んだだけですが、こうは書かんやろ・・・と思うけど、まぁ置いて先に進む。
参照渡しは低凝集に繋がりやすいので、凝集性に問題のないケースに限定しよう、という趣旨でした。
ただ、この章は魔法力回復と定価ロジックの例が入り乱れていて、例示コードはこんな感じ。
- リスト5.23 引数の多いメソッド(魔法力)
- リスト5.24 プリミティブ執着(定価)
- リスト5.25 プリミティブ執着によるコード重複(定価)
- リスト5.26 定価という具体的な型として設計する(定価)
- リスト5.27 プリミティブではなくクラスの型を渡す(定価)
- リスト5.28 引数ではなくインスタンス変数として表現する(魔法力)
書き出してみると、定価のコードからやるのが良さそう。
リスト5.24 プリミティブ執着の例
といっても、まぁ Perl にゃ元々 スカラー、配列、ハッシュしか型がないので、執着も何も・・・なのですが、あえて Perl で書きます。
#!/usr/bin/env perl use strict; use warnings; use feature qw/say/; use Carp qw/croak/; use Function::Parameters; use Types::Standard -types; fun discounted_price( Int $regular_price, Num $discount_rate ) { if ( $regular_price < 0 ) { croak("定価が0未満です"); } if ( $discount_rate < 0 ) { croak("割引率が0未満です"); } return $regular_price - ( $regular_price * $discount_rate ); } say discounted_price( 100, 0.1 ); # 90
リスト5.25 プリミティブ型に執着するとコード重複が生じやすい
プリミティブ型ばかり使っていると、先のコードの
if ( $regular_price < 0 ) { croak("定価が0未満です"); }
みたいなのが、コードのあちらこちらに書かれちゃうよね、という。
まぁ、そうよね。
リスト5.26 「定価」という具体的な方として設計する
ここはまぁ、シンプルに。
#!/usr/bin/env perl use strict; use warnings; use feature qw/say/; package RegularPrice { use Mouse; use Carp qw/croak/; has amount => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '', if ( $_[0] < 0 ) }, ); } package main; my $regular_price = RegularPrice->new( amount => 100 ); say $regular_price->amount; # 100
リスト5.27 プリミティブではなくクラスの型を渡す
たかが数値を10%引くだけで・・・
my $discount_price *= 0.9;
とか書いてしまいますね。
でも、自分が書いている業務コードで確かに、同じようなコードで同じような処理が散乱しているよな・・・という思い当たりがあって苦しい。
苦しいからこそ、書いてみるしかない。
まずは雑に型定義したファイル。
ここクリックして展開
package MyType; use strict; use warnings; use Type::Library -base; use Type::Utils; use Types::Standard -types; # RegularPrice 型 declare 'RegularPrice', as Object, where { ref $_ eq 'RegularPrice' }, message { 'RegularPrice クラスのみ受け付けます' }; # DiscountRate 型 declare 'DiscountRate', as Object, where { ref $_ eq 'DiscountRate' }, message { 'DiscountRate クラスのみ受け付けます' }; # DiscountedPrice 型 declare 'DiscountedPrice', as Object, where { ref $_ eq 'DiscountedPrice' }, message { 'DiscountedPrice クラスのみ受け付けます' }; 1;続いて本体。
#!/usr/bin/env perl use strict; use warnings; use feature qw/say/; package RegularPrice { use Mouse; use Carp qw/croak/; has amount => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '定価が0未満です', if ( $_[0] < 0 ) }, ); } package DiscountRate { use Carp qw/croak/; use Mouse; use lib qw/./; use MyType; has amount => ( is => 'ro', isa => 'Num', required => 1, trigger => sub { croak '割引率が0未満です', if ( $_[0] < 0 ) }, ); } package DiscountedPrice { use Carp qw/croak/; use Mouse; use Function::Parameters; use lib qw/./; use MyType; # 雑に型定義をしたファイル fun discounted_price( MyType::DiscountedPrice $self, MyType::RegularPrice $regular_price, MyType::DiscountRate $discount_rate ) { return $regular_price->amount - ( $regular_price->amount * $discount_rate->amount ); } } package main; my $regular_price = RegularPrice->new( amount => 100 ); say $regular_price->amount; # 100 my $discount_rate = DiscountRate->new( amount => 0.1 ); my $discounted_price = DiscountedPrice->new(); say $discounted_price->discounted_price( $regular_price, $discount_rate ); # 90
リスト5.23 多すぎるメソッド
ここから魔法力のコード。
うん、うちの知ってるRPGは確かにこんな感じだ。
これを全部引数にして、一つの関数に突っ込むと訳がわからなくなる・・・
ここクリックして展開
#!/usr/bin/env perl use strict; use warnings; use feature qw/say/; package Recover { use Carp qw/croak/; use lib qw/./; use List::Util qw/min/; use Function::Parameters; use Mouse; use MyType; use Types::Standard -types; method recover_magic_point( MyType::Int $current_magic_point, MyType::Int $originam_max_magic_point, MyType::ArrayRef [Int] $max_magic_point_increments, MyType::Int $recover_amount, ) { # 現在の最大マジックポイント(可変) my $current_max_magic_point = $originam_max_magic_point; for my $each ( @{$max_magic_point_increments} ) { # 現在の最大マジックポイントをマジックアイテム分増やす $current_max_magic_point += $each; } # 現在の最大マジックポイントと、回復量の小さい方を返り値として返す return min( ( $current_magic_point + $recover_amount ), $current_max_magic_point ) } __PACKAGE__->meta->make_immutable(); } package main; # 現在のマジックポイント:10 # 最大マジックポイント:100 # 装備で嵩上げされているマジックポイント: 40 (10, 10, 20 の合計 40) # 薬かなんかで回復するマジックポイントの値:10 my $magic_point = Recover->recover_magic_point( 10, 100, [ 10, 10, 20 ], 10 ); say $magic_point; # 20
リスト5.29 魔法力に関するロジックをカプセル化
本では省略されているけど、せっかくなので動くように書いてみた。
ところで、マジックポイントを「魔法力」と書けないのは、やっぱ商標とか著作権とかそういう・・・?
ここクリックして展開
#!/usr/bin/env perl use strict; use warnings; use feature qw/say/; package MagicPoint { use Carp qw/croak/; use Mouse; use Function::Parameters; use lib qw/./; use MyType; use List::Util qw/min/; # use Readonly; # use constant { MIN => 0 }; has current_amount => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); has original_max_amount => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); has max_increments => ( is => 'ro', isa => 'ArrayRef[Int]', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); # 現在の魔法力残量 method current() { return $self->current_amount; } # 魔法力の最大量 method max() { my $amount = $self->original_max_amount; for my $each ( @{ $self->max_increments } ) { $amount += $each; } return $amount; } # 魔法力を回復する fun recover( MyType::MagicPoint $self, MyType::Int $recovery_amount) { my $current = min( $self->current_amount + $recovery_amount, $self->max() ); return MagicPoint->new( current_amount => $current, original_max_amount => $self->original_max_amount, max_increments => $self->max_increments, ); } # 魔法力を消費する fun consume( MyType::MagicPoint $self, MyType::Int $consume_amount) { return MagicPoint->new( current_amount => $self->current_amount - $consume_amount, original_max_amount => $self->original_max_amount, max_increments => $self->max_increments, ); } __PACKAGE__->meta->make_immutable(); } package main; my $magic_point = MagicPoint->new( current_amount => 10, original_max_amount => 100, max_increments => [ 10, 10, 20 ], ); # 現在の魔法力残量 say $magic_point->current(); # 10 # 魔法力の最大量 say $magic_point->max(); # 140 # 魔法力を回復する my $new_magic_point = $magic_point->recover(10); say $new_magic_point->current(); # 20 # 魔法力を過剰に回復するも、最大値までしか回復しない my $over_max_magic_point = $magic_point->recover(1000); say $over_max_magic_point->current(); # 140 # 魔法力を消費する my $consume_magic_point = $over_max_magic_point->consume(100); say $consume_magic_point->current(); # 40
リスト5.30 数珠繋ぎにコールする「メソッドチェイン」
掲載されているサンプルコードを、ちゃんとメソッドとして動くようにするのめんどくさすぎる!と直感が叫んだので、単なる構造体で済ませてます。
でも、それでも面倒くさそうなコードに仕上がりました。
あと、ローカルの環境が perl 5.36 になったので、use v5.36
としています。
これ一つで use strict;
と use warnings
を宣言したのと同じ意味になるのでお得ですね!
ここクリックして展開
#!/usr/bin/env perl use v5.36; use feature qw/say signatures/; my $party = { members => [ { equipments => { can_change => 1, } } ], }; sub equip_armor ( $member_id, $new_armor ) { if ( $party->{members}[$member_id]->{equipments}->{can_change} ) { $party->{members}[$member_id]->{equipments}->{armor} = $new_armor; } } equip_armor( 0, 'ChainMail' ); print $party->{members}[0]->{equipments}->{armor};
リスト5.31 詳細なロジックは呼ぶ側ではなく、呼ばれる側に実装しよう
例示のコードは装備を外すものだったけど、せっかくなので装備した後の防御力を返すメソッドを作ってみた。
あと、head, armor, arm ってプロパティの並びに違和感があったので、head, body, arm にしてみたり。
「源氏の籠手」は憧れです。
好きなRPGは「FF5」と「女神転生 ディープストレンジジャーニー」です。
ここクリックして展開
#!/usr/bin/env perl use v5.36; use feature qw/say/; use Function::Parameters; package Equipment { use Carp qw/croak/; use Mouse; use Readonly; use constant { MIN => 0 }; has name => ( is => 'ro', isa => 'Str', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); has defense_power => ( is => 'ro', isa => 'Int', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, ); fun empty($self) { return Equipment->new( name => '', defense_power => 0 ); } __PACKAGE__->meta->make_immutable(); } package Equipments { use Carp qw/croak/; use Mouse; use Readonly; use constant { MIN => 0 }; has can_change => ( is => 'ro', isa => 'Bool', required => 0, trigger => sub { croak '', unless ( $_[0] ) }, ); has head => ( is => 'ro', isa => 'Equipment', required => 0, trigger => sub { croak '', unless ( $_[0] ) }, ); has body => ( is => 'ro', isa => 'Equipment', required => 0, trigger => sub { croak '', unless ( $_[0] ) }, ); has arm => ( is => 'ro', isa => 'Equipment', required => 0, trigger => sub { croak '', unless ( $_[0] ) }, ); fun defense_power($self) { return $self->head->defense_power + $self->body->defense_power + $self->arm->defense_power; } fun deactivate_all($self) { return Equipments->new( head => Equipment->empty(), body => Equipment->empty(), arm => Equipment->empty(), ) } __PACKAGE__->meta->make_immutable(); } package main; # my $equipment = Equipments->new(); # 頭部防具 my $iron_helmet = Equipment->new( name => '鉄のヘルメット', defense_power => 10, ); # 鎧 my $chain_mail = Equipment->new( name => 'チェインメイル', defense_power => 15, ); # 籠手 my $genji_s_gauntlet = Equipment->new( name => '源氏の籠手', defense_power => 80, ); my $equipment = Equipments->new( head => $iron_helmet, body => $chain_mail, arm => $genji_s_gauntlet, ); # 防具を装備した防御力 say $equipment->defense_power(); # 105 # 全装備を解除する my $full_monty = $equipment->deactivate_all(); # 全装備を解除した後の防御力 say $full_monty->defense_power(); #0