sironekotoroの日記

Perl で楽をしたい

「良いコード/悪いコードで学ぶ設計入門」第13章 モデリング 〜 第14章 リファクタリング

13章もパラパラっとページをめくってみた感じ、文章がメインぽいですね。

13.1 邪悪な構造に陥りがちな User クラス

面白い。いかにもありそうな気がする。

  • User というログインユーザーを示すクラスを作る
  • そこにIDや名前などのプロパティを追加していく
  • User を管理するための UserManager というクラスも作る
  • さらに法人ユーザーも同じ User クラスを使い(ん?)
  • 法人番号などのプロパティを追加していく(個人ユーザー使わないよね、そのプロパティ)
  • 法人ユーザー を管理するための CorporationManager というクラスも作る

そして、UserMaganeger, CorporationManager 双方が User クラスを見た時・・・

  • CorporationManager が個人 User の法人番号が空白としてエラー(個人ユーザーだし当然)
  • UserManager が人名に利用できない (株) などの文字を利用しているとしてエラー

これらを回避するために、分岐が増え・・・メンテナンスが難しくなり・・・おぉ、目に見えるようだ。

13.2 モデリングの考え方とあるべき構造

  • モデルはシステム構造の説明のために用いる
  • システムとは何か?
  • システムは目的達成のための手段
  • 特定の目的達成のために、最低限考慮が必要な要素を備えたものがモデル

ここで、例として通販サイトにおける商品モデルを取り上げるが、商品を構成するプロパティを全部突っ込んである巨大な商品モデルになっている。

これを、「注文時の商品モデル」「配送時の商品モデル」のように、目的ごとに定義した商品モデルにする。

13.3 よくないモデルの問題点と解決方法

上記の User モデルは、複数の目的のために無理やり利用されており、モデリングしているようでモデリングしていないといえる

うむ。

「User が持ちうるもの」という意味では一貫しているといえるけど、特定の目的のためという意味では不要なプロパティも多いよなぁ。

ここから先は文章が続き、本の要約にしかならないので、しばし飛ばします。

飛ばしつつ、気になったところを書き抜きます。

  • 特定の目的に特化して設計することで、変更に強い高品質な構造になる
  • モデルに目的外の要素が入り込んでいる場合、さらに見直す

思えば、自分も巨大モデルを作りたがる傾向がある気がします。

それで楽をしてきたと思うのですが、果たして本当に楽だっただろうか・・・?

うーん。

13.4 機能を左右するモデリング

  • 裏に隠れた真の目的を見破る
    • これは起こりうるトラブルを解決するための情報が揃えられるか?的な観点で説明してる
  • 機能性をイノベートする「深いモデル」
    • 目的・手段に応じた抽象化をする
    • 本質的課題を解決し、機能性の確信に貢献するモデル

14.1 リファクタリングの流れ

  • 外から見た挙動を変えずに、構造を整理すること
  • おっと、久々に骨のありそうなコードだ。書いてみよう。
  • なお、久々すぎてJavaのコードの読み方をすっかり忘れてしまっていた
  • リスト14.1相当のつもりで書き始めたけど、Mouse (使ってのオブジェクト指向)の書き方に従ってたらリスト14.5相当くらいのコードになってた。

ここクリックして展開

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

package Customer {
    use Mouse;

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

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

    has possession_point => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
        default  => 1000,
    );

    sub is_enabled {
        return !!1;
    }

}

package Comic {
    use Mouse;

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

    has current_purchase_point => (
        is      => 'ro',
        isa     => 'Int',
        default => 100,
    );

    sub is_enabled {
        return !!1;
    }

}

package PurchasePointPayment {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use Time::Piece;

    # 購入者
    has customer => (
        is       => 'ro',
        isa      => 'Customer',
        required => 1,
        trigger  => sub {
            croak "有効な購入者ではありません。" unless $_[1]->is_enabled;
        }
    );

    # 購入するWebコミック
    has comic => (
        is       => 'ro',
        isa      => 'Comic',
        required => 1,
        trigger  => sub {
            croak "現在取扱できないコミックです。" unless $_[1]->is_enabled;
        }
    );

    # 購入日時
    has payment_date_time => (
        is       => 'ro',
        isa      => 'Time::Piece',
        required => 0,
        default  => sub {
            croak "所持ポイントが不足しています。"
              unless $_[0]->comic->current_purchase_point <=
              $_[0]->customer->possession_point;

            return localtime();
        }
    );

    __PACKAGE__->meta->make_immutable();
}

