sironekotoroの日記

Perl で楽をしたい

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

ここはコードがほとんどないというか、あっても見てすぐに動作が予測できるものばかりなので、さらっと。

9.1 デッドコード

または到達不能コード。

本文では絶対に通らない else 文の例で表されている。

コードの可読性を損なうので、削除。

9.2 YAGNI原則

「こんなこともあろうかと」

真田さんだ。

ja.wikipedia.org

「こんなこともあろうかと」というセリフが代名詞として各種媒体で多用されている[7][8]が、実際に本編中でこのセリフを発したことは『宇宙戦艦ヤマト 完結編』までのシリーズ作品では1回もなく、『ヤマト2』第10話で「たぶんこんなこともあろうと思って〜」という似たセリフを発したのみである。

え、そうなの!?

それはともかく、将来の仕様を予見して実装しておくのは無駄だし、デッドコードになるからやめておけ、と。

9.3 マジックナンバー

コード中に、意図が不明な数字や文字列を直接書かない、と。

これは意図して、constance とか、変数名を全部大文字で MINIMUM_VALUE みたいにするのが鉄板よね。

同一のマジックナンバーは複数の箇所で実装されがちで、重複コードを生みます

そして、第6章の同じような switch 文が複数箇所に書かれちゃうという問題と類似の事態になると。

9.4 文字列型執着

// ラベル文字列、表示色(RGB)、上限文字数
String title = "タイトル,255,255,240,64"

おお、これはひどい

結局、これを使う段になったら split で分割した上で取り出さなきゃならんし面倒よなぁ。

メリットは・・・変数ひとつで済むとか?

いや、だったらハッシュにしたほうが。

9.5 グローバル変数

使わない方が良い、というのは知ってる。

設計が不十分なシステムでは、巨大データクラスが非常に生み出されやすいです。グローバル変数を使っていなくとも、グローバル変数と同質のものを知らず知らずのうちに使っているのです。

これは陥ってる可能性がありそう。

9.6 null問題

リスト 9.6

Perl 学んでて、null 意識することはなかったなぁ・・・というのは、Perl の場合は undef なので。

ここクリックして展開

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

package Equipment {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has defence => ( is => 'ro', default => undef );
    __PACKAGE__->meta->make_immutable();
}

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

    has head    => ( is => 'ro', default => undef );
    has body    => ( is => 'ro', default => undef );
    has arm     => ( is => 'ro', default => undef );
    has defence => ( is => 'ro' );

    method total_defence() {
        my $total = $self->defence;
        $total += $self->head->defence;
        $total += $self->body->defence;
        $total += $self->arm->defence;
        return $total;

    }
    __PACKAGE__->meta->make_immutable();
}

package main;

my $iron_helm = Equipment->new( defence => 10 );
say $iron_helm->defence;    # 10;

my $member = Member->new();
say $member->total_defence;  # Can't call method "defence" on an undefined value

まぁ、エラーでるよね。

デフォルトが undef にしていると、そのまま表示して良いかとかにいちいちチェックが必要になっちゃうと。

Perl はコンテキストに沿って undef を 0 とか "" に読み替えてくれる。

use strict;
use warnings;
use feature qw/say/;

my $num = undef;
$num = +1;
say $num;    # 1

my $str = undef;
$str .= "hello";    # . は文字列連結演算子
say $str;           # hello

リスト 9.11 「装備なし」をnullでない方法で実現

装備するってメソッドを、インスタンス変数を不変にするというルールを設けて実装してみた。

ここクリックして展開

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

package Equipment {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has name          => ( is => 'ro', default => '装備なし' );
    has price         => ( is => 'ro', default => 0 );
    has defence       => ( is => 'ro', default => 0 );
    has magic_defence => ( is => 'ro', default => 0 );

    method empty() {
        return $self->new();
    }

    __PACKAGE__->meta->make_immutable();
}

package Member {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has head    => ( is => 'ro', default => sub { Equipment->empty() } );
    has body    => ( is => 'ro', default => sub { Equipment->empty() } );
    has arm     => ( is => 'ro', default => sub { Equipment->empty() } );
    has defence => ( is => 'ro', default => 0 );

    method total_defence() {
        my $total = $self->defence;
        $total += $self->head->defence;
        $total += $self->body->defence;
        $total += $self->arm->defence;
        return $total;
    }

    method take_off_all_equipments() {
        $self->head( Equipment->empty );
        $self->body( Equipment->empty );
        $self->arm( Equipment->empty );
    }

    method equip(%hash) {

        return Member->new(
            head => $hash{head},
            body => $hash{body},
            arm  => $hash{arm},
        );

    }

    __PACKAGE__->meta->make_immutable();
}

package main;

my $member = Member->new();
say $member->head->name;       # 装備なし
say $member->total_defence;    # 0

my $iron_helm  = Equipment->new( name => 'iron_helm',  defence => 10 );
my $iron_armor = Equipment->new( name => 'iron_armor', defence => 10 );
my $empty_arm  = Equipment->empty;

my $equiped_member =
  $member->equip( head => $iron_helm, body => $iron_armor, arm => $empty_arm );

say $equiped_member->total_defence();    # 20;

9.7 例外の握り潰し

Perl の場合、 use strict;, use warnings; つけておけば、意図しない限りは例外握り潰せない。

意図するなってことね・・・。

例外をキャッチしたときには、通知や記録、場合によってはリカバリ処理を実行します。

#!/usr/bin/env perl
use v5.36;
use feature qw/try/;
no warnings "experimental::try";

try {
    die("突然の死");  # 例外出して死ぬ
}
catch ($e) {

    # 何も表示されず終了する
}

9.8 設計秩序を破壊するメタプログラミング

まぁ、難しそうだし使うことないよなー・・・というのはあれで、リフレクションの仕方がわからなかっただけです。

例示コードだけ書いておきます。

ここクリックして展開

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

package Level {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { MIN => 1, MAX => 99 };

    has value => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
        trigger  =>
          sub { croak "out of range." if ( $_[1] < MIN || MAX < $_[1] ) },
    );

    sub initialize {
        return Level->new( value => MIN );
    }

    sub increase {
        my $self  = shift;
        my $value = $self->value;
        if ( $value < MAX ) {
            return Level->new( $value + 1 );
        }
    }
    __PACKAGE__->meta->make_immutable();
}

package main;

my $one = Level->new( value => 1 );
say $one->value;
my $ninty_nine = Level->new( value => 99 );
say $ninty_nine->value;

9.9 技術駆動パッケージング

Web アプリでお馴染みの MVC アーキテクチャによるフォルダ分け。

これを、「設計クラスが似ている」という理由でビジネスクラスに適用してフォルダ分けしちゃうと混乱するよ、というお話。

Perl で Web アプリといえば Mojolicious !ということで、やってみます。

まずはインストール。

cpanm Mojolicious

雛形作成。今回はいつもの lite_app ではなく、lite_app なし、つまり本格アプリつくるときの構成でいきます。

いつも lite_app ばかりだったので、実は初めて。

# mojo generate app

雛形作成後のディレクトリ構造はこんな感じ

$ tree
.
├── lib
│   ├── MyApp
│   │   └── Controller
│   │       └── Example.pm
│   └── MyApp.pm
├── my_app.yml
├── public
│   └── index.html
├── script
│   └── my_app
├── t
│   └── basic.t
└── templates
    ├── example
    │   └── welcome.html.ep
    └── layouts
        └── default.html.ep

この lib/MyApp/Controller 配下に、 Order(注文), Payment(支払い), Stock(在庫) とフォルダを作ります。

これが本文中では図 9.4 にあたる「ビジネス概念の種類ごとにフォルダ分け」です。

$ tree
.
├── lib
│   ├── MyApp
│   │   └── Controller
│   │       ├── Example.pm
│   │       ├── Order
│   │       │   └── Example.pm
│   │       ├── Payment
│   │       └── Stock
│   └── MyApp.pm
├── my_app.yml
├── public
│   └── index.html
├── script
│   └── my_app
├── t
│   └── basic.t
└── templates
    ├── example
    │   └── welcome.html.ep
    └── layouts
        └── default.html.ep

