sironekotoroの日記

Perl で楽をしたい

Perl 5.36 が出ました &(anyenv経由の)plenvでのインストール&移行方法

祝!Perl 5.36 !

ということで、初夏のはじめ頃のイベントである Perl の新規安定版リリースです。

metacpan.org

うちは(anyenv経由の) plenv で複数の Perl のバージョンを扱っているので、 plenv での作業を書いておきます。

誰のためってもちろん、来年の自分用です。

インストールできるバージョンを確認

$ plenv install -l | grep 5\.36
 5.36.0
 5.36.0-RC3
 5.36.0-RC2

インストール

$ plenv install 5.36.0

現時点で plenv がメインで使っているバージョンを確認する

$ plenv versions
  system
  5.32.0
* 5.34.0 (set by /Users/sironekotoro/.anyenv/envs/plenv/version)
  5.36.0

plenv がメインで使っているバージョンを、新しくインストールしたものに変更する

$ plenv global 5.36.0

plenv がメインで使っているバージョンが変更されていることを確認する

$ plenv versions
  system
  5.32.0
  5.34.0
* 5.36.0 (set by /Users/sironekotoro/.anyenv/envs/plenv/version)

perl のバージョンを確認する

$ perl -v

This is perl 5, version 36, subversion 0 (v5.36.0) built for darwin-2level

Copyright 1987-2022, Larry Wall

Perl may be copied only under the terms of either the Artistic License or the
GNU General Public License, which may be found in the Perl 5 source kit.

Complete documentation for Perl, including FAQ lists, should be found on
this system using "man perl" or "perldoc perl".  If you have access to the
Internet, point your browser at https://www.perl.org/, the Perl Home Page.

VScode でも新規に入れた perl を利用しているか確認する

vscode で Code Runner とか入れている場合には、ちゃんと更新したバージョンで実行されているかを確認する。

marketplace.visualstudio.com

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

print $];   # 5.036000

この $] というのが特殊変数で、実行している perl のバージョンを返してくれる。

VScode + Code Runner でうまく動かない場合には、Code Runner の「拡張機能の設定」→「Code-runner: Executor Map」→setting.jsonperl が plenv(またはanyenv) でインストールされたものになっているか確認しておく。

ターミナルで perl -v で確認したバージョンでよければ、ターミナルで which perl として出てきたパスを設定する。

うちの場合はこんな感じ。

        "perl": "$HOME/.anyenv/envs/plenv/shims/perl",

これまで入れてきた perl のモジュールをマイグレーションする

マイグレーションと言っても以前入れていたバージョンの perl でインストールしたモジュールを移行(移動)してくるのではなく、新規バージョンの perl にモジュールを新規でインストールを行う作業。

GitHub とか 自前ソースで作ったものは移行されない模様。

都度自分でインストールする。

-n(モジュールインストール時にテストしない)つけているのは自己責任いうことで・・・

$ plenv migrate-modules -n 5.32.0 5.36.0

インストールしてきたモジュール数によっては時間がかかる。

なので寝て待つか、ゲームでもして待ちましょう。

perl のバージョンを上げてもアプリを今まで通り動かしたい

後方互換性が売りの Perl なので、大概は動きます。

ただ、モジュール関連は先の項で説明した通り入らないものもあります・・・

と、こうなって初めて Carton とか Docker のような、環境ごと切り出してくれるような技術が重要になってくるわけですね!(毎年痛感)

Perl 5.36 とまた一年

面白くやって行きましょう!

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

目的を見失いがち

そもそも、例示コードを自分の好きに発展させて満足するのが目的ではない・・・ので、そういうのはほどほどに本を読み進めることにする。

5.1 staticメソッドの誤用

Java にはインスタンスを生成することなく、メソッドを呼び出す方法として static メソッドというのがあるらしい・・・

これPerlで再現できるんかな?

できなくない?

できたわ。

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

package Hello {

    sub world {
        print "Hello, World";
    }

}

package main;

Hello->world(); # Hello, World

インスタンス生成せずにメソッドだけ使うとか考えたことなかったわ・・・固定観念

ただまぁ、ここではデータとメソッドを一緒にした凝集度の高い設計にしましょう、ということで基本使わないということで良さそう。

staticメソッドの正しい使い方としては、凝集度に無関係なログ出力やフォーマット変換用メソッドなどがある、と。

リスト5-4 ギフトポイントを表現するクラス

初期化ロジックがいろんなところで書かれてしまい、結果として低凝集になってしまうという状況。

なるほど、コンストラクタ作る都度、初期ポイント数を設定していると、いろんなところで様々な値で呼び出されちゃうと。

ある時は初期ポイント3000で作って、その次は1500で作ってみたり・・・

携帯各社や楽天とか、時期やサービスの加入状態に応じて初期ポイントの額を変えたりしてるよねー。

あんな感じ?

ここクリックして展開

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

use Function::Parameters;
use lib qw/./;
use MyType;
use Readonly;

package GiftPoint {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use constant { MIN_POINT => 0 };

    has value => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
        trigger  =>
          sub { croak 'ポイントが0以上ではありません', if ( $_[0]->value < MIN_POINT ) },
    );

    # ポイントを加算する
    # $other 加算ポイント
    # return 加算後の残余ポイント
    method add( MyType::GiftPoint $other) {
        return GiftPoint->new( value => $self->value + $other->value );
    }

    # return 残余ポイントが消費ポイント以上であればtrue
    # consumption: 消費
    method is_enough($point) {
        return 1 if $point->value < $self->value;
    }

    # ポイントを消費する
    method consume($point) {
        if ( !$self->is_enough($point) ) {
            croak("ポイントが不足しています");
        }
        return GiftPoint->new( value => $self->value - $point->value );
    }

}

package main;

# ポイント初期状態
my $initial_point = GiftPoint->new( value => 0 );

# ポイントを付与する
my $added_point = $initial_point->add( GiftPoint->new( value => 3000 ) );

# ポイントはいくら?
warn $added_point->value;    # 3000

# ポイントを消費
my $consume_point = $added_point->consume( GiftPoint->new( value => 1000 ) );

# ポイントはいくら?
warn $consume_point->value;    # 2000

# ポイントを派手に消費
my $too_consume_point =
  $added_point->consume( GiftPoint->new( value => 10000 ) );

#   => ポイントが不足しています

リスト5.7 ファクトリメソッドを備えたGiftPointクラス

インスタンスを直接作るのではなく、メソッド経由で作成させることで、インスタンス作成の幅を狭める作戦・・・としてのファクトリーメソッドパターン。

ここで、インスタンス化しなくとも、メソッドを使えるstaticメソッドが生きるわけですね。

なるほど。

デザインパターン・・・なんか、大昔にチャレンジしてみたけど、どの本読んでも何言ってるか全然わからなかったなぁという悲しい記憶。

もっとも、当時はPerlのオブジェクトもまだ満足に使いこなしていない頃だったのに無謀すぎた。

で、オブジェクト使って自分でプログラム作るようになってしばらくするとあ、あぁ、なるほど、ってなるという(ならないデザインパターンの方が全然多いけど)。

オブジェクト指向自体もそうだったよなぁ。なんなんだろうな、あれ。

それはさておき。

今回は直接インスタンス化はできなくて、メソッド経由でのみインスタンス化が可能というのがわからなかったのですが、Mouse と BUILD サブルーチンで解決しました。

