sironekotoroの日記

Perl で楽をしたい

「良いコード/悪いコードで学ぶ設計入門」第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) {

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

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