今回は Order だけにコントローラーを追加してみます。

  1. lib/MyApp/Controller/Example.pmlib/MyApp/Controller/Order 配下にコピー

  2. コピーした lib/MyApp/Controller/Order/Example.pm の1行目、package のところを package MyApp::Controller::Order::Example; と実際のフォルダ構造に合わせて編集

  3. lib/MyAppp.pm にルーティング追加

package MyApp;
use Mojo::Base 'Mojolicious', -signatures;

# This method will run once at server start
sub startup ($self) {

    # Load configuration from config file
    my $config = $self->plugin('NotYAMLConfig');

    # Configure the application
    $self->secrets( $config->{secrets} );

    # Router
    my $r = $self->routes;

    $r->get('/')->to( controller => 'Example', action => 'welcome' );

    # 以下5行を追加
    $r->get('/order')->to(
        controller => 'Controller::Order::Example',
        action     => 'welcome',
        template   => 'example/welcome',
    );
    # 追加ここまで
}

1;

Mojoliciou 付属のWebサーバアプリ morbo で起動します。

morbo script/my_app

これで、http://localhost:3000/order にアクセスしても、トップページと同じ画面が出るようになりました。

このように、ビジネス概念ごとにフォルダを分け、その中に関連するコントローラを集めていけば、修正や追加も容易となるのは明白ですね。

今回は結構勉強になりました。

自分の要求範囲だと、lite_app 程度のもので十分で、lite_app なしで雛形作ったのも、そこでルーティング追加したのも初めてでした。

勉強駆動というか、本駆動でやることが広がるのはいいですね。

9.10 サンプルコードのコピペ

サンプルコートはあくまで言語仕様やライブラリの機能性を説明するためにかかれたものです。

うちはよくサンプルコードを書くのですが、これは本当にそうで、使われる場所の保守性や変更容易性は考えたことがないです。

あくまで、その関数やライブラリがどのような働きをするか、のみに絞って書くことがほとんどです。

ということで、サンプルコードはあくまでサンプル。

あるべきクラス構造を設計しましょう。

ところで、脚注にでてきた「サンプルコードのコピペだけで製品を動かしてしまう「コピペ職人」、逆にすごいのでは。

何をコピペしたら動くのか知っているわけで・・・まぁ、設計品質に期待できないのはそのとおりなのですが。

9.11 銀の弾丸

そんなものはない。

というのはちょっと長く IT 業界にいるとわかるのではないかなぁ、と思います。

4, 5 年で移り変わる主流の言語、フレームワーク・・・

しかし、そこを乗り切っている考え方はあります。

設計に best はありません。常に better を目指しましょう。

はい。

「良いコード/悪いコードで学ぶ設計入門」第8章 密結合 各種事例 #ミノ駆動本

リスト8.7 ,リスト8.8 物理攻撃クラス&武闘家の物理攻撃クラス

Mouse での継承で override を使うの初めて。

というか、それ以前でもなかったかも。

親クラスの同名のメソッドを上書きして使いたい時に使うもの、程度の認識です。

今回は差をわかりやすくするために、親クラスである PhysicalAttack の攻撃力と、それを継承する FighterPhysicalAttack の攻撃力に極端に差をつけてます。

おかげで、武闘家のダメージが通常 : 21に対してダブル : 12 と逆転してますが、まぁ。

ここクリックして展開

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

package PhysicalAttack {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { MIN => 0 };

    method single_attack_damege() {
        return 1;
    }

    method double_attack_damege() {
        return 2;
    }

    __PACKAGE__->meta->make_immutable();
}

package FighterPhysicalAttack {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { MIN => 0 };

    extends 'PhysicalAttack';

    override 'single_attack_damege' => sub {
        return super() + 20;
    };

    override 'double_attack_damege' => sub {
        return super() + 10;
    };

    __PACKAGE__->meta->make_immutable();
}

package main;

my $physical_attack = PhysicalAttack->new();
say $physical_attack->single_attack_damege();    # 1
say $physical_attack->double_attack_damege();    # 2

my $fighter_physical_attack = FighterPhysicalAttack->new();
say $fighter_physical_attack->single_attack_damege();    # 21
say $fighter_physical_attack->double_attack_damege();    # 12

で、こちらが親クラスをいじった結果、子クラスの数字がおかしくなっちゃう版。

言語は違えど、同じオブジェクト指向。見事に同様のエラーになりますなぁ。

ここクリックして展開

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

package PhysicalAttack {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { MIN => 0 };

    method single_attack_damege() {
        return 1;
    }

    method double_attack_damege() {
        return $self->single_attack_damege * 2
    }

    __PACKAGE__->meta->make_immutable();
}

package FighterPhysicalAttack {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { MIN => 0 };

    extends 'PhysicalAttack';

    override 'single_attack_damege' => sub {
        return super() + 20;
    };

    override 'double_attack_damege' => sub {
        return super() + 10;
    };

    __PACKAGE__->meta->make_immutable();
}

package main;

my $physical_attack = PhysicalAttack->new();
say $physical_attack->single_attack_damege();    # 1
say $physical_attack->double_attack_damege();    # 2

my $fighter_physical_attack = FighterPhysicalAttack->new();
say $fighter_physical_attack->single_attack_damege();    # 21
say $fighter_physical_attack->double_attack_damege();    # 52 !?

8.9 武闘家の物理攻撃クラス(コンポジション版)

というわけで、継承を使わずにやってみます。

子クラスの中で親クラスのインスタンス変数を用意し

my $physical_attack = PhysicalAttack->new();

おぉ、ちゃんと正しい、意図した数値になった。

ここクリックして展開

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

package PhysicalAttack {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { MIN => 0 };

    method single_attack_damege() {
        return 1;
    }

    method double_attack_damege() {
        return $self->single_attack_damege * 2
    }

    __PACKAGE__->meta->make_immutable();
}

package FighterPhysicalAttack {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { MIN => 0 };

    my $physical_attack = PhysicalAttack->new();

    method single_attack_damege() {
        return $physical_attack->single_attack_damege() + 20;
    }

    method double_attack_damege() {
        return $physical_attack->double_attack_damege() + 10
    }

    __PACKAGE__->meta->make_immutable();
}

package main;

my $physical_attack = PhysicalAttack->new();
say $physical_attack->single_attack_damege();    # 1
say $physical_attack->double_attack_damege();    # 2

my $fighter_physical_attack = FighterPhysicalAttack->new();
say $fighter_physical_attack->single_attack_damege();    # 21
say $fighter_physical_attack->double_attack_damege();    # 12

リスト8.10 基底クラスでの悪しき共通化 〜 リスト8.17

もう、ここはこれに尽きるのではないでしょうか。

ある継承クラスにとっては関係があっても、別の継承クラスにとっては無関係なメソッドが登場し始めると問題です。

あぁ、これは第5章の「共通処理クラス」のところの横断的関心から外れているから、と言うことで良いのだろうか。

サブクラスの都合でスーパークラスを変更してはいけない!

本文読んだ後でこの動画を見ると沁みるなぁ。

第8章の残りの部分

以降は文章による説明が続くので、簡潔に。

なんでもpublicで密結合

・・・たぶん、Perl でクラスをパブリックにするとかプライベートにするとかないんじゃないかなぁ。

ちょっとググったりした程度では不明でした。

ということで、次。

privateメソッドだらけ

多くの責務を持ってしまっている可能性あり。

別々のクラスに分離しよう。

高凝集の誤解から来る密結合

たとえば、販売価格クラスに対して、販売手数料、配送料、ショッピングポイントなどが含まれているクラス。

支払いについては凝集していると言えるが、別々の責務が入り込んでいる。

「ある概念の値を使って別の概念の値を算出したい場合」は、計算に使う値をコンストラクタの引数として渡す。

販売手数料クラス、配送料クラス、ショッピングポイントクラス、など。