Mouseはこれまでしれっと使ってきたのですが、sub new {} しなくともインスタンス化可能です。

metacpan.org

でも sub new {}に相当するところで何か処理を入れたい時にはBUILDサブルーチンを設けてそこに処理を入れます。

コードがどこから呼ばれているかを判定する caller を使って、BUILDサブルーチンが含まれる GiftPoint パッケージ以外からの呼び出し時にはエラーが出るようにしました。

ここクリックして展開

#!/usr/bin/env perl
use strict;
use warnings;
use v5.32.0;
use feature qw/say isa/;

use Function::Parameters;
use lib qw/./;
use MyType;
use Readonly;

package GiftPoint {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use constant {
        MIN_POINT                 => 0,
        STANDARD_MEMBERSHIP_POINT => 3000,
        PRMIUM_MEMBERSHIP_POINT   => 10000,
    };

    has value => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
        trigger  => sub {
            croak 'ポイントが0以上ではありません', if ( $_[0]->value < MIN_POINT );
        },
    );

    # インスタンス化するときに、外部のPackage名だったらエラー
    sub BUILD {
        my ( $package, undef, undef ) = caller;
        croak '直接GiftPointクラスをnewすることができません' if ( $package ne 'GiftPoint' );
    }

    # 標準会員向け入会ギフトポイント
    method forStandardMenbership() {
        return 'GiftPoint'->new( value => STANDARD_MEMBERSHIP_POINT )
    }

    # ポイントを加算する
    # $other 加算ポイント
    # return 加算後の残余ポイント
    method add( MyType::GiftPoint $other) {
        return GiftPoint->new( value => $self->value + $other->value );
    }

    # return 残余ポイントが消費ポイント以上であればtrue
    # consumption: 消費
    method is_enough($point) {
        return 1 if $point->value < $self->value;
    }

    # ポイントを消費する
    method consume($point) {
        if ( !$self->is_enough($point) ) {
            croak("ポイントが不足しています");
        }
        return GiftPoint->new( value => $self->value - $point->value );
    }
}

package main;

# my $standard_point = GiftPoint->forStandardMenbership();
# say $standard_point->value;

my $point = GiftPoint->new( value => 100 );

GiftPointクラスの中に、初期ポイント付与のパターンを入れておくことで凝集度を高めるわけですね。

なるほど。

5.3 共通処理クラス

さまざまなロジックが雑多に置かれがち

・・・はい。

うちは仕事効率化でよくGoogle Apps Script 使うんですが、いろんなシートからよく呼ばれる処理をまとめて Util ってクラスに集めてます。

まさに、本文の リスト5.11 のような雑多に処理を便利置き場的に使っています。

横断的関心ごとに関する処理でまとめ上げようと思います。

・・・まぁ、一個インポートしてくるだけで色々と使えるの便利なんですけどね!!!

リスト5.14 引数の変更をしている

出力引数?初めて聞いた。というか、引数は入力なのに出力?

掲載されているコードを参考に、足りないところを(足りない頭で)補ってみる。

ここクリックして展開

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

use Function::Parameters;
use lib qw/./;
use MyType;
use Readonly;

package Location {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use constant { MIN => 0 };

    has x => (
        is       => 'rw',
        isa      => 'Int',
        required => 1,
        trigger  => sub { croak '', unless ( $_[0] ) },
    );

    has y => (
        is       => 'rw',
        isa      => 'Int',
        required => 1,
        trigger  => sub { croak '', unless ( $_[0] ) },
    );

}

package ActorManager {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use constant { MIN => 0 };

    method shift(
        MyType::Location $location,
        MyType::Int $shiftX,
        MyType::Int $shiftY,
      )
    {
        $location->x($shiftX);
        $location->y($shiftY);

        return $location;
    }
}

package main;

my $location = Location->new( x => 0, y => 0 );

my $moved = ActorManager->shift( $location, 3, 3 );

say $moved->x;    # 3
say $moved->y;    # 3

これは座標を表すクラス Location と、その位置を変更する ActorManager(のshiftメソッド)だけど、なるほど、別れてる意味ないよなって感じ。

あと、Mouse でも has でプロパティ作らなくてもエラー出ないんだな。

まぁ、コンストラクタ&引数をちゃんと設定しないのはアンチパターンであることは前の章で出ていたから、これは良くない。

次のリスト5.16の例は短いけど、 set ってメソッドで引数とっているのに、メソッドの中でやっているのは引数の減算。

なんでやっていう。

set ってメソッド名だったら、その引数の値がそのまま設定されるようなイメージなのに。

引数が入力なのか出力なのかぱっと見でわからないのはストレスだなぁ。

でもまだ「出力引数」って単語がしっくりこないな・・・

5.18 引き数を変更しない構造へ改善

第5章結構長いので、ここまでで前半としておきます。

先に作成した Location クラスの改修です。

ここで、ふと、あれ?

Perlのこのメソッドって他のオブジェクトから呼べちゃったりするのでは?と思い至りました。そして呼べます。

まぁ、Perlは動的型付けの言語で、Javaとは異なる思想で作られた言語なので当然なのですが、まぁ、それでも「そういう要望」に応えられるんかな?と思いました。

ここクリックして展開

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

use Function::Parameters;
use lib qw/./;
use MyType;
use Readonly;

package Location {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use constant { MIN => 0 };

    has x => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
        trigger  => sub { croak '', unless ( $_[0] ) },
    );

    has y => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
        trigger  => sub { croak '', unless ( $_[0] ) },
    );

    fun move( MyType::Location $self, MyType::Int $x, MyType::Int $y) {
        Readonly my $next_X => $self->x + $x;
        Readonly my $next_Y => $self->y + $y;
        return Location->new( x => $next_X, y => $next_Y );
    }

}

# 他のクラスのインスタンスからオブジェクトを呼べちゃう?可動化を確認するためのテストクラス。
package Hoge {

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

};

package main;

my $location = Location->new( x => 0, y => 0 );
my $moved    = $location->move( 3, 3 );

say $moved->x;    # 3
say $moved->y;    # 3

my $hoge = Hoge->new;
$hoge->Location::move( 1, 1 );
# エラー: In fun move: parameter 1 ($self): Location クラスのみ受け付けます

先の他のオブジェクトから、Locationが呼べちゃう問題を解決するために、Function::Parameters で作る関数を method から fun にしています。