package main;

my $purchase_pointp_ayment = PurchasePointPayment->new(

    customer => Customer->new( name => 'sironekotoro', id => 1 ),
    comic    => Comic->new( id => 10 ),
);

  • ここからは以下の改修ポイントを実装していく。
  • unless $_[1]->is_enabled; でも良い気がするが、if $_[1]->is_disabled; にしておく
  • 購入日のプロパティでの残高チェックのサブルーチンを、customer に移す

で、改修したのがこちら。

しれっと use v5.36;, use signatures 使ってます。

ここクリックして展開

#!/usr/bin/env perl use strict; use warnings; use v5.36; use feature qw/say signatures/; package Customer { use Mouse; has name => ( is => 'ro', isa => 'Str', required => 1, ); has id => ( is => 'ro', isa => 'Int', required => 1, ); has possession_point => ( is => 'ro', isa => 'Int', required => 1, default => 1000, ); sub is_disabled { return !!0; } sub is_short_of_point ( $self, $comic ) { return !!1 if $self->possession_point <= $comic->current_purchase_point; } } package Comic { use Mouse; has id => ( is => 'ro', isa => 'Int', required => 1, ); has current_purchase_point => ( is => 'ro', isa => 'Int', default => 100, ); sub is_disabled { return !!0; } } package PurchasePointPayment { use Carp qw/croak/; use Mouse; use namespace::autoclean; use Time::Piece; # 購入者 has customer => ( is => 'ro', isa => 'Customer', required => 1, trigger => sub { my $customer = $_[1]; croak "有効な購入者ではありません。" if $customer->is_disabled; } ); # 購入するWebコミック has comic => ( is => 'ro', isa => 'Comic', required => 1, trigger => sub { my $comic = $_[1]; croak "現在取扱できないコミックです。" if $comic->is_disabled; } ); # 購入日時 has payment_date_time => ( is => 'ro', isa => 'Time::Piece', required => 0, default => sub { my $self = $_[0]; croak "所持ポイントが不足しています。" if $self->customer->is_short_of_point( $self->comic ); return localtime(); } ); __PACKAGE__->meta->make_immutable(); } package main; my $purchase_pointp_payment = PurchasePointPayment->new( customer => Customer->new( name => 'sironekotoro', id => 1 ), comic => Comic->new( id => 10 ), );

14.2 ユニットテストリファクタリングのミスを防ぐ

  • 悪魔を呼び寄せるような邪悪なコードには、テストコードが書かれていないことが多いです

ということで、リファクタリングをする前のコード書いていきます。

ここクリックして展開

#!/usr/bin/env perl
use strict;
use warnings;
use v5.36;
use feature qw/say signatures/;

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

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