スマートUI

表示と、表示以外の責務は分ける。

巨大データクラス

様々なデータを持つが故に、色々なところで使われ、値の変更がどこかで行われてしまう可能性が高まる。

グローバル変数と同様の弊害を招きます。

トランザクションスクリプトパターン

メソッド内に一連の処理手順がダラダラと長く書き連ねられている構造

(中略)

データを所持するクラス(データクラス)と、データを処理するクラスとで分けている場合に頻繁に実装されます。

・・・あれ、うちの書くコードってほとんどこれなのでは(思い当たる節がある)

手続き型プログラミングとも呼びます。

お?

ja.wikipedia.org

手続き型プログラムは、開始点のメインルーチンから階層的に分割された数々のサブルーチン及びそのローカル変数と、全てのサブルーチンからアクセス可能な数々のグローバル変数で構成される。複数のサブルーチンからアクセスされるあらゆるデータを、グローバル変数としてまとめてしまう簡素な設計は、小中規模のソフトウェア開発には適したものとされている。

ふむ。

代表的な手続き型言語 Perl - 1987年、WEBアプリケーション向け。

まぁ、単機能スクリプトというか、ソースが1画面で表示終わっちゃうようなものであれば手続き型で、それ以上ならちゃんと構造化したオブジェクト指向でって感じかな。

神クラス

トランザクションスクリプトが巨大になりすぎたもの。

密結合クラスの対処法

  • オブジェクト指向設計
  • 単一責任の原則
  • 責務ごとのクラス分割
  • 100 〜 200 行が一つのクラスの目安
    • (思い当たる節がある)
  • 使えるテクニック
    • 早期return
    • ストラテジパターン
    • ファーストクラスコレクション
      • クラスの集合をクラスで表す

「良いコード/悪いコードで学ぶ設計入門」第8章 密結合 単一責任の原則 #ミノ駆動本

ちょっと時間あいてしまいましたあが、のんびりやっていきます。

来月7月はお仕事が忙しくなる予定なので、やっぱり時間が空いてしまうのではないかなぁ、という感じです。

8章は6章なみに長くて盛り沢山だし・・・

しかし、現実逃避的に学習が進む可能性も(稀によくある)。

リスト8.1 商品割引に関連するクラス

コード書きながら思ったことを、そのままコードのコメントとして挿入してみました。

ここクリックして展開

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

package Product {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has id => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
    );
    has name => (
        is       => 'ro',
        isa      => 'Str',
        required => 1,
    );
    has price => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
    );
    __PACKAGE__->meta->make_immutable();
}

package ProductDiscount {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has id => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
    );
    has can_discount => (
        is       => 'ro',
        isa      => 'Bool',
        required => 1,
    );
    __PACKAGE__->meta->make_immutable();
}

package DiscountManager {
    use Carp qw/croak/;
    use Mouse;
    use MouseX::AttributeHelpers;
    use namespace::autoclean;
    use constant { TRUE => !!1, FALSE => !!0 };

    has discount_products => (
        metaclass => 'Collection::Array',
        is        => 'ro',
        isa       => 'ArrayRef[Object]',
        default   => sub { [] },
        required  => 0,
        provides  => {
            push => 'add_product',
        }
    );

    has total_price => (
        is       => 'rw',
        isa      => 'Int',
        required => 0,
        default  => 0,
    );

    method add( $product, $product_discount ) {

        # この4つはこのクラスの責務ではないよなぁ
        croak "そんな商品idないよ"   if ( $product->id < 0 );
        croak "商品の名前が空欄だよ"   if ( $product->name eq "" );
        croak "商品の価格がマイナスだよ" if ( $product->price < 0 );
        croak "商品の割引情報がないよ"  if ( $product->id != $product_discount->id );

        # え、まだこの add メソッドの中に何か書くの?
        my $discount_price = $self->get_discount_price( $product->price() );

        my $tmp = 0;
        if ( $product_discount->can_discount ) {
            $tmp = $self->total_price + $discount_price;
        }
        else {
            $tmp = $self->total_price + $product->price;
        }

        if ( $tmp <= 20000 ) {
            $self->total_price($tmp);
            $self->add_product($product);
            return TRUE;
        }
        else {
            return FALSE;
        }

    }

    # 割引価格を取得する
    method get_discount_price($price) {
        my $discount_price = $price - 300;
        $discount_price = 0 if $discount_price < 0;
        return $discount_price;
    }

    __PACKAGE__->meta->make_immutable();
}

package main;

# 商品aと値引き情報
my $product_a  = Product->new( id => 1, name => 'a', price => 1000 );
my $discount_a = ProductDiscount->new( id => 1, can_discount => 1 );

# 商品bと値引き情報
my $product_b  = Product->new( id => 2, name => 'b', price => 2000 );
my $discount_b = ProductDiscount->new( id => 2, can_discount => 1 );

# 商品cと値引き情報
my $product_c  = Product->new( id => 3, name => 'c', price => 30000 );
my $discount_c = ProductDiscount->new( id => 3, can_discount => 1 );

my $discount_manager = DiscountManager->new();        # インスタンス化
$discount_manager->add( $product_a, $discount_a );    # 商品a追加
$discount_manager->add( $product_b, $discount_b );    # 商品b追加
$discount_manager->add( $product_c, $discount_c );    # 商品c追加

say $discount_manager->discount_products->[0]->name;  # a 値引品リストの最初の商品の名前
say $discount_manager->total_price;                   # 2400 値引品の総額(aとb)

リスト8.2 夏季限定割引を管理するクラス

extend 'DiscountManager';

というわけで、とうとう「継承」を使う日が来てしまった。

こんな感じの構成でやります。

$ tree
.
├── DiscountManager.pm
└── SummerDiscountManager.pm

あと、DiscountManager.pm でコード動かすために書いてた package main; 以下はコメントアウトしてます。

うちがオブジェクト指向を学び始めた頃には、既に継承は「人類に早すぎた」ものとされていました。

だもんで、継承は使わないほうが良いもの、という認識でした。

しかし、こうやって改めて使ってみると、親クラスのメソッドやプロパティをそのまま使えるのはとてもとても便利ですね・・・!

ここクリックして展開

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

package Product {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has id => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
    );
    has name => (
        is       => 'ro',
        isa      => 'Str',
        required => 1,
    );
    has price => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
    );
    has can_discount => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
    );
    __PACKAGE__->meta->make_immutable();
}

package ProductDiscount {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has id => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
    );
    has can_discount => (
        is       => 'ro',
        isa      => 'Bool',
        required => 1,
    );
    __PACKAGE__->meta->make_immutable();
}

package SummerDiscountManager {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { TRUE => !!1, FALSE => !!0 };

    use lib qw/./;                # 同じフォルダにあるモジュールを探す
    extends 'DiscountManager';    # DiscountManager を継承

    method add($product) {
        croak "そんな商品idないよ" if ( $product->id < 0 );
        croak "商品の名前が空欄だよ" if ( $product->name eq "" );

        my $tmp = 0;

        # ???
        # Productクラスにcan_discountメソッド、8-01のコードになかったような?
        # => なかった
        # 同名同機能のメソッドが複数のクラスにばら撒かれるのは辛い
        if ( $product->can_discount ) {

            # total_price, get_discount_price は継承してきた DiscountManager のものを利用している
            $tmp =
              $self->total_price + $self->get_discount_price( $product->price );
        }
        else {
            $tmp = $self->total_price + $product->price;
        }

        if ( $tmp < 30000 ) {

            # total_price, add_product も継承したDiscountManagerから利用
            $self->total_price($tmp);
            $self->add_product($product);
            return TRUE;
        }
        else {
            return FALSE;
        }

    }

    __PACKAGE__->meta->make_immutable();
}

package main;

# 商品aと値引き情報
my $product_a =
  Product->new( id => 1, name => 'a', price => 1000, can_discount => 1 );

# 商品bと値引き情報
my $product_b =
  Product->new( id => 2, name => 'b', price => 2000, can_discount => 1 );