method だと、仮引数の $self を省略してかけるのですが、fun ではしっかり書かなくてはいけない・・・ので、Location 型の仮引数として書いてます。

    fun move( MyType::Location $self, MyType::Int $x, MyType::Int $y) {

なんでそこまで・・・というのは完全に趣味の世界です。

この本を読み終えた時自分のコードがどうなってるか楽しみです。

Google フォーム → Google スプレッドシート → PDF で出力君を作った

想定外に時間ができた2022年5月

今は経理のお仕事メインでやってるのですが、5月は月初の連休のため月次の処理が後ろ倒しになります。

しかし、親会社への報告とか後ろ倒しにできないものもあり、気合を入れてゴールデンウィーク前から準備し、ゴールデンウィーク明けから前のめりでやっておりました・・・が、気合い入りすぎて普通の月よりも早く手が空いてしまいました。

そんな時、人事の方から「Google フォーム → Google スプレッドシート → PDFで出力」する的なのがあると、定型の書類処理が楽なんだけどなぁ、と話があり、空いてる時間でやってみました。

使うのはもちろん GAS、Google Apps Script です。

戦略

  1. Google フォームを作成し、Google スプレッドシートと関連つける
  2. Google フォームに入力し送信
  3. 「フォームの回答1」シートに反映
  4. トリガーをきっかけとして、「フォームの回答1」シートの一番下に入力された行(最新の行)の内容を取得
  5. 前項で取得した内容を元に、シート雛形に値を埋める
  6. シート雛形をPDF化する
  7. 指定のフォルダに保存

下準備その1 Google フォームから Google スプレッドシート

まずは Google Form で普通にアンケートフォーム的なのを作ります。

Google Drive 開いて、左上の「新規」から

Google フォーム を選択。

初期状態だとなんなので、選択肢を1つ増やして一旦完成とします。

この Google フォームだけでも回答の集計などができるのですが、 Google スプレッドシートに一旦出力するよう設定します。

上にある「回答」から、右側にあるスプレッドシートのアイコンをクリックします。

今回は「新しいスプレッドシートを作成」を選択し、右下の「作成」ボタンを押します。

すると、スプレッドシートが出来上がります。

このスプレッドシートに、GASを書いていきます。

その前に一回くらいアンケートやってみますかね。

右上にある「送信」から

リンクのマークを選び、そこで表示されるURLにアクセスします。

アンケートフォームが出てくるので、回答して「送信」ボタンを押します。

無事、スプレッドシートに回答が反映されました。

下準備その2 定型フォーマット準備

と言っても大したことではなく、適当にフォーマットを用意します。

シート名は「シート雛形」としておきます。

今回は項目名を左に、回答が入るところを右にしておきました。

この回答が入るセルには「名前付き範囲」で名前をつけておきます。

回答が入るセルを選択して、上のメニューから、「データ→名前付き範囲」と進み、名前をつけます。

名前は、質問項目と同じにしておきます。

この「名前付き範囲」が今回の肝です。

コード

コードを貼り付けてから、main 関数を実行します。上にある 「実行」ボタンで実行してください。

すると権限の確認メッセージが出てくると思うんですが、これを承認しておいてください。

数秒で Google ドライブのルートにファイルが出来上がります。

function main() {
  // Google フォームの結果が集計されるシート
  const totalingSheetName = "フォームの回答 1";
  // 雛形シート
  const templateSheetName = "シート雛形";

  // 集計シートの1行目をkeyに、最終行をvalueにしたMapを作る
  const hash = getMapFromAnswerListTitlesAndLastValues(totalingSheetName);

  // Mapを元に、雛形シートに値を埋める
  fillTemplateSheetFromHash({
    sheetName: templateSheetName,
    hash: hash,
  });

  // 雛形シートをPDFにして、保存先フォルダの中に格納する
  transformSheetToPdf(templateSheetName);
}

/**
 * Google Formの集計スプレッドシートから、1行目の項目名をkeyに、最終行の値をvalueにセットしたMapを返す
 * @module getMapFromAnswerListTitlesAndLastValues
 * @param {string} sheetName - Google Form の集計スプレッドシート名
 * @return {Map.<string, string>} - 集計スプレッドシートの1行目をkeyに、最終行をvalueとするMap
 */
function getMapFromAnswerListTitlesAndLastValues(sheetName) {
  // sheetNameを元にシートを特定し、データの入っている最終行と最終列を取得する
  const sourceSheet =
    SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  const lastRow = sourceSheet.getLastRow();
  const lastColumn = sourceSheet.getLastColumn();

  // 最初の行の要素(タイムスタンプ, 無題の質問, ...)を配列で取得する
  const keys = sourceSheet.getRange(1, 1, 1, lastColumn).getValues()[0];

  // 最新の行(最終行)の要素(2022/5/26, 無題の質問, ...)を配列で取得する
  const values = sourceSheet
    .getRange(lastRow, 1, lastRow, lastColumn)
    .getValues()[0];

  // Map型の構造体に
  // タイムスタンプ => 2022/5/26 ,無題の質問 => オプション2, ...
  // と言った形でデータを入れていく
  const hash = new Map();
  keys.forEach((key, index) => {
    value = values[index];
    hash.set(key, value);
  });

  return hash;
}

/**
 * hashのkey名に対応した名前付きセルに、hashのvalueを入力する
 * @module fillTemplateSheetFromHash
 * @param {object} obj - sheetName, hash が入った構造体
 * @return {undefined} - なし
 */
function fillTemplateSheetFromHash(obj) {
  // 分割代入で引数を取得
  const { sheetName, hash } = obj;

  const templateSheet =
    SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);

  // 名前付き範囲を集める
  const namedCells = templateSheet.getNamedRanges();

  // 名前付き範囲をのセルに、その名前と同じフォームの回答を入力する
  namedCells.forEach((namedCell) => {
    const name = namedCell.getName().toString();
    namedCell.getRange().setValue(hash.get(name));
  });
}

/**
 * 指定したシートをPDFにして、シートとともにフォルダに格納する
 * @module transformSheetToPdf
 * @param {string} sheetName - 雛形シート名
 * @return {undefined} - なし
 */
function transformSheetToPdf(sheetName) {
  // 雛形シート
  const sourceSheet =
    SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);

  // ファイル名
  const fileName = "Googleフォームからファイル出力";

  //スプレッドシートの個別シートをPDF化するために新規のスプレッドシートを作成
  const temporarySheet = SpreadsheetApp.create(fileName);

  //PDF化したい個別シートを新規作成したスプレッドシートにコピー
  sourceSheet.copyTo(temporarySheet);

  //スプレッドシート新規作成でデフォルト作成されるシートを削除
  temporarySheet.deleteSheet(temporarySheet.getSheets()[0]);

  //PDFとしてgetAsメソッドでblob形式で取得
  const pdf = temporarySheet.getAs("application/pdf");

  //pdfファイルの名前を設定
  pdf.setName(fileName);

  //GoogleドライブにPDFに変換したデータを保存
  DriveApp.createFile(pdf);
}

実際のコードは、

  • 出力先のフォルダを指定できるようにしたり
    • const folder = DriveApp.getFolderById(folderId);
  • そのフォルダに出力したり
    • folder.createFile(pdf);
  • フォームの内容からファイル名を持ってきたり
    • const fileName = `${fileNamePrifix}_${fileNameBody}`;
  • ファイルのオーナー権限を任意の人に変えたり
    • file.setOwner(fileOwner);

ということをしています。

工夫したのは Google シートの項目を拾って、同名の名前付きセルに当てはめることで、Google フォーム側で項目が増えても、それに対応する名前付きセルを作るだけでよくしたこと。

つまり、コード側の改修なしで反映できるようにしたことです。

困ったのは

  • メソッドを () をつけずに呼んできて意図しない値が出てエラーになったり(ありがち)
  • 作成した Google スプレッドシートGoogle Driveでどう扱えばいいかわからなかったり
    • const file = DriveApp.getFileById(spreadSheet.getId());
  • main サブルーチンに引数を設定したら、そこにGoogle フォームから構造体が入ってきて上書きされてびっくりしたり
  • Google フォームや、Google スプレッドシートのオーナーを変えたらコードが吹っ飛んだり(謎)

でした。

Google スプレッドシートをPDFにする方法は、公式の関数を使って実現してた以下のブログを参考にしました。

ありがとうございます。