# 配送管理クラス
package DeliveryManager {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has products => (
        is       => 'ro',
        isa      => 'ArrayRef',
        required => 1,

        # trigger  => sub { croak '', unless ( $_[0] ) },
    );

    sub delivery_charge ($self) {
        my $charge      = 0;
        my $total_price = 0;

        for my $product ( @{ $self->products } ) {
            $total_price += $product->price;
        }

        if ( $total_price < 2000 ) {
            $charge = 500;
        }
        else {
            $charge = 0;
        }
        return $charge;

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

package main;

my $deliverely_manager = DeliveryManager->new(
    products => [ Product->new( price => 100 ), Product->new( price => 200 ), ]
);

say $deliverely_manager->delivery_charge();

まず、Manager って名前が悪いよね。何でもメソッドを放り込まれる悪魔の名前だったような。

それはさておき、(この本の)リファクタリングの仕方を追っていきます。

まず、あるべき形を決めて、そこに旧来の DeliveryManager クラスから移植していきます。

  • 購入する商品一覧である、買い物かごクラス
# 買い物かご
package ShoppingCart {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has products => (
        is       => 'ro',
        isa      => 'ArrayRef[Product]',
        required => 0,
        default  => sub { [] },
    );

    sub add ( $self, $product ) {
        my @adding = @{ $self->products };
        push @adding, $product;
        return __PACKAGE__->new( products => \@adding );
    }

    __PACKAGE__->meta->make_immutable();
}
  • 商品クラス
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 DeliveryCharge {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has amount => (
        is       => 'ro',
        isa      => 'Int',
        required => 1,
        default  => -1
    );
    __PACKAGE__->meta->make_immutable();
}

このあとにテストを書きます。

#!/usr/bin/env perl
use strict;
use warnings;

use Test::Simple tests => 2;
use lib qw(. ../);
use Product;
use ShoppingCart;
use DeliveryCharge;

{
    # 商品の合計額が2000円未満の場合、配送料は500円
    my $empty_cart = ShoppingCart->new();

    my $one_product_added =
      $empty_cart->add( Product->new( id => 1, name => '商品A', price => 500 ) );
    my $two_product_added =
      $one_product_added->add( Product->new( id => 2, name => '商品B', price => 1499 ) );

    my $charge = DeliveryCharge->new( shopping_cart => $two_product_added );

    ok( $charge->amount == 500, "商品の合計金額が2000円未満の場合、配送料は500円" );
}

{
    # 商品の合計額が2000円以上の場合、配送料は無料
    my $empty_cart = ShoppingCart->new();

    my $one_product_added =
      $empty_cart->add( Product->new( id => 1, name => '商品A', price => 500 ) );
    my $two_product_added =
      $one_product_added->add( Product->new( id => 2, name => '商品B', price => 1500 ) );

    my $charge = DeliveryCharge->new( shopping_cart => $two_product_added );

    ok( $charge->amount == 0, "商品の合計金額が2000円以上の場合、配送料は0円" );
}

この時点でのファイル構成はこんな感じ。

Perl では t というディレクトリの中に拡張子 t でテストファイルを作ります。

$ tree
.
├── DeliveryCharge.pm
├── Product.pm
├── ShoppingCart.pm
├── list_14_11.pl
└── t
    └── deliver_charge_test.t

で、t ディレクトリと同じ階層(t ディレクトリの中ではない)で prove コマンドを打つとテストを実行してくれます。

もちろん、t ディレクトリの中で perl deliver_charge_test.t でもok

$ prove
t/deliver_charge_test.t .. 1/2
#   Failed test '商品の合計金額が2000円未満の場合、配送料は500円'
#   at t/deliver_charge_test.t line 22.

#   Failed test '商品の合計金額が2000円以上の場合、配送料は0円'
#   at t/deliver_charge_test.t line 36.
# Looks like you failed 2 tests of 2.
t/deliver_charge_test.t .. Dubious, test returned 2 (wstat 512, 0x200)
Failed 2/2 subtests

Test Summary Report
-------------------
t/deliver_charge_test.t (Wstat: 512 (exited 2) Tests: 2 Failed: 2)
  Failed tests:  1-2
  Non-zero exit status: 2
Files=1, Tests=2,  0 wallclock secs ( 0.03 usr  0.01 sys +  0.06 cusr  0.01 csys =  0.11 CPU)
Result: FAIL

はい、エラーが出ました。

今回は、というかテストファーストで進める場合は「あるべき構造」のガワを作り「あるべき応答」でテストをしたので、中身を作っていない以上エラーが出て当然です。

ここからリファクタリングをしていきます。

リスト14.13

まずはエラーを出さないように、最低限の実装です。

ここクリックして展開

package DeliveryCharge {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use List::Util;

    has shopping_cart => (
        is       => 'ro',
        isa      => 'ShoppingCart',
        required => 1,
    );

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

    sub _build_amount {
        my $self = shift;

        my $amount      = 0;
        my $total_price =
          $self->shopping_cart->products->[0]->price +
          $self->shopping_cart->products->[1]->price;

        if ( $total_price < 2000 ) {
            $amount = 500;
        }
        else {
            $amount = 0;
        }

        return $amount;
    }

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

リスト14.14

このままだと、テストは通るけど、商品が2つまでしか入らないショッピングカートになってしまうので、ちゃんと書きます。

ここクリックして展開

package DeliveryCharge {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use List::Util;

    has shopping_cart => (
        is       => 'ro',
        isa      => 'ShoppingCart',
        required => 1,
    );

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

    sub _build_amount {
        my $self = shift;

        my $amount      = 0;
        my $total_price = 0;

        for my $product ( @{ $self->shopping_cart->products } ) {
            $total_price += $product->price;
        }

        if ( $total_price < 2000 ) {
            $amount = 500;
        }
        else {
            $amount = 0;
        }

        return $amount;
    }

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

今は配送料を扱う DeliveryCharge クラス内で、商品の合計を出して配送料を決めている。

本来、商品の合計を出すのは ShoppingCart クラスのはず。

ということで、リファクタリングしたのがこちらです!

ここクリックして展開

package ShoppingCart {
    use v5.36;
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;

    has products => (
        is       => 'ro',
        isa      => 'ArrayRef[Product]',
        required => 0,
        default  => sub { [] },
    );

    sub add ( $self, $product ) {
        my @adding = @{ $self->products };
        push @adding, $product;
        return __PACKAGE__->new( products => \@adding );
    }

    sub total_price {
        my $self   = shift;
        my $amount = 0;

        for my $product ( @{ $self->products } ) {
            $amount += $product->price;
        }

        return $amount;
    }

    __PACKAGE__->meta->make_immutable();
}

1;
package DeliveryCharge {
    use Carp qw/croak/;
    use Mouse;
    use namespace::autoclean;
    use List::Util;
    use lib qw/./;
    use ShoppingCart;

    use constant {
        CHARGE_FREE_THRESHOLD => 2000,
        PAY_CHARGE            => 500,
        CHARGE_FREE           => 0,
    };

    has shopping_cart => (
        is       => 'ro',
        isa      => 'ShoppingCart',
        required => 1,
    );

    has amount => (
        is       => 'ro',
        isa      => 'Int',
        required => 0,
        builder  => "_builder_amount",
    );

    sub _builder_amount {
        my $self = shift;
        my $amount =
          $self->shopping_cart->total_price() < CHARGE_FREE_THRESHOLD
          ? PAY_CHARGE
          : CHARGE_FREE;
    }

    __PACKAGE__->meta->make_immutable();

}
1;

いやー、正直ここの部分(リスト14.14〜リスト14.19)は本に買いてあるコードを追っても全然わからず混乱。

結局、最後のリファクタリングが終わった後のコードを見て、何をするべきなのかを理解したという感じです。

このあとはリファクタリングする時に気をつける点として「機能追加とリファクタリングを同時に行わない」とか、あやふやな仕様を理解するための分析手法として「仕様化テスト」「思考リファクタリング」といった手法が紹介されています。

「良いコード/悪いコードで学ぶ設計入門」第11章 コメント 〜 12章 メソッド

久々に

経理の仕事において、四半期決算ってのは本当に大変なお仕事で、ほぼ1ヶ月持っていかれます。

翌月はそのリカバリでボーッとしているという感じでした。

(これはどうにかして負荷を軽減したいところ・・・)

その余波で「良いコード/悪いコードで学ぶ設計入門」を読み進めるのもすっかり止まってしまい、また書き方もだいぶ忘れてしまった・・・んですが、こういう時にちゃんと学習記録を書き残してあると安心ですね。

11章はコメントってことで、コード少なめ文章多めという感じです。

コードも Perl で書き直すほどのものではないという感じなので、サクサク読んでいきます。

11章 コメント

11.1 退化コメント

  • 情報が古くなり、実装を正しく説明しなくなったコメントを退化コメントという
  • コメントは(コードの)劣化コピーにすぎないので、意図が通じるクラス名の命名などが必要
  • ロジックの挙動をなぞるだけのコメントは退化しやすい

11.2 コメントで命名をごまかす

  • すごい文字数のメソッドが出てくる
    • 書き写したくない
  • メソッドの可読性を上げるとで、説明のコメントが不要になる

11.3 意図や仕様変更時の注意点を読み手に伝えること

  • 意図や指標変更時の注意点をコメントしよう

11.4 コメントルールのまとめ

11.5 ドキュメントコメント

  • ドキュメントコメント?って思ったけど、Perl だったら POD 、JavaScript だったら JSDoc みたいなものを言うらしい。知らんかった

12章 メソッド

12.1 必ず自身のクラスのインスタンス変数を使うこと

  • 例外もあるが、原則は「自身のクラスのインスタンス変数を使う」

  • 完全コンストラクタパターンを用いて、コンストラクタにガード節を用意する

  • 他のクラスのインスタンス変数を変更するメソッドを作らない
    • 変更したいんんスタンス変数を持つクラスにメソッドを実装する

12.2 不変をベースに予期せぬ動作を防ぐ関数にすること

  • オブジェクトのプロパティ書き換えるときも新しいプロパティ作ってそれを返す、みたいな感じかな

12.3 尋ねるな、命じろ

  • あるクラスがよそのクラスの状態を判断したり、状態に応じてよその値を変更したりするのは低凝集構造
  • getter / setter が多用されているコードはその状況になりやすい
  • メソッドの呼び出し側で複雑な処理をするのはでなく、呼び出される側で制御をするよう設計する

12.4 コマンド・クエリ分離

  • 初めて聞く
  • メソッドはコマンド(変更)、またはクエリ(問い合わせ)のどちらか一方だけを行うよう設計する
  • コマンドとクエリを同時に行うメソッド種別をモディファイアという
    • 知らんかった
    • モディファイアはなるべく避ける
  • そろそろコード書きたくなってきたので書いてみる
#!/usr/bin/env perl
use strict;
use warnings;
use feature qw/say/;

use Function::Parameters;

fun gain_and_get_point($point){
    my $point += 10;
    return $point;
}

# モディファイア
my $origina_point = 0;
my $modifier = gain_and_get_point($origina_point);
say $modifier;  # 10

# コマンド・クエリ分離
fun gain_point($point){
    $point += 10
}

fun gat_point($point){
    return $point;
}

my $gain_pint = gain_point($origina_point);
say gat_point($gain_pint);  # 10

12.5 引数

  • 引数は不変にすること
  • フラグ引数は使わない
    • 切り替え機構はストラテジパターンを利用する
  • null を渡さない
    • 例)未装備状態を null ではなく、 Eqipment.EMPTY で表現する
  • 出力引数を使わない
  • 引数は限りなく少なくする

12.6 戻り値

  • 型を使って戻り値の意図を表明すること
    • プリミティブ型を使わず、独自の型を使って戻り値の意図を明確に表明する
  • 引数に null を渡さないように、null を返さない
  • エラーは戻り値で返さず、例外にする

と、12章はここまで。次の13章も文章が多い感。

macOS Monterey で IO::Socket::SSL, Net::SSLeay のインストールに失敗する

何度目かの環境再構築中

色々あって、業務で利用している Macbook Pro 2017 をリカバリすることにしました。

ディスクユーティリティで SSD を一旦まっさらに。

そこに macOS Monterey をネットワーク経由でクリーンインストール

ちょうど経理の月初作業も終わり、お盆休みもあったので落ち着いて環境構築を進めることができました。

自作モジュールのインストールでエラー

この自作モジュールは業務を楽にするために自分で作ったもので、以下のような動きをします。

  1. Google Sheet を csv にしてダウンロード
  2. ダウンロードした csv から数値を抜き出す
  3. 抜き出した数字に応じて画像生成

この "Google Sheet を csv にしてダウンロード" の時に(透過的に)利用するモジュールが IO::Socket::SSL です。 文字通り、SSL通信に関係するモジュールです。

これのインストールに失敗しました。

$ cpanm IO::Socket::SSL
--> Working on IO::Socket::SSL
Fetching http://www.cpan.org/authors/id/S/SU/SULLR/IO-Socket-SSL-2.074.tar.gz ... OK
==> Found dependencies: Net::SSLeay
--> Working on Net::SSLeay
Fetching http://www.cpan.org/authors/id/C/CH/CHRISN/Net-SSLeay-1.92.tar.gz ... OK
Configuring Net-SSLeay-1.92 ... N/A
! Configure failed for Net-SSLeay-1.92. See /Users/sironekotoro/.cpanm/work/1660879857.14507/build.log for details.
! Installing the dependencies failed: Module 'Net::SSLeay' is not installed
! Bailing out the installation for IO-Socket-SSL-2.074.

IO::Socket::SSL が依存している Net::SSLeay のインストールに失敗してるようです。 ログはここに出力されているので確認。

! Configure failed for Net-SSLeay-1.92. See /Users/sironekotoro/.cpanm/work/1660879857.14507/build.log for details.

失敗時のログはこんな感じ。

cpanm (App::cpanminus) 1.7046 on perl 5.036000 built for darwin-2level
Work directory is /Users/sironekotoro/.cpanm/work/1660879879.14561
You have make /usr/bin/make
You have /usr/local/bin/wget
You have /usr/bin/tar: bsdtar 3.5.1 - libarchive 3.5.1 zlib/1.2.11 liblzma/5.0.5 bz2lib/1.0.8 
You have /usr/bin/unzip
Searching Net::SSLeay () on cpanmetadb ...
--> Working on Net::SSLeay
Fetching http://www.cpan.org/authors/id/C/CH/CHRISN/Net-SSLeay-1.92.tar.gz
-> OK
Unpacking Net-SSLeay-1.92.tar.gz
Entering Net-SSLeay-1.92
Checking configure dependencies from META.json
Checking if you have ExtUtils::MakeMaker 6.58 ... Yes (7.64)
Checking if you have English 0 ... Yes (1.11)
Checking if you have constant 0 ... Yes (1.33)
Checking if you have File::Spec::Functions 0 ... Yes (3.84)
Checking if you have Text::Wrap 0 ... Yes (2021.0814)
Configuring Net-SSLeay-1.92
Running Makefile.PL
Do you want to run external tests?
These tests *will* *fail* if you do not have network connectivity. [n] n
*** Be sure to use the same compiler and options to compile your OpenSSL, perl,
    and Net::SSLeay. Mixing and matching compilers is not supported.

******************************************************************************
* COULD NOT FIND LIBSSL HEADERS                                              *
*                                                                            *
* The libssl header files are required to build Net-SSLeay, but they are     *
* missing from /usr. They would typically reside in /usr/include/openssl.    *
******************************************************************************
-> N/A
-> FAIL Configure failed for Net-SSLeay-1.92. See /Users/sironekotoro/.cpanm/work/1660879879.14561/build.log for details.

まぁ、原因はわかっていて。

macOS Monterey が採用している SSL のライブラリは LibraSSL というものです。

$ openssl version
LibreSSL 2.8.3

対して、IO::Socket::SSL が要求しているのは OpenSSL というライブラリです。

エラーログのここですね。

******************************************************************************
* COULD NOT FIND LIBSSL HEADERS                                              *
*                                                                            *
* The libssl header files are required to build Net-SSLeay, but they are     *
* missing from /usr. They would typically reside in /usr/include/openssl.    *

ということで、macでお馴染みのパッケージマネージャ Homebrew で openSSL をインストールします。

$ brew install openssl

openSSL のインストール後、無事 IO::Socket::SSL のインストールが完了。

めでたしめでたし。

なぜこれを書いたのか

何度目かのインストールで毎回やってるなぁ・・・と思ったのと、割と MacPerl やってて引っかかりやすいところかなぁ、と思ったので。

autossh を macOS 起動時に実行する

macOS 起動時に autossh を実行したい

autossh という ssh のラッパーソフトがあり、sshが切れたら自動的に繋ぎ直してくれるという便利なやつです。

autossh は homebrew で入れられます。

これを ターミナル起動時 ではなく、macOS の起動時に自動実行させたいという要望です。

まずは普通にターミナルから実行できるか試す

うちの場合はこんな感じでした。

$ ssh -L 127.0.0.1:${LOCAL_PORT}:${SERVER_IP}:${SERVICE_PORT} -i ~/ssh/id_rsa -l ${LOGIN_NAME} -p ${SERVER_PORT} ${SERVER_ID}

ローカルポートにアクセスすると、サーバーの特定のサービスポートにつながる、いわゆるトンネルを掘るというやつです。

これを autossh に書き直すとこんな感じ。

${LOCAL_PORT} とかは 実際には 22 などのポート番号、${LOGIN_NAME} には sironekotoro とかの文字列が入っております。

$ autossh -M 0 -f -N -L 127.0.0.1:${LOCAL_PORT}:${SERVER_IP}:${SERVICE_PORT} -i ~/ssh/id_rsa -l ${USERNAME} -p ${SERVER_PORT} ${SERVER_ID}

これをターミナルから実行して、ちゃんとトンネルが掘れたことを確認。

これを launchd の作法で書くだけで大丈夫なはず!

launchd で動かせない

大丈夫じゃなかった。ダメでした。

もう、何がダメかわからないレベル。

これが動かなかった plist です。供養。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>local.mxcl.autossh</string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/bin:/usr/bin:/usr/local/bin:/usr/sbin:/sbin</string>
    </dict>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/autossh</string>
        <string>-M</string>
        <string>0</string>
        <string>-f</string>
        <string>-N</string>
        <string>-p</string>
        <string>${SERVER_PORT}</string>
        <string>-l</string>
        <string>${LOGIN_NAME}</string>
        <string>-i</string>
        <string>/Users/${USERNAME}/.ssh/id_rsa</string>
        <string>-L</string>
        <string>127.0.0.1:${LOCAL_PORT}:${SERVER_IP}:${SERVICE_PORT}</string>
        <string>${SERVER_IP}</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/Users/${USERNAME}/Desktop/standard_out.txt</string>
    <key>StandardErrorPath</key>
    <string>/Users/${USERNAME}/Desktop/standard_error.txt</string>
  </dict>
</plist>

エラーを吐き出すようにしてるけど、standard_error.txt には、autossh を引数なし(あるいは不正な引数)時の usage が出力されてるのみ。

で、最小限の引数でってことでバージョンを表示する -V オプションだけを入れると、ちゃんと standard_out.txt にバージョン番号が追記される。

ぐぐぐぐ。

    <array>
        <string>-V</string> 
    </array>

なお、launchd ではシェル変数とかホームディレクトリを表す ~ とかが展開されないってことに気づくのにえらい時間がかかった。

基本に立ち返る

いやいや、俺は致命的な勘違いをしているのかも。

よくやるじゃん致命的な勘違い。

ってことで、ファイルを作るだけの簡単なやつで試してみる。

$ cd ~/Library/LaunchAgents
$ touch local.test.touch.plist

作った local.test.touch.plist は以下の内容で編集する

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>local.test.touch</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/touch</string>
        <string>/Users/sironekotoro/Desktop/test.txt</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/Users/sironekotoro/Desktop/standard_out.txt</string>
    <key>StandardErrorPath</key>
    <string>/Users/sironekotoro/Desktop/standard_error.txt</string>
  </dict>
</plist>

作ったら、文法に間違いないか確認する

$ plutil -lint local.test.touch.plist
local.test.touch.plist: OK

ではいざ実行!

$ launchctl load local.test.touch.plist

これでうまく動いて、デスクトップ上に text.txt という空のファイルができた。

うん、間違ってない・・・となると何故 autossh は動かないのか。

とりあえず、後片付け。

$ launchctl unload local.test.touch.plist
$ rm local.test.touch.plist

ワークアラウンド

いいかげん、あれこれ試すのも飽きてきたので、諦めてワークアラウンド(回避策)さがす。

Autometer でコマンド実行するやつを作って、それを 環境設定 → ユーザとグループ のログイン項目に入れる

Autometer 単体ではちゃんと動いた。

でも、macOS 起動時になぜか、なぜか、Xcode が立ち上がる。なんで???

autossh のコマンド入れたシェルクスリプト作って、それを launchd から呼ぶ

これはうまくいきました。

autossh.sh というファイルを以下の内容で作って

#!/bin/bash
/usr/local/bin/autossh -M 0 -f -N -L 127.0.0.1:${LOCAL_PORT}:${SERVER_IP}:${SERVICE_PORT} -i ~/ssh/id_rsa -l ${LOGIN_NAME} -p ${SERVER_PORT} ${SERVER_ID}

実行権限つけておきます。

$ chmod +x autossh.sh

このファイルを起動する plist を作ります。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>local.mxcl.autossh</string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/bin:/usr/bin:/usr/local/bin:/usr/sbin:/sbin</string>
    </dict>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/${USERNAME}/autossh.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/Users/sironekotoro/Desktop/standard_out.txt</string>
    <key>StandardErrorPath</key>
    <string>/Users/sironekotoro/Desktop/standard_error.txt</string>
  </dict>
</plist>

あとはさっきの touch の時のように起動時のリストに登録・・・これで動きました。

本当は、シェルスクリプト通さずに plist の中身だけで autossh を実行したかったんですが、今はこれが精一杯ってことで。

お疲れ様でした。

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

第9章から文字比率が高くなり、そのまま書き出すと文章を書写するだけになってしまう・・・ということで、小見出し&ときどき感想って形式で。

目的駆動名前設計

名前から目的や意図が読み取れることを特徴とします。

10.1 悪魔を呼び寄せる名前

例として挙げられているのは「商品」。

「予約」「注文」「出品」「発送」それらのロジックが入り込みやすい名称になってしまう。

これらを関心事ごとに分離し、隔離する。

関心が密結合になっているものを、疎結合高凝集とし、分離し、関心事に相応しい名称に変更する。

・・・ここは例示がめっちゃ鮮やかで、うちに出来るんかな?

となったところです。

まぁ、最初っから正解に辿り着けるものでもないので、意識しながら、良いコードに近づいていきたいですね。

関心の分離には、ビジネス目的を名前として表現することがポイントになります。

この辺りをヒントにやっていきましょう。

10.2 名前を設計する - 目的駆動名前設計

ここは筆者さんが先に重要ポイントをまとめてくれているので、引用します

  • 可能な限り具体的で、意味範囲が狭い、特化した名前を選ぶ
  • 存在ベースではなく、目的ベースで名前を考える
  • どんな関心ごとがあるか分析する
  • 声に出して話してみる
  • 利用規約を読んでみる
  • 違う名前に置き換えられないか検討する
  • 疎結合高凝集になっているか点検する

10.3 設計時の注意すべきリスク

  • 名前無頓着になるな
  • 仕様変更時の「意味範囲の変化」に警戒
  • 会話には登場するのにコード上に登場しない名前に注意
    • 会話に登場する重要な概念が、ソースコード上で名前もつけられず、雑多なロジックの中に埋没していることが本当に頻繁に見受けられます。

  • 形容詞で区別が必要な時はクラス化のチャンス

10.4 意図がわからない名前

  • tmp とか foo とか baz とかを業務コードで使わない
  • 技術駆動命名
    • int とか str おか memory とか flag とか value01 とか
    • ビジネス目的を指し示す命名に直す
  • ロジック構造をなぞった名前
    • isMemberHpMoreThenZeroAndIsMemberCanActAndIsMemberMpMoreThanMagicCostMp

    • 邪悪すぎるメソッド名
      • 業務のコードでこんなの見たら叫ぶと思う
    • クラス内で、早期リターンのif文 * 3 で書き直されてた
  • 驚き最小の原則
    • メソッド名から類推できないコードを書かない
    • ロジックの意図と名前を一致させる

10.5 構造を大きく歪ませてしまう名前

  • データクラスに陥る名前

    • データだけではなく、ちゃんとメソッドも同じクラスに設定しよう
    • ただし、DTO(Data Transfer Object)のような、データ転送用途に使われる(データしか入っていないオブジェクトを用いる)設計パターンもある
  • Manager, Processr、Controller などの、意味が広すぎる名前は用いない

  • もっと具体的な、意味の狭い概念を見つけていく

  • 連番連名

10.6 名前的に居場所が不自然なメソッド

  • 「動詞 + 目的語」のメソッド名に注意

    • consumeMagicpoint
    • addiemToParty
    • 関心に無関係なメソッドは「動詞+目的語」の形になる傾向がある
  • 可能な限り動詞1語で済む名前にする

  • 不適切な居場所の boolean メソッド

    • boolean 型を返すメソッドは 「Class名 is メソッド名」とした時に自然かどうかを考える
    • This is member ins hungry

  • 名前の省略

    • しない

10.7 名前の省略

(2022/08/27 追記)

  • 意図がわからなくなる省略はしない

  • 基本的に名前は省略しない

  • 省略をどう判断するか

    • 意味が失われていないか?
    • 問題が生じていないか?
    • プログラム言語ごとの習慣に合致しているか?
    • 命名方法がチームで一致しているか?

「良いコード/悪いコードで学ぶ設計入門」第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
    • ストラテジパターン
    • ファーストクラスコレクション
      • クラスの集合をクラスで表す