# 商品cと値引き情報
my $product_c =
  Product->new( id => 3, name => 'c', price => 30000, can_discount => 1 );

my $summer_discount_manager = SummerDiscountManager->new();    # インスタンス化
$summer_discount_manager->add($product_a);                     # 商品a追加
$summer_discount_manager->add($product_b);                     # 商品b追加
$summer_discount_manager->add($product_c);                     # 商品c追加

say $summer_discount_manager->discount_products->[0]->name;  # a 値引品リストの最初の商品の名前
say $summer_discount_manager->total_price;                   # 2400 値引品の総額(aとb)

リスト8.3 割引金額の仕様変更

ここで親クラスである DiscountManager の割引額を変更します。

    # 割引価格を取得する
    method get_discount_price($price) {

        # my $discount_price = $price - 300;
        my $discount_price = $price - 400;  # リスト8.3での変更
        $discount_price = 0 if $discount_price < 0;
        return $discount_price;
    }

すると、それを継承している SummerDiscountManager でも額が変わってしまいました。

まぁ、当然よな。

say $summer_discount_manager->discount_products->[0]->name;  # a 値引品リストの最初の商品の名前
say $summer_discount_manager->total_price;                   # 2200 値引品の総額(aとb)

いやー、書いてみるまでは「こんなん書かなくてもヤバいコードだってわかるし、書かなくていいか・・・」とか思ってたんですが、やっぱ書いて良かったです。

「これはいかん」「これは美しくない」みたいなのを体感できました。

リスト8.4〜リスト8.6 責務が単一になるようクラスを設計する

ここはこれまでのコードをきれいな構造にした、いわば解決編。

それだけにクラス図もスッキリ、コードもスッキリかけました。継承も非使用です。

ここクリックして展開

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

package RegularPrice {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { MIN_AMOUNT => 0 };

    # with 'RoleName';

    has amount => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
        trigger  => sub { croak '価格が0以上でありません', if $_[1] < 0 },
    );

    __PACKAGE__->meta->make_immutable();
}

package RegularDiscountPrice {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { MIN_AMOUNT => 0, DISCOUNT_AMOUNT => 400 };

    has regular_price => (
        is       => 'ro',
        isa      => 'Object',
        required => 1,
    );

    has amount => (
        is       => 'rw',
        isa      => 'Any',
        required => 0,
        builder  => "_build_amount",
    );

    sub _build_amount {
        my $self            = shift;
        my $discount_amount = $self->regular_price->amount - DISCOUNT_AMOUNT;

        $discount_amount = 0 if ( $discount_amount < MIN_AMOUNT );

        return $discount_amount;
    }

    __PACKAGE__->meta->make_immutable();
}

package SummerDiscountPrice {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { MIN_AMOUNT => 0, DISCOUNT_AMOUNT => 300 };

    has regular_price => (
        is       => 'ro',
        isa      => 'Object',
        required => 1,
    );

    has amount => (
        is       => 'rw',
        isa      => 'Any',
        required => 0,
        builder  => "_build_amount",
    );

    sub _build_amount {
        my $self            = shift;
        my $discount_amount = $self->regular_price->amount - DISCOUNT_AMOUNT;

        $discount_amount = 0 if ( $discount_amount < MIN_AMOUNT );

        return $discount_amount;
    }

    __PACKAGE__->meta->make_immutable();
}

package main;
my $regular_price = RegularPrice->new( amount => 1000 );
say $regular_price->amount;    # 1000

my $regular_discount_price =
  RegularDiscountPrice->new( regular_price => $regular_price );
say $regular_discount_price->amount();    # 600

my $summer_discount_price =
  SummerDiscountPrice->new( regular_price => $regular_price );
say $summer_discount_price->amount();     # 700

いろいろと学びが大きいですが、やはり

DRYにすべきは、それぞれの概念単位なのです。同じようなロジック、似ているロジックであっても、概念が違えばDRYにすべきではないのです。

ここが響きましたね。