auto-worker.com

仕上げ

さて、最後にフォーム送信時に自動でPDFが出力されるようにします。

左側のメニューの時計のマーク、トリガーをクリックします。

画面の右下、青い「トリガーを追加」ボタンをクリックして設定します。左下のイベントの種類を「フォーム送信時」に変えます。

以下のようになっていれば、トリガーの設定は完了です。

フォームからテストデータを送信して、データができていたら完了です。

お疲れ様でした。

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

リスト4.1 変数tmpへの度重なる再代入

サンプルコードから書かれてないところを想像して書いていくの、楽しいですよね。

ってわけで、Perl で書いてみます。

これまでいろんな方法で Perl のオブジュエクト書いてきたけど、Function::Parameters でやります。

仮引数に型付けられるし。

型定義モジュールはこれ。前章の Money クラスの名前変えただけ。

ここクリックして展開

package MyType;
use strict;
use warnings;

use Type::Library -base;
use Type::Utils;
use Types::Standard -types;

# Character型
declare 'Character', as Object,
  where { ref $_ eq 'Character' },
  message { 'Characterクラスのみ受け付けます' };

1;
本文を参考に作ったCharacterクラスと、それを呼び出すスクリプト(`package main` 以降)。
#!/usr/bin/env perl
use strict;
use warnings;
use Function::Parameters;

package Character;
use Carp qw/croak/;
use Mouse;
use lib qw/./;
use List::Util qw/max/;
use MyType qw/Character/;

has power => (
    is       => 'ro',
    isa      => 'Int',
    required => 1,
    trigger  => sub { croak '0以上の値を設定してください', unless ( $_[0]->power >= 0 ) },
);

has weapon_attack => (
    is       => 'ro',
    isa      => 'Int',
    required => 1,
    trigger  =>
      sub { croak '0以上の値を設定してください', unless ( $_[0]->weapon_attack >= 0 ) },
);

has speed => (
    is       => 'ro',
    isa      => 'Num',
    required => 1,
    trigger  => sub { croak '0以上の値を設定してください', unless ( $_[0]->speed >= 0 ) },
);

has defence => (
    is       => 'ro',
    isa      => 'Int',
    required => 1,
    trigger  => sub { croak '0以上の値を設定してください', unless ( $_[0]->defence >= 0 ) },
);

method damege( Character $enemy) {

    # メンバーの腕力と武器性能が基本攻撃力
    my $tmp = $self->power() + $self->weapon_attack();

    # メンバーのスピードで攻撃力を補正
    $tmp = $tmp * ( 1 + $self->speed() / 100 );

    #   攻撃力から敵の防御力を差し引いたのがダメージ
    $tmp = $tmp - ( $enemy->defence() / 2 );

    # ダメージ値が負数にならないよう補正
    $tmp = max( $tmp, 0 );

    return $tmp;
};

package main;

my $member = Character->new(
    power         => 1,
    weapon_attack => 10,
    speed         => 100,
    defence       => 10
);

my $enemy = Character->new(
    power         => 1,
    weapon_attack => 10,
    speed         => 100,
    defence       => 10
);

print $member->damege($enemy);    # 17

一つの変数に何回も何回も代入する再代入がよくない、というのは知っているし、経験もしている。

for 文の一時変数とか以外では避けるべきよね。

元のJava のコードで???となったのは、1f とか 100f という表記。

これってJava浮動小数リテラルの表記法なんですね。

わからなかったなぁ。

あと、Math.MaxPerlList::Utilmax 関数をそのまま使いました。

第3章の表3.3では値オブジェクトとして設計可能な値、概念の例というのがあり、そこではヒットポイントや攻撃力なんかも挙げられていました。

が、この先やるんだろうな・・・ってことで、ここではやっておりません。

リスト4.2 ローカル変数にfinalを付与すると再代入不可

Java で final なら、Perl は Readonly で。

お手軽で、見てわかりやすいのでReadonly使います。

おそいけど、まぁ、学習用だしヨシ!とします。

ちなみにコアモジュールだとばかり思ってたんですが、違うんですね。びっくりした。

$ corelist Readonly

Data for 2021-05-20
Readonly was not in CORE (or so I think)
use Readonly;
#(中略)

method damege( Character $enemy) {

    # メンバーの腕力と武器性能が基本攻撃力
    Readonly my $tmp => $self->power() + $self->weapon_attack();

    # メンバーのスピードで攻撃力を補正
    $tmp = $tmp * ( 1 + $self->speed() / 100 );

    # ここでエラー
    # Modification of a read-only value attempted 

このあとのリスト4.4, 4.5 のサンプルコードは productPrice の例になって、4章冒頭のサンプルコードからちょっとズレたような?

関数の引数にもfinalをつけましょう、というお話。

ここは3章でやりましたねというか、ここまでは3章でやったことの復習編みたいな感じ。

リスト4.6 攻撃力を表現するクラス

インスタンスの使い回しにより、意図せぬ変更が出ちゃったというお話。

Perlでもしっかり再現できました。

(package {} でクラスを囲ったり囲っていないのは、まだ自分のスタイルが見つからないから・・・)

ここクリックして展開

#!/usr/bin/env perl
use strict;
use warnings;
use Function::Parameters;

package AttackPower {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use constant { MIN => 0 };

    has value => (
        is       => 'rw' # 今回はrw、つまり可変で
        isa      => 'Int',
        required => 1,
        trigger  => sub { croak '', if ( $_[0]->value < MIN ) },
    );

    1;
}

package Weapon {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use constant { MIN => 0 };
    use lib qw/./;
    use MyType qw/AttackPower/;

    has attack_power => (
        is       => 'ro',
        isa      => 'AttackPower',
        required => 1,
        trigger  => sub { croak '', unless ( $_[0] ) },
    );

    1;
}

package main;
use feature qw/say/;

# 使い回す攻撃力インスタンス
my $attack_power = AttackPower->new( value => 10 );

# 武器A、武器Bにそれぞれ設定
# 同じ攻撃力だからええやろ、的な?
my $weaponA = Weapon->new( attack_power => $attack_power );
my $weaponB = Weapon->new( attack_power => $attack_power );

# 武器Aの攻撃力を25に設定
$weaponA->attack_power->value(25);

# 武器Aも「武器Bも」攻撃力が25になってしまった
say "Weapon A attack power:", $weaponA->attack_power->value();    # 25
say "Weapon B attack power:", $weaponB->attack_power->value();    # 25

Perl の場合にはリファレンスなんかでも同種の問題が起きたりしますね。

前章ではプロパティの値を更新したら、更新した値で新たなオブジェクトを返す、ってなことをやってたけど、今回のケースはそれでは防げないかな・・・あー、攻撃力を変更するってメソッドが必要か。

今回は直接プロパティ値を変更する場合のパターンなのね。

ということで、武器Aの攻撃力の変更が、武器Bに反映しないバージョンです。

ここクリックして展開

#!/usr/bin/env perl
use strict;
use warnings;
use Function::Parameters;

package AttackPower {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use constant { MIN => 0 };

    has value => (
        is       => 'rw',    # 諸悪の根源
        isa      => 'Int',
        required => 1,
        trigger  => sub { croak '', if ( $_[0]->value < MIN ) },
    );

    1;
}

package Weapon {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use constant { MIN => 0 };
    use lib qw/./;
    use MyType qw/AttackPower/;

    has attack_power => (
        is       => 'ro',
        isa      => 'AttackPower',
        required => 1,
        trigger  => sub { croak '', unless ( $_[0] ) },
    );

    1;
}

package main;
use feature qw/say/;

# 使い回さない攻撃力インスタンス
my $attack_power_A = AttackPower->new( value => 10 );
my $attack_power_B = AttackPower->new( value => 10 );

# 武器A、武器Bにそれぞれ設定
my $weaponA = Weapon->new( attack_power => $attack_power_A );
my $weaponB = Weapon->new( attack_power => $attack_power_B );

# 武器Aの攻撃力を25に設定
$weaponA->attack_power->value(25);

# 武器Aの攻撃力の変更が、武器Bには及ばない
say "Weapon A attack power:", $weaponA->attack_power->value();    # 25
say "Weapon B attack power:", $weaponB->attack_power->value();    # 10

リスト4.21 武器を表現するクラス

先ほどは攻撃力の変更、今度は武器クラスWeaponも新しいWeaponオブジェクトを返すように変更。

これで、攻撃力も武器もインスタンスの使い回しは無くなりました。

いまのところ、Perlで仮引数を型付けして、かつ読み込み専用にするってのがわからないのと、今回の $increment はメソッドの中で手を加えることなく使われているので、Readonly処理はせずにやっていきます、

ここクリックして展開

#!/usr/bin/env perl
use strict;
use warnings;
use Function::Parameters;

use lib qw/./;
use MyType;
use Readonly;

package AttackPower {
    use Carp qw/croak/;
    use Mouse;
    use constant { MIN => 0 };

    has value => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
        trigger  => sub {
            croak '0以上の値を設定してください',
              if ( $_[0]->value < MIN );
        },
    );

    # 攻撃力を強化する
    method rein_force( MyType::Int $increment ) {
        return AttackPower->new( value => $self->value + $increment );
    }

    # 攻撃力を0にする
    method disable() {
        return AttackPower->new( value => MIN );
    }

    1;
}

