sironekotoroの日記

Perl で楽をしたい

「良いコード/悪いコードで学ぶ設計入門」第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章楽しみ〜