いや、自作の経理楽するスクリプトで、買掛金処理、未払金処理とかで同じコードを共通化して、DRYだ!みたいなことをしていたので・・・(顔を背ける

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

リスト7.1 「牢屋の鍵」の所持を調べるコード

まずは例示コードを愚直に。

#!/usr/bin/env perl
use strict;
use warnings;
use feature qw/say/;
use constant { false => !!0, true => !!1 };

my $has_prison_key = false;

my @items = qw(steal_sword iron_mail pearl_ring prison_key);
for my $item (@items) {
    if ( $item eq 'prison_key' ) {
        $has_prison_key = true;
        last;
    }
}

say $has_prison_key;    # 1;

7.2 anyMatchメソッド

例示コードは Java なので anyMatch の紹介なのだけど、うちは Perl で書いているので

  • 標準で使える
  • リストや配列から一致するもので引っ張って来られる

という例示コードの条件を満たすなると、grep ですかね。

#!/usr/bin/env perl
use strict;
use warnings;
use feature qw/say/;
use constant { false => !!0, true => !!1 };

my $has_prison_key = false;

my @items = qw(steal_sword iron_armor pearl_ring prison_key);

$has_prison_key = grep { $_ eq 'prison_key' } @items;

say $has_prison_key;    # 1

ただ、コレクション処理、ということであれば Perl では標準で入っている List::Util を使うのが良いでしょう。

ありがちな関数はだいたい入ってます。

List::Util は Perl のバージョンが上がるごとに使える関数が増えているので、 id:xtetsuji さんの記事で補足しておくと良いでしょう。

qiita.com

なお、Perl をインストールすると List::Util の他にも便利モジュールが入ってきます。

corelist -v 5.36.0

とかやるとずらっと出てきます。

List::Util の他にも役に立つモジュールが見つけられるかもしれません。

リスト7.3 ありがちなネスト構造

ところで、悪い例示コードも書写してるんですが、このネストはなんというか、心削られるものがありますね・・・

なお、今回から「良いコードの肝」に型やオブジェクトが必要ない、関係なさそうな場合には作らずにいきたいと思います。

今回はハッシュリファレンスを配列の中に入れて代用してます。

これで少しは進捗の速度上がるかな

ここクリックして展開

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

my @members = (
    { name => 'alice', hit_point => 10, state => 'poison' },
    { name => 'bob',   hit_point => 20, state => '' },
    { name => 'carol', hit_point => 15, state => 'stone' },
);

for my $member (@members) {

    # hit_point が 0 より大きかったら
    if ( 0 < $member->{hit_point} ) {

        # 毒状態だったら
        if ( $member->{state} eq 'poison' ) {
            $member->{hit_point} -= 10;

            # hit_point が 0 以下になったら
            if ( $member->{hit_point} <= 0 ) {
                $member->{hit_point} = 0;
                $member->{state}     = 'dead';
            }

        }

    }

}

say Dumper @members;
# $VAR1 = {
#           'name' => 'alice',
#           'state' => 'dead',
#           'hit_point' => 0
#         };
# $VAR2 = {
#           'state' => '',
#           'name' => 'bob',
#           'hit_point' => 20
#         };
# $VAR3 = {
#           'name' => 'carol',
#           'state' => 'stone',
#           'hit_point' => 15
#         };

リスト7.4 早期 continue でネスト解消 〜 リスト7.5 if分のネストがすべて解消された

Perl の場合、ループ中の処理を途中で切り上げて次のループに移る時に使うのは next なので、こんな感じ。

ついでに、Perl では if 文の先頭に nextreturn を持ってくる後置 if って書き方ができるのでそれで。

ここでのポイントは、先の例とは条件式を反転させることですね。

例示コードはまとめて、すべてのネストが解消されたリスト7.5のものです。

ここクリックして展開

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

my @members = (
    { name => 'alice', hit_point => 10, state => 'poison' },
    { name => 'bob',   hit_point => 20, state => '' },
    { name => 'carol', hit_point => 15, state => 'stone' },
);

for my $member (@members) {

    # 生存していない場合 next で次のループ処理に移行する
    # 早期 netxt への変更には、条件を反転させる
    next if ( $member->{hit_point} == 0 );

    # 毒状態でなかったら next
    # 文字列の比較演算子 ne(not equal)を利用 
    next if ( $member->{state} ne 'poison' );
    $member->{hit_point} -= 10;

    # hit_point が 0 より大きかったら next
    next if ( 0 < $member->{hit_point} );

    $member->{hit_point} = 0;
    $member->{state}     = 'dead';

}

say Dumper @members;
$VAR1 = {
          'state' => 'dead',
          'hit_point' => 0,
          'name' => 'alice'
        };
$VAR2 = {
          'state' => '',
          'name' => 'bob',
          'hit_point' => 20
        };
$VAR3 = {
          'state' => 'stone',
          'hit_point' => 15,
          'name' => 'carol'
        };

リスト7.7 早期breakで見通し改善

Perl の場合、ループ中の処理を抜け出す時に使うのは last なので、こんな感じ。

ここクリックして展開

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

my @members = (
    {
        name                => 'alice',
        attack              => 35,
        team_attack_succeed => team_attack_succeed()
    },
    {
        name                => 'bob',
        attack              => 40,
        team_attack_succeed => team_attack_succeed()
    },
    {
        name                => 'carol',
        attack              => 50,
        team_attack_succeed => team_attack_succeed()
    },
);

# 連携の成功/失敗の結果を生成する関数
sub team_attack_succeed {
    my $result = rand(1);
    return 1 if $result > 0.8;
    return 0;
}

my $total_damege = 0;

for my $member (@members) {
    last if ( $member->{team_attack_succeed} );

    my $damege = $member->{attack} * 1.1;

    last if $damege < 30;

    $total_damege += $damege;
}

say $total_damege;

リスト7.8 〜 7.13

コレクション処理も低凝集に陥りやすい。

ここでの例では、RPGにおいて「パーティメンバーが1人でも生存しているか?」という判定メソッドや、「メンバーを追加する」のような、よく利用されるメソッドが色々なところで実装されて重複コードになっているというもの。

時には名前が違うだけで、同じ処理内容のコードが生まれることも・・・これは低凝集ですね。

ところで、コレクション処理っていうと、何かの共通項を持った要素の集まり、程度の認識で良いのかな?

例示のコードだと、 member を集めて members みたいな。

まぁ、そんな認識でやっていこう。

このコレクションの低凝集を解決するためにすることが、コレクション処理のカプセル化

ファーストクラスコレクション。おぉ、初めて聞いた・・・

強靭なクラスを作るための

を応用し

を備えたものとのこと。

うーん、うーん?

まぁ、書いていくか。ここはちゃんとオブジェクトで書いていかないとダメっぽそう。

ここクリックして展開

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

package Member {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has name => (
        is       => 'ro',
        isa      => 'Str',
        required => 1,
    );
    __PACKAGE__->meta->make_immutable();
}

package Party {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has _members => (
        is       => 'ro',                      # イミュータブル(不変)にする
        isa      => 'ArrayRef[Object]',        # Mouse が用意している型
        required => 0,
        default  => sub { _members => [] },    # デフォルトを設定し、生焼けオブジェクトにしない
    );

    # プライベートにしたインスタンス変数に直接アクセスはさせたくないが、
    # 参照はしたい。ので、アクセサ(getterとかsetter)を作る。
    # メソッド名 get_members とかにするところだが、本に合わせて party とする。
    method party() {
        my @members =
          @{ $self->_members() };    # ここでデリファレンスにしておかないとイミュータブルにならなくて泣く
        return \@members;
    }

    # MouseX::AttributeHelper で provide => {add => 'add'}を使っても
    # よかったが、本文でオブジェクトをイミュータブルにしているので、
    # それなら使わなくてもいいか、と言うわけで自前実装
    # 一人ずつ追加の実装
    method add(:$new_member) {
        my $adding = $self->party();
        push @{$adding}, $new_member;
        return Party->new( _members => $adding );
    }

    __PACKAGE__->meta->make_immutable();
}

package main;

my $alice = Member->new( name => 'alice' );
my $bob   = Member->new( name => 'bob' );
my $party = Party->new();

my $alice_in_party = $party->add( new_member => $alice );

say $alice_in_party->party()->[0]->name;    # alice

my $bob_in_party = $alice_in_party->add( new_member => $bob );

say $bob_in_party->party()->[1]->name;      # bob

リスト型をインスタンス変数で持つ、をこう表現してみた

    has _members => (
        is       => 'ro',                      # イミュータブル(不変)にする
        isa      => 'ArrayRef[Object]',        # Mouse が用意している型
        required => 0,
        default  => sub { _members => [] },    # デフォルトを設定し、生焼けオブジェクトにしない
    );

で、メンバーに変更があったら(今回は加入)、加入した情報でオブジェクトを作り直して渡す、と。

    method add(:$new_member) {
        my $adding = $self->party();
        push @{$adding}, $new_member;
        return Party->new( _members => $adding );
    }

ふむふむ。

ここはわかった。

ところで、渡した後のオブジェクトは用済みにしておかなくていいのかな・・・?

あれ、それってシングルトンってやつ?違うか?まぁいいや。

先に進もう。

リスト 7.14 リスト操作に必要なロジックを同じクラスに定義

ここで、コードが整形できてなくて???となる。

Perl では perltidy というツールでコードを整形するのだけど、それが効いてない。

原因は Function::Parameters での名前付き引数 :$new_member のところだった。

    method add(:$new_member) {

便利なんだけど、コードは整形したいから封印かなぁ。

残念。

あと、ちょっと前から真偽値で !!1 とか !!0 ってのを使ってます。

Perl 5.36 では組み込みの変数でとうとう truefalse ってのが実験的機能として追加されました。

早いこと実験的が外れるといいなぁ。

metacpan.org

それまでは

    use constant {  true => !!1, false => !!0 };

で生きていこうと思います。

で、Party クラスに、必要なコレクションを追加したのが以下のコード。

もっと手間かかると思ったけど、意外とあっさり終わったなぁ。

ファーストクラスコレクション、コレクション処理を1箇所にまとめたクラス。

今回は追加とか、メンバー数の判定とか、生存者確認とか。

やっぱ、コード書いてみると理解度が上がるなぁ。

ここクリックして展開

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

package Member {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has name => (
        is       => 'ro',
        isa      => 'Str',
        required => 1,
    );

    has id => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
    );

    has is_alive => (
        is       => 'ro',
        isa      => 'Bool',
        required => 0,
    );

    __PACKAGE__->meta->make_immutable();
}

package Party {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use constant { MAX_MEMBER_COUNT => 4, TRUE => !!1, FALSE => !!0 };

    has _members => (
        is       => 'ro',                      # イミュータブル(不変)にする
        isa      => 'ArrayRef[Object]',        # Mouse が用意している型
        required => 0,
        default  => sub { _members => [] },    # デフォルトを設定し、生焼けオブジェクトにしない
    );

    method party() {
        my @members = @{ $self->_members() };
        return \@members;
    }

    # メンバーを追加する
    method add($new_member) {
        croak "すでにパーティに加わっています" if ( $self->exists($new_member) );

        croak "これ以上メンバーを追加できません" if ( $self->is_full );

        my $adding = $self->party();
        push @{$adding}, $new_member;
        Party->new( _members => $adding );
    }

    # パーティのメンバーが1人でも生存している場合 true
    method is_alive() {
        return TRUE if ( grep { $_->is_alive == 1 } @{ $self->party() } );
    }

    # パーティに所属しているかを調べたいメンバー
    # 既にパーティに所属している場合 true
    method exists($member) {
        return TRUE if ( grep { $_->id eq $member->id } @{ $self->party() } );
    }

    # パーティが満員の場合 true
    method is_full() {
        my $count = scalar @{ $self->party() };
        return TRUE if ( MAX_MEMBER_COUNT == $count );
    }

    __PACKAGE__->meta->make_immutable();
}

package main;

my $alice = Member->new( name => 'alice', id => 1, is_alive => 1 );
my $bob   = Member->new( name => 'bob',   id => 2, is_alive => 1 );
my $party = Party->new();

my $alice_in_party = $party->add($alice);

say $alice_in_party->party()->[0]->name;    # alice

my $bob_in_party = $alice_in_party->add($bob);

say $bob_in_party->party()->[1]->name;      # bob

# my $bob_in_party_twice = $bob_in_party->add($bob);

リスト 7.17 外部には不変にして渡す

へー! Java にはそういうメソッドがあるんだ。unmodifiableList 知らなかった。

と言うことで、第7章終わり!

「良いコード/悪いコードで学ぶ設計入門」第1章〜第6章のまとめ #ミノ駆動本

一旦まとめ

7章を目前に、どの内容をどこで学んだんだっけ・・・?というのを自分なりにまとめておきたくなりました。

コード書いたからか、結構覚えているなーという感じです。

あと、本を先に進めたい!という気が勝っておざなりにしてるところがあるなぁ、と。

普遍的な変数のところですね。

Perl の場合には Readonlyconstant を使うのですが・・・ううん(顔を背ける)

さて、7章以降もがんばってくぞー

第1章 悪しき構造の弊害を知覚する

  • 弊害とは

    • コードを読み解くのに時間がかかる
    • バグを埋め込みやすくなる
    • 悪しき構造がさらに悪しき構造を誘発する
  • 意味不明な命名

    • 技術駆動命名
      • 変数名に int, flag などプログラミング用語やコンピュータ用語に基づいた名前
      • 内容や用途に基づいてない名前
    • 連番命名
      • Class00, method01 など
  • 何重にもネストしたロジック

  • 巨大なネスト
  • データを保持するだけのクラス
    • データを扱うメソッドがない
    • 重複コード(重複メソッド)がコードのあちらこちらに書かれてしまう
    • 修正漏れが発生する可能性
    • 可読性の低下
    • 生焼けオブジェクト
      • 未初期化状態のオブジェクトが利用できてしまう
        • 当然利用しようとすれば undef になる
    • 不正値の混入
  • 悪魔退治の基本

第2章 設計の初歩

  • 省略せずに伝わる名前を設計する
  • 変数を使いまわさない、目的ごとの変数を用意する
    • 再代入を避ける
  • 意味あるまとまりでメソッド化する
    • サブルーチン化か
    • 種類の異なる処理をメソッドにまとめる
    • だらだらと書かない
    • 理解のしやすさを優先に、行数や変数名の文字数が増えることを厭わない
  • 関係し合うデータとロジックをクラスにまとめる
    • データクラスの問題でも書かれていた「いろいろなところに類似のロジックが書かれる」問題

第3章 クラス設計

  • オブジェクト指向は、ソフトウェアの品質向上を目的とする考え方の一種
  • 適切なクラス設計により、保守や変更が容易になる
  • 頑強なクラスの構成要素
    • インスタンス変数
    • インスタンス変数を不正状態から防御し、正常に操作するメソッド
    • 基本的にインスタンス変数とメソッドはワンセット、一つのクラスに入れる
      • 例外はある
    • 不正値チェックをクラス内で行うことができる
      • データしか持たないデータクラスだとできない
  • 自己防衛責務を持たせる
    • 一つ一つのクラスが完結している
      • NG:他のクラスによる初期化が必要
      • NG:データ入力を他のクラスにしてもらう
  • 成熟したクラスへ成長させる設計術
    • コンストラクタで確実に正常値を設定する
      • インスタンス変数に格納する前に、正常値であるかをチェックする
      • 不正値だったらガード節で即エラーを出しインスタンスを作成させない
  • 計算ロジックをデータ保持側に寄せる
  • 不変で思わぬ動作を防ぐ
    • 変数の上書きができると思わぬ副作用を招く
    • final, const Readonly 使おう
  • インスタンス変数を変更したい場合は、インスタンスごと作り直す
  • メソッド引数やローカル変数も不変にする
  • 「値の渡し間違い」を型で防止する
    • 引き数をプリミティブ型にしない
      • Int 型ではなく、Money 型などで渡す
    • 同じ型同士で処理するように、メソッドの引数の型チェックを行う
  • 現実の営みにないメソッドを追加しない

    プログラム構造の問題解決に役立つ設計パターン

  • 完全コンストラク
    • 生焼けオブジェクトを作らせない
      • コンストラクタで生成する時点でしっかり引数入れて引き数チェックもする
  • 値オブジェクト
    • 値の概念そのものをクラスとして定義する
  • 「値オブジェクト」+「完全コンストラクタ」はオブジェクト指向設計の最も基本形を体現している構造の一つ

第4章 不変の活用

  • 再代入を避ける
    • 不変にする
    • 引数も不変にする
  • 関数による可変インスタンスの操作
    • 主作用(関数が値を受け取り、値を返す)以外の副作用が出ないようにする
    • データを引数で受け取る
    • 状態は変更しない
    • 値は関数の戻り値として返す
    • インスタンス変数を不変にしておくことで、副作用の余地をなくす
      • 変えようとするとエラーになるから
      • 意図して変えたい時はちゃんとメソッド作る
  • 不変と可変の取り扱い方針
    • デフォルトは不変
    • スコープが局所的なケースのみ可変
      • ループカウンタなど
    • 可変の変数で状態を変更する時は、状態変更のみ発生するように設計する
      • 副作用がないようにする
    • コード外とのやり取りは局所化する

第5章 低凝集

  • static メソッドを誤用しない
    • static メソッドはインスタンス変数を利用できない
      • このため、第3章の「頑強なクラス」の要件を満たさない
    • static がついていないだけで、同じ問題を抱える(インスタンス変数を用いない)クラスも作成可能だが、もちろん作らない
    • staticメソッドは、凝集度に無関係なものに利用する
      • ログ出力
      • フォーマット変換
      • ファクトリメソッド
  • 初期化ロジックの分散
    • コンストラクタを公開すると、さまざまなところで利用され、メンテが大変になる
    • コンストラクタをプライベートメソッドにすることで、外部から直接インスタンス生成させない
      • 内部でだけインスタンス化できる
      • インスタンス化したものを返す
      • static なファクトリメソッドが生きる
        • データと結びついていない(結びついている必要がない)
        • インスタンス化しなくても利用できる
  • 共通処理クラス
    • common, util などと名付けられるクラス
    • 低凝集になりやすい
    • static メソッドが入り込みやすい
    • 第2章でいうところの「意味のあるまとまり」でまとめられるべきメソッドが、共通処理クラスに入れられる
      • クラス設計の基本に立ち返る
    • 横断的関心ごとを共通処理クラスにする
      • static クラスにしても良い
      • ログ出力
      • エラー検出
      • デバッグ
      • 例外処理
      • キャッシュ
      • 同期処理
      • 分散処理
  • 結果を返すために引数を使わない(?)
    • もっといい言い換えがありそう。
      • 「出力引数」、直感的じゃない。
    • インスタンスと数値を渡し、インスタンスをその数値によって変更する例
    • そのインスタンスに変更メソッドとして実装すべき
      • クラスは、データとそれに関するメソッドをまとめるものという基本に立ち返る
    • メソッドの中身を確認しないとわからないような名前づけや処理にしない
  • 多すぎる引数
    • 引数が多い = 処理する内容が多い
      • 適切なクラス設計ができていない可能性
    • プリミティブ執着しない
      • コード重複が生じやすい
      • 適切なクラス(の型)を渡す
        • その型に必要なインスタンス変数やメソッドは、その型に集約され、高凝集になる
    • 意味のある単位ごとにクラス化する
  • メソッドチェイン
    • 似たようなコードが量産される原因の一つ
    • デメテルの法則「利用するオブジェクトの内部を知るべきではない」
    • 「尋ねるな、命じろ」
      • 他のオブジェクトの状態を尋ねない。他のオブジェクトの状態に応じて呼び出し側が判断をしない
      • 命じられた側で判断する
      • 詳細なロジックは、呼ぶ側ではなく、呼ばれる側に実装する

第6章 条件分岐

  • 条件分岐のネストによる可読性低下
    • 早期 return で解消
      • 条件と実行の分離
      • 条件の追加が容易になる
      • else 句をなくすことも可能
  • switch 文の重複
    • 条件分岐は同じだが、返り値だけ異なる switch 文が量産されやすい
      • 量産されることで、仕様変更・条件追加・条件削除時の修正漏れが生じやすい
      • switch 文は増えやすい
    • 条件分岐は1箇所にまとめる
    • interface を使い、スマートに重複を解決する
      • 同名のメソッド(例:area)を引数で渡すクラスに実装しておく
      • 呼ぶメソッドは area() で共通
      • ダックタイピング
      • Perl だと Mo[o|u]se::role
      • interface は利用するクラスに共通のメソッドがあることを要求するので、実装漏れにも対処できる
  • 条件分岐の重複とネスト
    • ポリシーパターンで対処
      • 条件の部品化、部品化した条件の組み替え
      • 条件ごとのクラスを作、(例:GoldCustomer, SilverCustome)条件判定するメソッドを実装する(例:ok)
      • okメソッドを持っているクラスを集約する interface を作る
      • GoldCuster クラスでは、すべての条件を満たす、SilverCustomer クラスでは2つの条件を満たす、などの条件で実装する
      • switch や if 文を使わずとも、条件分岐が可能に
  • 型チェックで分岐しない
    • interface を実装しても、実装したメソッド側で型による分岐をしたのではもったいない
      • 条件分岐削減の役に立っていない
    • if や switch の代わりに interface が使えないかを考える
  • フラグ引数
    • メソッド側で処理を分岐するためにつける引数
    • 型オブジェクトを渡し、interface を実装して共通のメソッドで処理させる

「良いコード/悪いコードで学ぶ設計入門」第6章その4 フラグ引数 #ミノ駆動本

リスト6.62

フラグ引数を使っている例

damege(true, damegeAmount);

確かに、これだけでは何が true なのかはコードを追いかけて関数の中身を見ないとわからんよなぁ。

ということで、それを「メソッドを分離する」で改良したところから。

ここクリックして展開

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

package Member {
    use Carp qw/croak/;
    use Mouse;
    use MouseX::AttributeHelpers;
    use namespace::autoclean;

    has hit_point => (
        metaclass => 'Number',
        is        => 'rw',
        isa       => 'Int',
        default   => 0,
        provides  => {
            sub => 'hit_sub',
        }
    );
    has magic_point => (
        metaclass => 'Number',
        is        => 'rw',
        isa       => 'Int',
        default   => 0,
        provides  => {
            sub => 'magic_sub',
        }
    );
    has state => (
        is       => 'rw',
        isa      => 'Str',
        required => 1,
    );

    __PACKAGE__->meta->make_immutable();
}

package Damege {
    use Mouse::Role;
    requires 'execute';
}

package HitPointDamege {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    with 'Damege';

    has member => (
        is       => 'ro',
        isa      => 'Member',
        required => 1,
    );

    has damege_amount => (
        is       => 'ro',
        isa      => 'Int',
        required => 0,
    );

    method execute($damege_amount) {

        my $member    = $self->member;
        my $hit_point = $member->hit_sub($damege_amount);

        return $member if ( 0 < $hit_point );

        $member->hit_point(0);
        $member->state('dead');
        return $member;

    }

    __PACKAGE__->meta->make_immutable();
}

package MagicPointDamege {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    with 'Damege';

    has member => (
        is       => 'ro',
        isa      => 'Member',
        required => 1,
    );

    has damege_amount => (
        is       => 'ro',
        isa      => 'Int',
        required => 0,
    );

    method execute($damege_amount) {

        my $member      = $self->member;
        my $magic_point = $member->magic_sub($damege_amount);

        return $member if ( 0 < $magic_point );

        $member->magic_point(0);
        return $member;

    }

    __PACKAGE__->meta->make_immutable();
}

package main;

# メンバークラスのインスタンス
my $member = Member->new( hit_point => 100, state => '' );

# ヒットポイントクラスのインスタンス
my $hit_point_damege = HitPointDamege->new( member => $member );

# ヒットポイントのダメージメソッド
my $dameged_member = $hit_point_damege->execute(10);

# メンバーの現時点のヒットポイント
say $dameged_member->hit_point();    # 90

# メンバーの現時点の状態
say $dameged_member->state();        # 空欄

my $dead_member = $hit_point_damege->execute(100);
say $dead_member->hit_point();       # 0
say $dead_member->state();           # dead

$member = Member->new( hit_point => 100, magic_point => 50, state => '' );
my $magic_point_damege = MagicPointDamege->new( member => $member );
my $use_magic_member   = $magic_point_damege->execute(10);
say $use_magic_member->magic_point;    # 40

my $more_use_magic_member = $magic_point_damege->execute(1000);
say $more_use_magic_member->magic_point;    # 0

リスト6.63 〜 リスト6.66

ここでやっと、ストラテジパターンとはこういうものかな?という引っ掛かりが得られた気がします。

(理解したとは言えない)

関数に渡す引数にオブジェクトを渡すことで、オブジェクトに含まれたメソッドを活用できるんだなぁ、と。

今まではそれを禁忌とでも思って避けていました。

つまり、プリミティブ型執着(第5章)ですね。

Enum 使わずにやってますが、ここのポイントはこれですね。

package Damege {
    use Mouse::Role;
    requires 'execute';

    method apply_damege( $damege_type, $damege_amount ) {

        return $damege_type->execute($damege_amount);
    }
}

引数にオブジェクトを渡す。

渡ってくるオブジュエクトはインターフェイスで必要なメソッドを備えていることは保証されているので、ダックタイピングで処理できる。

if 文で分岐せずとも、引数にオブジェクトを渡した時点で処理のルートが確定する。

頭いいなぁ!すごいなぁ。

そして、昔、これに似たコードをわからないまま触った記憶が蘇ってきました。こういう意味だったのか・・・

あの時に理解したかったなぁ。

ここクリックして展開

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

package Member {
    use Carp qw/croak/;
    use Mouse;
    use MouseX::AttributeHelpers;
    use namespace::autoclean;

    has hit_point => (
        metaclass => 'Number',
        is        => 'rw',
        isa       => 'Int',
        default   => 0,
        provides  => {
            sub => 'hit_sub',
        }
    );
    has magic_point => (
        metaclass => 'Number',
        is        => 'rw',
        isa       => 'Int',
        default   => 0,
        provides  => {
            sub => 'magic_sub',
        }
    );
    has state => (
        is       => 'rw',
        isa      => 'Str',
        required => 1,
    );

    __PACKAGE__->meta->make_immutable();
}

package Damege {
    use Mouse::Role;
    requires 'execute';

    method apply_damege( $damege_type, $damege_amount ) {

        return $damege_type->execute($damege_amount);

    }

}

package HitPointDamege {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    with 'Damege';

    has member => (
        is       => 'ro',
        isa      => 'Member',
        required => 1,
    );

    has damege_amount => (
        is       => 'ro',
        isa      => 'Int',
        required => 0,
    );

    method execute($damege_amount) {

        my $member    = $self->member;
        my $hit_point = $member->hit_sub($damege_amount);

        return $member if ( 0 < $hit_point );

        $member->hit_point(0);
        $member->state('dead');
        return $member;

    }

    __PACKAGE__->meta->make_immutable();
}

package MagicPointDamege {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    with 'Damege';

    has member => (
        is       => 'ro',
        isa      => 'Member',
        required => 1,
    );

    has damege_amount => (
        is       => 'ro',
        isa      => 'Int',
        required => 0,
    );

    method execute($damege_amount) {

        my $member      = $self->member;
        my $magic_point = $member->magic_sub($damege_amount);

        return $member if ( 0 < $magic_point );

        $member->magic_point(0);
        return $member;

    }

    __PACKAGE__->meta->make_immutable();
}

package main;

my $member           = Member->new( hit_point => 100, state => '' );
my $hit_point_damege = HitPointDamege->new( member => $member );
my $dameged_member   = Damege->apply_damege( $hit_point_damege, 10 );
say $dameged_member->hit_point();    # 90
say $dameged_member->state();        # 空欄

# クリティカルヒット!
my $dead_member = Damege->apply_damege( $hit_point_damege, 100 );
say $dead_member->hit_point();       # 0
say $dead_member->state();           # dead

# 復活
$member = Member->new( hit_point => 100, magic_point => 50, state => '' );

# マジックポイントにダメージ
my $magic_point_damege = MagicPointDamege->new( member => $member );
my $use_magic_member   = Damege->apply_damege( $magic_point_damege, 10 );
say $use_magic_member->magic_point;    # 40

# マジックポイント枯渇
my $more_use_magic_member = Damege->apply_damege( $magic_point_damege, 1000 );
say $more_use_magic_member->magic_point;    # 0

やっと6章終わった

理解が難しかったのと、Perl で同じような動作をするための環境づくりで結構時間食った気がします。

ただ、得られたものはあったなぁ、と充足感ありあり。

この先の章も楽しみ〜

「良いコード/悪いコードで学ぶ設計入門」第6章その3 型チェックで分岐しない #ミノ駆動本

リスト6.51〜6.54まで

一気にまとめたコードがこれ。

途中でしれっと出てくる Money クラスの add メソッドも書いております。

・・・Perl で書き換えながらめっちゃ書きにくさを感じることがあります。

このコードだと、interface の HotelRates の中で分岐コードを書くところ。

まぁ、ここで書いているのは「悪い(例示の)コード」なので、書きにくくて正解なんですけどね。

そうそう。

途中、コメントアウトしたところがあります。

# return $self->fee()->add( Money->new( amount => 3000 ) );

my $money = $self->fee();
return $money->add( Money->new( amount => 3000 ) );

1行でメソッドチェーンでかけるんですが、途中途中で返り値はなんだっけ・・・?

値?値オブジェクト?

みたいになったので、統一した方がいいんだろうなぁ、と思いました。

ここクリックして展開

use strict;
use warnings;
use Function::Parameters;
use feature qw/say/;

package Money {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use Readonly;
    use constant { MIN => 0 };

    has amount => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
    );

    method add($other) {

        my $added = $self->amount() + $other->amount();
        return Money->new( amount => $added );
    }

    __PACKAGE__->meta->make_immutable();
}