package Weapon {
    use Carp qw/croak/;
    use Mouse;
    use constant { MIN => 0 };

    has attack_power => (
        is       => 'ro',
        isa      => 'AttackPower',
        required => 1,
        trigger  => sub { croak '', unless ( $_[0] ) },
    );

    method rein_force( MyType::AttackPower $increment) {

        my $hoge = AttackPower->new( value => 0 );

        my $rein_forced = AttackPower->new(
            value => $self->attack_power->value + $increment->value );

        return Weapon->new( attack_power => $rein_forced );

    }

    1;
}

package main;
use feature qw/say/;

my $attack_power_A = AttackPower->new( value => 20 );
my $attack_power_B = AttackPower->new( value => 20 );

my $weapon_A = Weapon->new( attack_power => $attack_power_A );
my $weapon_B = Weapon->new( attack_power => $attack_power_B );

my $increment = AttackPower->new( value => 5 );

my $rein_forced_weapon_A = $weapon_A->rein_force($increment);

say "Weapon A attack power:", $weapon_A->attack_power->value;    # 20
say "Reinforced Weapon A attack power:",
  $rein_forced_weapon_A->attack_power->value;                    #25
say "Weapon B attack power:", $weapon_B->attack_power->value;    #20

はやくも bless 使ったPerlの素朴なオブジェクト指向へ回帰したい欲が湧いてきましたね。

リスト4.24 正しく動作するのか怪しげなロジック

ここらで中断してもいいかな〜、と思ったら次で最後のコードだったのでやります(フラグ)。

インスタンス変数を可変にする場合の注意点の例示コード。

ゲームのヒットポイントの処理です。

  • ヒットポイントは0以上
  • ヒットポイントが0になった場合、死亡状態にする

ここクリックして展開

#!/usr/bin/env perl
use strict;
use warnings;
use Function::Parameters;

use lib qw/./;
use MyType;
use Readonly;

package HitPoint;
use Carp qw/croak/;
use Mouse;
use Readonly;
use constant { MIN => 0 };

has amount => (
    is       => 'rw',
    isa      => 'Int',
    required => 1,
    trigger  => sub { croak '', unless ( $_[0] ) },
);

package Member;
use Carp qw/croak/;
use Mouse;
use Readonly;
use constant { MIN => 0 };

has hit_point => (
    is       => 'rw',
    isa      => 'Int',
    required => 1,
    trigger  => sub { croak '', unless ( $_[0] ) },
);

has states => (
    is       => 'rw',
    isa      => 'Str',
    required => 1,
    trigger  => sub { croak '', unless ( $_[0] ) },
);

# ダメージを受ける
method damege( MyType::HitPoint $damege_amount) {

    $self->hit_point( $self->hit_point - $damege_amount->amount );

}

package main;
use feature qw/say/;

my $member = Member->new( hit_point => 100, states => 'Normal' );

# ダメージのインスタンス化
my $damege = HitPoint->new( amount => 20 );

# ダメージを受ける
$member->damege($damege);

# 現在のヒットポイント
say $member->hit_point;    # 80

# 致命的なダメージのインスタンス化
my $critical_hit = HitPoint->new( amount => 100 );

# ダメージを受ける
$member->damege($critical_hit);

# 現在のヒットポイント
say $member->hit_point;    # -20

まだ、死亡判定入れてないのでヒットポイントがマイナスになったりします。

こちらが死亡判定を入れたもの・・・なのですが、ちょっと変えています。

先のコードでは3章とか4章の前半に従って値オブジェクトを使ってヒットポイントとかを設定していたんですが、それだとこの節の趣旨(インスタンス変数を可変にする)に合わないかなぁ?

ということで掲載コードに近く、数値を数値のまま(オブジェクトに包まずに)引数として渡すコードに書き換えてます。

ダメージ計算メソッドをMemberクラスからHitPointクラスに移し、HitPointクラスにヒットポイントがゼロかどうかの判定メソッドを追加してます。

これで状態変化も実装できました。

ここクリックして展開

!/usr/bin/env perl

use strict; use warnings; use Function::Parameters;

use lib qw/./; use MyType; use Readonly; use feature qw/say/;

package HitPoint; use Carp qw/croak/; use Mouse; use Readonly; use constant { MIN => 0 }; use List::Util qw/max/;

has amount => ( is => 'rw', isa => 'Int', required => 1, trigger => sub { croak 'お前はもう死んでいる', if ( $_[0]->amount < MIN ) }, );

ダメージ計算

method damege( MyType::Int $damege_amount) {

Readonly my $next_amount => $self->amount - $damege_amount;
$self->amount( max( MIN, $next_amount ) );

}

ヒットポイントがゼロだったら1(true)

method is_zero() { return 1 if $self->amount == 0; }

package Member; use Carp qw/croak/; use Mouse; use Readonly; use constant { MIN => 0 };

has hit_point => ( is => 'rw', isa => 'Int', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, );

has states => ( is => 'rw', isa => 'Str', required => 1, trigger => sub { croak '', unless ( $_[0] ) }, );

ダメージを受ける

method damege( MyType::Int $damege_amount) {

# ダメージ判定をHitPointクラスのdamegeメソッドに投げる
my $hit_point = HitPoint->new( amount => $self->hit_point );
$hit_point->damege($damege_amount);

# ダメージ後のヒットポイントをセット
$self->hit_point( $hit_point->amount );

# 状態を変更
if ( $hit_point->is_zero ) {
    $self->states('dead');
}

}

