sironekotoroの日記

Perl で楽をしたい

「良いコード/悪いコードで学ぶ設計入門」を読み始めた

巷で話題の本(2022年春現在)

gihyo.jp

DMM Books が 30% ポイント還元をやっていたので、他の本と一緒に購入しました。

なんで読もうと思ったか

自分のコードが良いコードではない、という漠然とした不安がありました。

また自分の書いたコードも保守性が低く、これなら書き直したほうが早い・・・となるならマシなほうで、読むのも書き直すの面倒だからやりたくない、とか。

・・・それって良いコード書けていないよね。

そういう状態を脱却する一助になるのではと思いました。

良い「型」を身につけたいです。

時間かけて読む

で、読み進めていくと・・・これはあかん、と。

サンプルコードはJavaで書かれているんですが、変数名やメソッド名はちゃんと意味が通じる名称なので読みやすいです。

読みやすくて、すいすいと読み進めてしまいます。

これはよくない。

よくない読み方をしている。

読みやすいが故に、読み飛ばして栄養素を十分に摂取できない気がしました。

ということで、ちゃんと写経します。

時間はかかるけど、まぁ読んだ冊数を競っているわけではないし。

Javaはわからんし、環境構築の手間も惜しいのでいつものPerlでやります。

リスト2.1 一体何のロジックだろう?

これがダメな理由はわかります。

  • 不明瞭な変数名
  • 変数への再代入

こういうコードは書いてない(はず)なので、まずは安心。

#!/usr/bin/env perl
use strict;
use warnings;

# RPGにおけるダメージ量計算のコード、らしい
# $d    ダメージ量
# $p1   プレイヤー本体の攻撃力
# $p2   プレイヤー武器の攻撃力
# $d1   敵本体の防御力
# $d2   敵の防具の防御力

sub damege_culc {
    my ( $p1, $p2, $d1, $d2 ) = @_;
    my $d = 0;
    $d = $p1 + $p2;
    $d = $d - ( ( $d1 + $d2 ) / 2 );
    if ( $d < 0 ) {
        $d = 0;
    }
    return $d;
}

print damege_culc( 10, 5, 3, 5 );    # 11

リスト2.5 メソッドを呼び出す形に整理

本文ではリスト2.2〜2.4と段階的に改善して行っているのですが、そこは割愛して最終形のコードです。

本来は、Perlのオブジェクトの作法に従ってDamegeCalc.pmとそれを呼び出すdamege.plとかに分けるべきなのでしょうけど、ここは無精して1つのファイルでpackageで分けて書いてみます。

本文のJavaのコードをPerlに翻訳しつつ、やっていることはあまり変わらないように書いてみました。

bless で作る素朴なPerlオブジェクト指向です。

うちは結構好きです。

ここでの疑問が、estimate_damegeメソッドにわざわざ引数を設定するのはなぜかなぁという・・・うちが想像している呼び出すコードとは違うのかな?ボス戦だけ与えるダメージを半分にする、とかの調整を入れやすくするためかなぁ?

#!/usr/bin/env perl
use strict;
use warnings;

package DamegeCalc;

sub new {
    my $class = shift;
    my $param = shift;
    my $self  = bless $param, $class;
    return $self;
}

sub sum_up_player_attack_power {
    my $self = shift;
    return $self->{player_arm_power} + $self->{player_weapon_power};
}

sub sum_up_enemy_defence {
    my $self = shift;
    return $self->{enemy_body_defence} + $self->{enemy_armor_defence};
}

sub estimate_damege {
    my $self = shift;
    my $argv = shift;

    my $total_player_attack_power = $argv->{total_player_attack_power};
    my $total_enemy_defence       = $argv->{total_enemy_defence};

    my $damege_amount =
      $total_player_attack_power - ( $total_enemy_defence / 2 );

    if ( $damege_amount < 0 ) {
        return 0;
    }
    return $damege_amount;
}

package main;

# RPGにおけるダメージ量計算のコード
# $damege    ダメージ量
# player_arm_power   プレイヤー本体の攻撃力
# player_weapon_power   プレイヤー武器の攻撃力
# enemy_body_defence   敵本体の防御力
# enemy_armor_defence   敵の防具の防御力