# 宿泊料金を表すロール
package HotelRate {
    use Mouse::Role;
    requires 'fee';

    fun busy_season_fee($self) {
        if ( ref $self eq 'RegularRates' ) {

            # return $self->fee()->add( Money->new( amount => 3000 ) );
            my $money = $self->fee();
            return $money->add( Money->new( amount => 3000 ) );

        }
        elsif ( ref $self eq 'PremiumRates' ) {

            # return $self->fee()->add( Money->new( amount => 5000 ) );
            my $money = $self->fee();
            return $money->add( Money->new( amount => 5000 ) );
        }
    }

}

#通常宿泊料金
package RegularRates {
    use Mouse;
    use namespace::autoclean;

    with 'HotelRate';

    method fee() {
        return Money->new( amount => 7000 );
    }
    __PACKAGE__->meta->make_immutable();

}

# プレミアム宿泊料金
package PremiumRates {
    use Mouse;
    use namespace::autoclean;

    with 'HotelRate';

    method fee() {
        return Money->new( amount => 12000 );
    }
    __PACKAGE__->meta->make_immutable();
}

package main;

# 追加した繁忙期ロジック

# 通常宿泊料金&繁忙期
my $regular_rate = RegularRates->new();
say $regular_rate->fee->amount();                  # 7000
say $regular_rate->busy_season_fee()->amount();    # 10000