package main; use feature qw/say/;

my $member = Member->new( hit_point => 100, states => 'normal' );

ダメージを受ける

my $dameged = $member->damege(20);

現在のヒットポイント

printf( "ヒットポイント:%s 状態:%s\n", $member->hit_point, $member->states );

ヒットポイント:80 状態:normal

致命的なダメージを受ける

my $critical_hit = $member->damege(200);

現在のヒットポイント

printf( "ヒットポイント:%s 状態:%s\n", $member->hit_point, $member->states );

ヒットポイント:0 状態:dead

ただまぁ、せっかく学習したことなので、値オブジェクトに包むやつもやってみます。

あと、掲載コードの以下のところはわからなかった。

状態をリストで管理?うーんわからん。

final States states;
//中略
states.add(StateType.dead)

・・・あ、states 複数形だ。

そうか、死亡状態はともかく、「毒と石化」とか複数状態を併発することってあるものな。

モルボルみたいな。

せっかくだからここも実装してみよ。

まず状態変化のクラス States を作って・・・そして謎のこだわりを発揮した結果、めちゃ時間かかった。日を跨いでしまった。

ここクリックして展開

#!/usr/bin/env perl
use strict;
use warnings;
use Function::Parameters;

use lib qw/./;
use MyType;
use Readonly;
use feature qw/say/;

package States {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use List::Util qw/first uniq/;

    has states => (
        is       => 'ro',
        isa      => 'ArrayRef[Str]',
        required => 1,
        trigger  => sub { croak '', unless ( $_[0] ) },
    );

    method update( MyType::States $before_states) {

        # 追加される前のステータス
        # 今回追加されたステータス
        # まぜる
        Readonly my @added_states =>
          ( @{ $before_states->states }, @{ $self->states } );

        # ユニーク化
        Readonly my @uniqued_states => uniq @added_states;

        # dead があれば dead だけにする
        if ( first { $_ eq 'dead' } @uniqued_states ) {
            return States->new( states => ['dead'] );
        }

        # normal 以外の状態があれば、normalを削除する
        if ( grep { $_ !~ /normal/ } @uniqued_states ) {

            return States->new(
                states => [ grep { $_ !~ /normal/ } @uniqued_states ] );
        }

    }

    method show() {
        return join " ", @{ $self->states }
    }

}

package HitPoint {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use constant { MIN => 0 };
    use List::Util qw/max/;

    has amount => (
        is       => 'rw',
        isa      => 'Int',
        required => 1,
        trigger  => sub { croak 'お前はもう死んでいる', if ( $_[0]->amount < MIN ) },
    );

    # ダメージ計算
    method damege( MyType::HitPoint $damege) {

        Readonly my $next_amount => $self->amount - $damege->amount;

        Readonly my $hit_point => $self->amount( max( MIN, $next_amount ) );

        return HitPoint->new( amount => $hit_point );
    }

    # ヒットポイントがゼロだったら1(true)
    method is_zero() {

        return 1 if $self->amount == 0;
    }
}

package Member {
    use Carp qw/croak/;
    use Mouse;
    use Readonly;
    use constant { MIN => 0 };

    has hit_point => (
        is       => 'rw',
        isa      => 'HitPoint',
        required => 1,
        trigger  => sub { croak '', unless ( $_[0] ) },
    );

    has states => (
        is       => 'rw',
        isa      => 'States',
        required => 1,
        trigger  => sub { croak '', unless ( $_[0] ) },
    );

    # ダメージを受ける
    method damege( MyType::HitPoint $damege, MyType::States $states ) {

        # ダメージ判定をHitPointクラスのdamegeメソッドに投げ、
        # HitPointオブジェクトで受ける
        my $dameged_hit_point = $self->hit_point->damege($damege);

        # ヒットポイントゼロならガード節で処理を終わらせる
        if ( $dameged_hit_point->is_zero ) {
            return Member->new(
                hit_point => $dameged_hit_point,
                states    => States->new( states => ['dead'] ),
            );
        }

        # ダメージ前のステータス
        my $before_states = $self->states;

        # ダメージ後のステータス判定
        my $dameged_status = $states->update($before_states);

        return Member->new(
            hit_point => $dameged_hit_point,
            states    => $dameged_status,
        );
    }

}

package main;
use feature qw/say/;

my $member = Member->new(
    hit_point => HitPoint->new( amount => 100 ),
    states    => States->new( states => ['normal'] ),
);

# 初期状態
# 現在のヒットポイント
# ヒットポイント:100 状態:normal
printf(
    "ヒットポイント:%s 状態:%s\n",
    $member->hit_point->amount,
    $member->states->show,
);

# ダメージで減るヒットポイント
my $poison_damege = HitPoint->new( amount => 20 );

# ダメージによるステータス変化
my $poison_states = States->new( states => ['poison'] );

# ダメージを受けた後に、Memberオブジェクトを受け取る
my $poisond_member = $member->damege( $poison_damege, $poison_states );

# 現在のヒットポイント
# ヒットポイント:80 状態:poison
printf(
    "ヒットポイント:%s 状態:%s\n",
    $poisond_member->hit_point->amount,
    $poisond_member->states->show,
);

# 毒を受けたメンバーが石化ダメージ
# ダメージで減るヒットポイント
my $stone_damege = HitPoint->new( amount => 30 );

# ダメージによるステータス変化
my $stone_states = States->new( states => ['stone'] );

# ダメージを受けた後に、Memberオブジェクトを受け取る
my $stoned_member = $poisond_member->damege( $stone_damege, $stone_states );

# 現在のヒットポイント
# ヒットポイント:50 状態:poison stone
printf(
    "ヒットポイント:%s 状態:%s\n",
    $stoned_member->hit_point->amount,
    $stoned_member->states->show,
);

# 致命的なダメージ
my $critical_damege = HitPoint->new( amount => 200 );

# 致命的なダメージを受ける
my $critical_dameged_member =
  $stoned_member->damege( $critical_damege, States->new( states => ['normal'] ),
  );

# 現在のヒットポイント
# ヒットポイント:0 状態:dead
printf(
    "ヒットポイント:%s 状態:%s\n",
    $critical_dameged_member->hit_point->amount,
    $critical_dameged_member->states->show,
);

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

この調子でやっていくの・・・?

やっていきます。

リスト3.14 金額ではない値を渡せてしまう

final int ticketCount = 1 // チケット枚数
money.add(ticketCount);

とまー、金額の足し算のメソッドなのに、予期しない引数がきちゃうかもしれないよね?という話。

自分一人で作ったものであれば、そういうことはないかもしれないけど、二週間後の自分は別人。

これくらいのことはやりかねないですよね。

リスト3.15 Money 型だけ渡せるようにする。

おー、なるほど!

いや、たまーにそういうことをしている人やコードがあったんだけど、その重要性とかを認識はしていなかったです。

Intってだけだと、整数だったらなんでもokになっちゃうもんね。

ところで、サンプルコードの引数はなんで other って変数名なんだろ。

あとで伏線回収あるのかな?まあいいや。

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

