Featured image of post 組み合わせミスで起きる業務事故 - 製品ファミリの不一致

組み合わせミスで起きる業務事故 - 製品ファミリの不一致

Perlで作る注文フローの国別キット第3回。国内配送に海外決済が紛れ込む組み合わせミスで手数料計算が狂い、業務事故が発生する様子を体験します

第3回では、製品ファミリの不一致で起きる業務事故を体験します。国内配送に海外決済が紛れ込むと何が起きるのでしょうか。

組み合わせミスによる業務事故

前回の振り返り

前回は海外市場対応を追加し、if/elseによる市場判定を実装しました。

  • クラス数が3個から6個に増加
  • 市場判定のif/elseが増殖
  • 組み合わせミスが起きやすい構造

今回はその「組み合わせミス」が実際に発生するとどうなるか見ていきます。

この記事で学ぶこと

  • 製品ファミリの不一致とはどのような状態か
  • 組み合わせミスが業務にどのような影響を与えるか
  • 手動管理の限界を理解する

事故が起きるコード

以下のコードには、意図しないバグが含まれています。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#!/usr/bin/env perl
use v5.36;

# --- 国内向けクラス ---
package DomesticPayment;
use v5.36;
use Moo;

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

sub process ($self) {
    my $fee = int($self->amount * 0.03);
    my $total = $self->amount + $fee;
    say "【国内決済】金額: ¥" . $self->amount . " + 手数料: ¥$fee = 合計: ¥$total";
    return $total;
}

package DomesticShipping;
use v5.36;
use Moo;

has address => (is => 'ro', required => 1);

sub ship ($self) {
    say "【国内配送】お届け先: " . $self->address;
    say "  配送業者: ヤマト運輸";
    say "  配送日数: 1-2営業日";
    return { carrier => 'yamato', days => 2 };
}

package DomesticNotification;
use v5.36;
use Moo;

has email => (is => 'ro', required => 1);

sub notify ($self, $order_id) {
    say "【国内通知】$order_id の注文確認メールを送信";
    say "  宛先: " . $self->email;
    say "  言語: 日本語";
    return 1;
}

# --- 海外向けクラス ---
package GlobalPayment;
use v5.36;
use Moo;

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

sub process ($self) {
    my $fee = int($self->amount * 0.05);
    my $total = $self->amount + $fee;
    say "【海外決済】Amount: \$" . $self->amount . " + Fee: \$$fee = Total: \$$total";
    return $total;
}

package GlobalShipping;
use v5.36;
use Moo;

has address => (is => 'ro', required => 1);

sub ship ($self) {
    say "【海外配送】Delivery to: " . $self->address;
    say "  Carrier: FedEx International";
    say "  Estimated: 5-10 business days";
    return { carrier => 'fedex', days => 10 };
}

package GlobalNotification;
use v5.36;
use Moo;

has email => (is => 'ro', required => 1);

sub notify ($self, $order_id) {
    say "【海外通知】Order confirmation for $order_id sent";
    say "  To: " . $self->email;
    say "  Language: English";
    return 1;
}

# --- メイン処理(バグあり) ---
package main;
use v5.36;

sub process_order_buggy ($order_id, $market, $amount, $address, $email) {
    say "=" x 50;
    say "注文処理開始: $order_id (市場: $market)";
    say "=" x 50;

    my ($payment, $shipping, $notification);

    # ★バグ: 決済だけ海外、配送と通知は国内になっている
    if ($market eq 'domestic') {
        $payment = GlobalPayment->new(amount => $amount);  # ← 間違い!
        $shipping = DomesticShipping->new(address => $address);
        $notification = DomesticNotification->new(email => $email);
    }
    elsif ($market eq 'global') {
        $payment = GlobalPayment->new(amount => $amount);
        $shipping = GlobalShipping->new(address => $address);
        $notification = GlobalNotification->new(email => $email);
    }
    else {
        die "Unknown market: $market";
    }

    my $total = $payment->process;
    say "";
    my $delivery_info = $shipping->ship;
    say "";
    $notification->notify($order_id);

    say "";
    say "=" x 50;
    say "注文処理完了";
    say "=" x 50;
}

# 国内注文を処理(しかしバグで海外決済が適用される)
process_order_buggy('ORD-2026-0003', 'domestic', 5000, '大阪市北区4-5-6', 'yamada@example.com');

事故の結果

上記のコードを実行すると、以下のように出力されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
==================================================
注文処理開始: ORD-2026-0003 (市場: domestic)
==================================================
【海外決済】Amount: $5000 + Fee: $250 = Total: $5250

【国内配送】お届け先: 大阪市北区4-5-6
  配送業者: ヤマト運輸
  配送日数: 1-2営業日

【国内通知】ORD-2026-0003 の注文確認メールを送信
  宛先: yamada@example.com
  言語: 日本語

==================================================
注文処理完了
==================================================

何が問題でしょうか?

業務への影響

この事故では以下の問題が発生しています。

  1. 手数料が3%(¥150)ではなく5%($250)で計算された
  2. 円表記ではなくドル表記になっている
  3. 配送と通知は国内向けなのに、決済だけ海外設定

これは実際の業務では以下のような事態を引き起こします。

  • 顧客に誤った請求金額が表示される
  • 経理処理で通貨の不整合が発生する
  • 顧客からのクレームが発生する

なぜこの事故が起きるのか

この問題の根本原因は「製品ファミリの一貫性が保証されていない」ことです。

	flowchart LR
    subgraph 正しい組み合わせ
        DP[DomesticPayment] --> DS[DomesticShipping]
        DS --> DN[DomesticNotification]
    end

    subgraph 間違った組み合わせ
        GP[GlobalPayment] --> DS2[DomesticShipping]
        DS2 --> DN2[DomesticNotification]
    end

    style GP fill:#ffcccc
    style DS2 fill:#ccffcc
    style DN2 fill:#ccffcc

現在の設計では、決済・配送・通知を個別に生成しているため、どの組み合わせでもコンパイルが通ってしまいます。

手動管理の限界

if/elseで正しい組み合わせを維持するには、以下を常に確認する必要があります。

  • 決済クラスが市場に合っているか
  • 配送クラスが市場に合っているか
  • 通知クラスが市場に合っているか

市場が増えると、この確認作業が指数関数的に増加します。

市場数クラス数確認ポイント
266箇所
399箇所
51515箇所

人間の注意力には限界があり、必ずミスが発生します。

解決策の方向性

この問題を解決するには「市場ごとの製品セットをまとめて生成する仕組み」が必要です。

  • 国内市場 → 国内決済 + 国内配送 + 国内通知をセットで提供
  • 海外市場 → 海外決済 + 海外配送 + 海外通知をセットで提供

個別に生成するのではなく、セット(ファミリ)として一括生成することで組み合わせミスを防ぎます。

次回はこの考え方をコードに落とし込み、Abstract Factoryパターンを導入します。

まとめ

この記事では以下を学びました。

  • 製品ファミリの不一致とは、セットであるべきクラスが混在する状態
  • 組み合わせミスは手数料誤計算などの業務事故につながる
  • if/elseによる手動管理には限界がある
  • 解決には「セットとして生成する仕組み」が必要

次回は、国別Factoryを定義してこの問題を解決します。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。