# プレミアム宿泊料金&繁忙期
my $premium_rate = PremiumRates->new();
say $premium_rate->fee->amount();                  # 12000
say $premium_rate->busy_season_fee()->amount();    # 17000

リスト6.55 繁忙期料金を切り替えられるよう interface に定義

先の「悪いコード」に比べて、「いいコード」の方が直感的ですね。

あと、「図6.8」みたいなオブジェクト図のがいまいちわからなかったんですが、コードを見てからだと理解できるという。

というか、オブジェクト指向も書物を読んだだけではわからなくて、書いてみて、書き続けてやっとわかるみたいな。

割と脳筋使うところだなぁという感じです。

ここクリックして展開

use strict;
use warnings;
use Function::Parameters;
use feature qw/say/;

package Money {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has amount => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
    );

    method add($other) {

        my $added = $self->amount() + $other->amount();
        return Money->new( amount => $added );
    }

    __PACKAGE__->meta->make_immutable();
}

# 宿泊料金を表すロール
package HotelRate {
    use Mouse::Role;

    requires qw/fee busy_season_fee/;
}

#通常宿泊料金
package RegularRates {
    use Mouse;
    use namespace::autoclean;

    with 'HotelRate';

    method fee() {
        return Money->new( amount => 7000 );
    }

    method busy_season_fee() {
        my $money = $self->fee();
        return $money->add( Money->new( amount => 3000 ) );
    }

    __PACKAGE__->meta->make_immutable();

}