my $damege = DamegeCalc->new(
    {
        player_arm_power    => 10,
        player_weapon_power => 5,
        enemy_body_defence  => 3,
        enemy_armor_defence => 5,
    }
);

my $total_player_attack_power = $damege->sum_up_player_attack_power();
my $total_enemy_defence       = $damege->sum_up_enemy_defence();
my $damege_amount             = $damege->estimate_damege(
    {
        total_player_attack_power => $total_player_attack_power,
        total_enemy_defence       => $total_enemy_defence,
    }
);
print $damege_amount;

メソッドを呼び出す形に整理・改

しかし、Perlで書くと長いな・・・いやいや、便利モジュール使ってみたら?

思い立ったらやってみる。

上記のコードをPerlの便利モジュール使って書き直してみます。

metacpan.org

metacpan.org

これで、ちょっとJavaっぽいコードになりました(個人の感想です)。

#!/usr/bin/env perl
use strict;
use warnings;

package DamegeCalc;
use Mouse;
use Function::Parameters;

has player_arm_power    => ( is => 'ro', isa => 'Int' );
has player_weapon_power => ( is => 'ro', isa => 'Int' );
has enemy_body_defence  => ( is => 'ro', isa => 'Int' );
has enemy_armor_defence => ( is => 'ro', isa => 'Int' );

method sum_up_player_attack_power() {
    return $self->player_arm_power + $self->player_weapon_power;
}

method sum_up_enemy_defence() {
    return $self->enemy_body_defence + $self->enemy_armor_defence;
}

method estimate_damege( :$total_player_attack_power, :$total_enemy_defence ) {
    my $damege_amount =
      $total_player_attack_power - ( $total_enemy_defence / 2 );

    if ( $damege_amount < 0 ) {
        return 0;
    }
    return $damege_amount;
}

__PACKAGE__->meta->make_immutable();

package main;

my $damege = DamegeCalc->new(
    player_arm_power    => 10,
    player_weapon_power => 5,
    enemy_body_defence  => 3,
    enemy_armor_defence => 5,
);

my $total_player_attack_power = $damege->sum_up_player_attack_power;
my $total_enemy_defence       = $damege->sum_up_enemy_defence;

my $damege_amount = $damege->estimate_damege(
        total_player_attack_power => $total_player_attack_power,
        total_enemy_defence       => $total_enemy_defence,
);
print $damege_amount;

ゆっくり読む

まずはこの本で良いコードを身につけたいと思います。

おまけ

リスト2.9 クラスにすると強く関係するデータとロジックをまとめられる

さて寝るかー、と思ったら次のコードが2章の最後だったのでここまで書いて寝るとします。

ヒットポイントのダメージ処理と回復処理です。

コンストラクタのところで最小値、最大値を超えないように三項演算子でのチェックが入ってますね。

最終的な値をオブジェクトに入れて返してます。

最初は!?ってなったけど、確かに合理的かも。

#!/usr/bin/env perl
use strict;
use warnings;
use feature qw/say/;

package HitPoint;
use Carp qw/croak/;
use Mouse;
use Function::Parameters;
use constant {
    MIN => 0,
    MAX => 999,
};

has 'value' => (
    is      => 'rw',
    isa     => 'Int',
    trigger => sub {
        croak MIN . "以上を指定してください" if ( $_[0]->value < MIN );
        croak MAX . "以下を指定してください" if ( $_[0]->value > MAX );
    },
);

# ダメージを受ける
method damege(:$damege_amount){
    my $dameged = $self->value - $damege_amount;
    my $corrected = $dameged < MIN ? MIN : $dameged;
    return HitPoint->new(value => $corrected);
};

# 回復する
method recover(:$recover_amount){
    my $recovered = $self->value + $recover_amount;
    my $corrected = MAX < $recovered ? MAX : $recovered;
    return HitPoint->new(value => $corrected);
};

package main;

# 初期値10
my $hit_point = HitPoint->new( value => 10 );
say $hit_point->value;

# 2000ダメージ
$hit_point = $hit_point->damege(damege_amount => 2000);
say $hit_point->value;  # 結果は0

# 2000回復
$hit_point = $hit_point->recover(recover_amount => 2000);
say $hit_point->value;  # 結果は999