sironekotoroの日記

Perl で楽をしたい

「良いコード/悪いコードで学ぶ設計入門」第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(再代入できない変数宣言)使ってるから、むしろそれが自然か。

おもしろいなー!

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