# プレミアム宿泊料金
package PremiumRates {
    use Mouse;
    use namespace::autoclean;

    with 'HotelRate';

    method fee() {
        return Money->new( amount => 12000 );
    }

    method busy_season_fee() {
        my $money = $self->fee();
        return $money->add( Money->new( amount => 5000 ) );
    }
    __PACKAGE__->meta->make_immutable();
}

package main;

# 追加した繁忙期ロジック

# 通常宿泊料金&繁忙期
my $regular_rate = RegularRates->new();
say $regular_rate->fee->amount();                  # 7000
say $regular_rate->busy_season_fee()->amount();    # 10000

# プレミアム宿泊料金&繁忙期
my $premium_rate = PremiumRates->new();
say $premium_rate->fee->amount();                  # 12000
say $premium_rate->busy_season_fee()->amount();    # 17000

よし、6章も残りは「フラグ引数」のみ。がんばろー

おまけ

MouseX::AttributeHelpers; を使ってみたパターン。

さっぱり!

ここクリックして展開

use strict;
use warnings;
use Function::Parameters;
use feature qw/say/;

package Money {
    use Carp qw/croak/;
    use Mouse;
    use MouseX::AttributeHelpers;
    use namespace::autoclean;

    has amount => (
        metaclass => 'Number',
        is        => 'rw',
        isa       => 'Int',
        required  => 1,
        default   => 0,
        provides  => {
            add => 'add',
        }
    );

    __PACKAGE__->meta->make_immutable();
}

# 宿泊料金を表すロール
package HotelRate {
    use Mouse::Role;

    requires qw/fee busy_season_fee/;
}

#通常宿泊料金
package RegularRates {
    use Mouse;
    use namespace::autoclean;

    with 'HotelRate';

    method fee() {
        return Money->new( amount => 7000 );
    }

    method busy_season_fee() {
        my $money = $self->fee();
        return $money->add(3000);
    }

    __PACKAGE__->meta->make_immutable();

}

# プレミアム宿泊料金
package PremiumRates {
    use Mouse;
    use namespace::autoclean;

    with 'HotelRate';

    method fee() {
        return Money->new( amount => 12000 );
    }

    method busy_season_fee() {
        my $money = $self->fee();
        return $money->add(5000);
    }
    __PACKAGE__->meta->make_immutable();
}

package main;

# 追加した繁忙期ロジック

# 通常宿泊料金&繁忙期
my $regular_rate = RegularRates->new();
say $regular_rate->fee->amount();        # 7000
say $regular_rate->busy_season_fee();    # 10000

# プレミアム宿泊料金&繁忙期
my $premium_rate = PremiumRates->new();
say $premium_rate->fee->amount();        # 12000
say $premium_rate->busy_season_fee();    # 17000