sironekotoroの日記

Perl で楽をしたい

「良いコード/悪いコードで学ぶ設計入門」第5章 後半 #ミノ駆動本

リスト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