sironekotoroの日記

Perl で楽をしたい

「良いコード/悪いコードで学ぶ設計入門」第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だ!みたいなことをしていたので・・・(顔を背ける