package Money {
    use Mouse;
    use Carp qw/croak/;
    use feature qw/signatures state/;
    no warnings "experimental::signatures";
    use Readonly;

    has amount => (
        is      => 'ro',
        isa     => 'Int',
        trigger => sub {
            croak "金額が0以上でありません。" if ( $_[0]->amount < 0 );
        },
    );
    has currency => (
        is      => 'ro',
        isa     => 'Str',
        trigger => sub {
            croak "通貨を指定してください" if ( $_[0]->currency eq "" );
        }
    );

    sub add ( $self, $other ) {

        # Money型以外のものが渡ってきたら拒否る
        croak "Moneyクラスのみ受け付けます" if ( ref $other ne 'Money' );

        Readonly my $readonly_other => $other;
        my $new_amount = $self->amount + $readonly_other->amount;
        return Money->new(
            amount   => $new_amount,
            currency => $self->currency,
        );
    }

    __PACKAGE__->meta->make_immutable();
}

package main;

my $money = Money->new( amount => 500, currency => 'yen' );

# 足されるもの
my $add_money = Money->new( amount => 100, currency => 'yen' );

# 足された結果
my $added_money = $money->add($add_money);

print $added_money->amount;    # 600

3.16 add メソッドにもバリデーションを追加

add メソッドの引数で渡す Moneyオブジェクトだけど、中の currency (通貨)が違ってたらダメだよね?ドルと円を単純に足し算できないよね?ということで、add メソッドにもバリデーションを入れます。

が、入れるだけだとあれなので、Perl で型をつかってみます。

gihyo.jp

metacpan.org

まずは型定義のモジュール

# MyType.pm
package MyType;
use strict;
use warnings;

use Type::Library -base;
use Type::Utils;
use Types::Standard -types;

# Money型
declare 'Money', as Object, where { ref $_ eq 'Money' };

1;

型定義ファイルとこれから書くスクリプトは同じところに置いときます。

$ tree
.
├── MyType.pm
└── list3_16_kai.pl

次に本体のスクリプト

更新ついでに、has で定義しているプロパティに必須の required つけときます。

Java を意識しつつ、後置 if とかでちょっと Perl らしいところを見せていきたい。いや Perlスクリプトなんだけど。

まずは異なる currency の Money オブジェクトを足そうとしたらエラーを出すように1行追加。

        croak "通貨単位が違います" if ( $self->currency ne $other->currency );

さっき作った型定義ファイルを use して、

    use lib qw/./;
    use MyType qw/Money/;

再度便利モジュール Function::Parameters つかってさらに Java っぽく書いていきます。

    method add( MyType::Money $other) {

ほら Java っぽい(個人の感想です)。

なお、Class と同じ名前の型名は作れませんでした。他の方法だったら作れるんかな?

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

package Money {
    use Mouse;
    use Carp qw/croak/;
    use Function::Parameters;
    use Readonly;

    use lib qw/./;
    use MyType qw/Money/;

    has amount => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
        trigger  => sub {
            croak "金額が0以上でありません。" if ( $_[0]->amount < 0 );
        },
    );
    has currency => (
        is       => 'ro',
        isa      => 'Str',
        required => 1,
        trigger  => sub {
            croak "通貨を指定してください" if ( $_[0]->currency eq "" );
        }
    );

    method add( MyType::Money $other) {

        croak "通貨単位が違います" if ( $self->currency ne $other->currency );

        Readonly my $readonly_other => $other;
        my $new_amount = $self->amount + $readonly_other->amount;
        return Money->new(
            amount   => $new_amount,
            currency => $self->currency,
        );
    }

    __PACKAGE__->meta->make_immutable();
}

package main;

my $money = Money->new( amount => 500, currency => 'yen' );

# 足されるもの(通貨単位をドルにしてみる)
my $add_money = Money->new( amount => 100, currency => 'doller' );

# 足された結果
my $added_money = $money->add($add_money);    # 通貨単位が違います

print $added_money->amount;                   # この行は実行されない

ちゃんと異なる通貨単位で加算しようとした、ってことでエラーになりました。

さらに、Money オブジェクトではないものを足そうとした場合、型の方でエラーが出るようにします。

package main;

my $money = Money->new( amount => 500, currency => 'yen' );

# Moneyオブジェクトではない、ただの数値を入れた変数
my $add_money = 10;

# 足された結果
my $added_money = $money->add($add_money);

# エラー
# In method add: parameter 1 ($other): Moneyクラスのみ受け付けます
# MyType.pm で設定したエラーメッセージ

print $added_money->amount;    # この行は実行されない

出ました。

というわけで、Perl でも型を使えて幸せ・・・という話でもあるのですが、本の内容でいくと

を作成できたのでした。

写経(翻訳?)してよかった

やっぱ、コードは書かないと身につかないというか、この本は読むだけではもったいないよなぁちゃんと書いてこそ!という思いですね。

ここで学んだことを思うと・・・今まで自分が描いてきた脆弱なクラスたちを思い出して顔を伏せてしまいますね・・・ごめん。

この本では章ごとのまとめもしっかりあって、振り返りできて良いですね。

  • 完全コンストラク
    • 引数なしのデフォルトコンストラクタを生成できると、未初期化のまま使っちゃう恐れが(生焼けオブジェクト)
    • インスタンス変数を全て初期化できるコンストラクタを用意しよう
    • コンストラクタ内では早めに不正条件を弾くガード節を活用しよう
  • 値オブジェクト
    • 例えば、金額をそのまま Int 型(まぁ Perl には数値の型とかはないので数字だけだけど)で扱うと、金額計算ロジックが複数の場所に拡散する
      • 足し算と引き算で別サブルーチン作るとか
      • 同じ Int 型というだけで、金額以外の数字(チケット枚数とか)が混在する可能性もある
        • そんな奴いねぇよって思うのは今の自分だけで、明日の自分は忘れてるし、2週間後の自分はやらかす
    • ということで、値の概念そのものをクラスとして定義し、制約条件(0円以上)を課す
    • 加算メソッドも、同じ型の引数だけを受け取るようにチェックすることでミスを防ぐ

・・・でここで、金額以外に値オブジェクトにしておくと良いもののを表(表3.2)を書いてくれてる。

すごいお得。

このあと、コラムで RubyJavaScript での実装例が掲載されてる。

ということで第3章終わり。第4章楽しみ〜

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

リスト3.1 金額を表すクラス

前回に続いてやっていきます。

sironekotoro.hateblo.jp

そして早速なやむ。

インスタンス変数しか持っていない、典型的なデータクラスです。

Java でいうところのインスタンス変数ってのは、Perl のオブジェクトでいうところのプロパティですかね?

そういう理解でいきます。

というか、このプロパティというのが、本や言語によって「アトリビュート(属性)」とか「フィールド」とか「フィールド変数」っだったりで、似た概念のように思えるのに言葉が違って本当に混乱しましたね。バベルの塔の寓話か。

ここでは、インスタンス化しただけでは利用できない「生焼けオブジェクト」を作ってみます。

こんな感じ?

まぁ、こういうオブジェクトは作らんよね・・・いや、過去に作ってたかも・・・

あ、先のエントリに id:xtetsuji さんがコメント寄せてくれたように、Perl 5.14 からは package foo { } みたいに囲むことできるので、より Java っぽい見た目に近づけますね!

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

package Money {

    sub new {
        my $class = shift;
        my $self  = bless {
            amount   => undef,
            currency => undef,
        }, $class;
        return $self;
    }
}

package main;

# インスタンス化だけで使おうとしてエラー(当然)
my $money = Money->new();
print $money->{amount};    # Use of uninitialized value in print at ...

$money->{amount} = 100;
print $money->{amount};    # 100

リスト3.2 必ずコンストラクタで初期化する

まずは素直に bless で。

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

package Money {

    sub new {
        my $class = shift;
        my ( $amount, $currency ) = @_;
        my $self = bless {
            amount   => $amount,
            currency => $currency,
        }, $class;
        return $self;
    }
}

package main;

my $money = Money->new( 100, 'yen' );

say $money->{amount};      # 100
say $money->{currency};    # yen

xtetsuji さんといえば、Perl の実験的機能を集めたこの記事(と正規表現)。

qiita.com

せっかくなので、Perlの実験的機能である signatures 使ってみます。

前回使った Function::Parameters と違って(実験的機能とはいえ)オフィシャルってのが良いですね。

Function::Parameters みたいにハッシュとかハッシュリファレンス的に渡せるとなお良いんですが・・・いや、そんなの絶対にあるに決まってる。

と思って探すと出てくるんですよね。

www.effectiveperlprogramming.com

仮引数のところでハッシュの %hash で受ければ良いだけでした。納得〜

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

use v5.034.0;
use feature qw/say signatures/;
no warnings "experimental::signatures";

package Money {

    sub new ( $class, %hash ) {
        my $self = bless \%hash, $class;
        return $self;
    }

}

package main;

my $money = Money->new( amount => 100, currency => 'yen' );

say $money->{amount};      # 100
say $money->{currency};    # yen

こうやって、横道に逸れながら学ぶのも良いですね。

本のページ的には 2p ほどしか進んでないですが・・・

リスト3.3

ここまでは単に値をセットしただけなので、-100 円や null などの値を渡せてしまう・・・というところから始まります。

そこで、

  • 金額 amount:0以上の整数
  • 通過 currency:null以外

というレギュレーションを守るオブジェクトを作ろう、というのが次。

リスト3.4 コンストラクタで正常値のみが確実に設定される仕組み

普通に書くとこんな感じかな

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

package Money {

    sub new {
        my $class    = shift;
        my $hash_ref = shift;

        if ( $hash_ref->{amount} < 0 ) {
            die "金額が0以上でありません。";
        }
        if ( $hash_ref->{currency} == "" ) {
            die "通貨を指定してください。";
        }

        my $self = bless $hash_ref, $class;
        return $self;
    }
}

package main;

my $money = Money->new( { amount => 100, currency => '' } );    # 通貨を指定してください。

で、次は Mouse で。

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

package Money {
    use Mouse;
    use Carp qw/croak/;

    has amount => (
        is      => 'rw',
        isa     => 'Int',
        trigger => sub {
            croak "金額が0以上でありません。" if ( $_[0]->amount < 0 );
        },
    );
    has currency => (
        is      => 'rw',
        isa     => 'Str',
        trigger => sub {
            croak "通貨を指定してください" if ( $_[0]->currency eq "" );
        }
    );

    __PACKAGE__->meta->make_immutable();
}

package main;

my $money = Money->new( amount => 100, currency => '' ); # 通貨を指定してください

コンストラクタが値を設定する前に、処理の対象外となる条件を弾くガード節を置く。

うむうむ。

リスト3.5 Moneyクラスに金額加算メソッドを用意する

add サブルーチンは signatures で書いてみました。

で、なるほど、金額を加算した結果を同じamountプロパティに入れるのがよくないと。

いろんなメソッドで値が変わりまくると、それを追跡するのが大変そうだよね。

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

package Money {
    use Mouse;
    use Carp qw/croak/;
    use feature qw/signatures/;
    no warnings "experimental::signatures";

    has amount => (
        is      => 'rw',
        isa     => 'Int',
        trigger => sub {
            croak "金額が0以上でありません。" if ( $_[0]->amount < 0 );
        },
    );
    has currency => (
        is      => 'rw',
        isa     => 'Str',
        trigger => sub {
            croak "通貨を指定してください" if ( $_[0]->currency eq "" );
        }
    );

    sub add ( $self, $other ) {
        my $new_amount = $self->amount + $other;
        $self->amount($new_amount);
    }

    __PACKAGE__->meta->make_immutable();
}

package main;

my $money = Money->new( amount => 500, currency => 'yen' );

$money->add(100);
print $money->amount;

リスト3.7 final で普遍にする

Java の final をインスタンス変数に使うと、不変(イミュータブル)にできると。

Perl だったら Mouse の属性のところを rw(read write) から ro (read only)にしておけば良いかな。

プロパティの値を不変にしておくことで、(これから実装する)メソッドが値を書き換えたときに、どんな状態になっているか分からない・・・ということを避けることができる、と。

書き換えできないからね。

なるほどねー。

当然、そうなると先に実装した add メソッドはエラーになる。

amount は read only なプロパティなのに値を変えようとするから。

リスト3.9 変更値を持ったMoneyクラスのインスタンスを生成する

サンプルコードのJavaではaddメソッドの中で直接currencyを使えてる。

class Money {
// 省略
  Money add (int other){
    int added = amount + other;
    return new Moner(added, currency);
  }
}

けど、Perlではプロパティ値をそのまま持ってく方法がわからんかったので愚直に。

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

package Money {
    use Mouse;
    use Carp qw/croak/;
    use feature qw/signatures/;
    no warnings "experimental::signatures";

    has amount => (
        is      => 'ro',
        isa     => 'Int',
        trigger => sub {
            croak "金額が0以上でありません。" if ( $_[0]->amount < 0 );
        },
    );
    has currency => (
        is      => 'rw',
        isa     => 'Str',
        trigger => sub {
            croak "通貨を指定してください" if ( $_[0]->currency eq "" );
        }
    );

    # amount値を更新したオブジェクトを生成して返す。
    sub add ( $self, $other ) {
        my $new_amount = $self->amount + $other; # 愚直に
        return Money->new(
            amount   => $new_amount,
            currency => $self->currency,
        );
    }

    __PACKAGE__->meta->make_immutable();
}

package main;

my $money = Money->new( amount => 500, currency => 'yen' );

my $added_money = $money->add(100);
print $money->amount . "\n";          # 500
print $added_money->amount . "\n";    # 600

リスト3.10 メソッド引数やローカル変数にもfinalを付け不変にする

え・・・そこまでは考えたことなかったわ。

void doSomething(final int value){
  value = 100; // コンパイルエラー
}

えっと、Perlではどうやればいい?

use constant は定数宣言だから最初だけだし。

型を提供するモジュールを使って、仮引数の時点で型チェックすることはできるけど(Function::Parameters + Type::Tiny)、書き換え不可にする、読み込みのみにするモジュール・・・あったわ。

まんま、Readonly モジュール。

metacpan.org

add メソッドを書き換えます

# 抜粋

    # amount値を更新したオブジェクトを生成して返す。
    sub add ( $self, $other ) {
        Readonly my $freeze_other => $other;    # 再代入不可の変数を作る
        my $new_amount = $self->amount + $freeze_other;
        return Money->new(
            amount   => $new_amount,
            currency => $self->currency,
        );
    }

いやー、関数の引数まで不変にするとか全然考えたことなかったわ。

でもまぁ、JavaScriptの関数とかは仮引数も含めてほぼ全部const(再代入できない変数宣言)使ってるから、むしろそれが自然か。

おもしろいなー!

ということで今日はここまで。

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

巷で話題の本(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