Featured image of post EU市場追加でOCPを確認 - 既存コードを変更せずに拡張

EU市場追加でOCPを確認 - 既存コードを変更せずに拡張

Perlで作る注文フローの国別キット第7回。EU市場を追加し、既存のOrderProcessorやFactoryを変更せずに拡張できることを確認します

第7回では、EU市場を追加して拡張性を確認します。既存のコードを変更せずに新市場に対応できるでしょうか。

前回の振り返り

前回はOrderProcessorクラスを導入し、DIによる柔軟な設計を実現しました。

  • OrderProcessorがFactoryを保持
  • 依存性注入でFactoryを外部から受け取る
  • 依存性逆転の原則(DIP)に従った設計

今回はEU市場を追加し、Open-Closed Principle(OCP)を体験します。

この記事で学ぶこと

  • EU市場向けの製品クラスとFactoryを追加する
  • 既存コードを変更せずに拡張できることを確認する
  • Open-Closed Principle(OCP)を理解する

Open-Closed Principleとは

Open-Closed Principle(開放閉鎖の原則)はSOLID原則の1つです。

  • 拡張に対して開いている(Open): 新しい機能を追加できる
  • 修正に対して閉じている(Closed): 既存のコードを変更しない

新市場を追加する際に既存のOrderProcessorやFactoryを変更しなければ、OCPを満たしていると言えます。

EU市場向け製品クラス

EU市場向けの3つの製品クラスを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package EUPayment;
use v5.36;
use Moo;

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

sub process ($self) {
    my $fee = int($self->amount * 0.04);  # EU手数料 4%
    my $total = $self->amount + $fee;
    say "【EU決済】Montant: €" . $self->amount . " + Frais: €$fee = Total: €$total";
    return $total;
}

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package EUShipping;
use v5.36;
use Moo;

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

sub ship ($self) {
    say "【EU配送】Livraison à: " . $self->address;
    say "  Transporteur: DHL Express";
    say "  Délai estimé: 3-5 jours ouvrés";
    return { carrier => 'dhl', days => 5 };
}

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package EUNotification;
use v5.36;
use Moo;

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

sub notify ($self, $order_id) {
    say "【EU通知】Confirmation de commande $order_id envoyée";
    say "  Destinataire: " . $self->email;
    say "  Langue: Français";
    return 1;
}

1;

EU市場向けの特徴は以下の通りです。

  • 手数料率: 4%(国内3%と海外5%の中間)
  • 通貨: ユーロ(€)
  • 配送業者: DHL Express
  • 通知言語: フランス語

EU市場向けFactory

EU市場向けのFactoryを追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package EUOrderFlowFactory;
use v5.36;
use Moo;

with 'OrderFlowFactory';

sub create_payment ($self, %args) {
    return EUPayment->new(%args);
}

sub create_shipping ($self, %args) {
    return EUShipping->new(%args);
}

sub create_notification ($self, %args) {
    return EUNotification->new(%args);
}

1;

既存コードの変更確認

重要なのは、既存のコードに何も変更を加えていないことです。

  • OrderProcessor: 変更なし
  • OrderFlowFactory(ロール): 変更なし
  • DomesticOrderFlowFactory: 変更なし
  • GlobalOrderFlowFactory: 変更なし
  • 各製品クラス: 変更なし

追加したのは新しいファイルのみです。

完成コード

3市場に対応した完成版のコードです。

  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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#!/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;
}

# --- 製品クラス(EU) ---
package EUPayment;
use v5.36;
use Moo;

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

sub process ($self) {
    my $fee = int($self->amount * 0.04);
    my $total = $self->amount + $fee;
    say "【EU決済】Montant: €" . $self->amount . " + Frais: €$fee = Total: €$total";
    return $total;
}

package EUShipping;
use v5.36;
use Moo;

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

sub ship ($self) {
    say "【EU配送】Livraison à: " . $self->address;
    say "  Transporteur: DHL Express";
    say "  Délai estimé: 3-5 jours ouvrés";
    return { carrier => 'dhl', days => 5 };
}

package EUNotification;
use v5.36;
use Moo;

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

sub notify ($self, $order_id) {
    say "【EU通知】Confirmation de commande $order_id envoyée";
    say "  Destinataire: " . $self->email;
    say "  Langue: Français";
    return 1;
}

# --- 抽象Factoryロール ---
package OrderFlowFactory;
use v5.36;
use Moo::Role;

requires 'create_payment';
requires 'create_shipping';
requires 'create_notification';

# --- 国内向けFactory ---
package DomesticOrderFlowFactory;
use v5.36;
use Moo;

with 'OrderFlowFactory';

sub create_payment ($self, %args) { DomesticPayment->new(%args) }
sub create_shipping ($self, %args) { DomesticShipping->new(%args) }
sub create_notification ($self, %args) { DomesticNotification->new(%args) }

# --- 海外向けFactory ---
package GlobalOrderFlowFactory;
use v5.36;
use Moo;

with 'OrderFlowFactory';

sub create_payment ($self, %args) { GlobalPayment->new(%args) }
sub create_shipping ($self, %args) { GlobalShipping->new(%args) }
sub create_notification ($self, %args) { GlobalNotification->new(%args) }

# --- EU向けFactory(新規追加) ---
package EUOrderFlowFactory;
use v5.36;
use Moo;

with 'OrderFlowFactory';

sub create_payment ($self, %args) { EUPayment->new(%args) }
sub create_shipping ($self, %args) { EUShipping->new(%args) }
sub create_notification ($self, %args) { EUNotification->new(%args) }

# --- OrderProcessor(変更なし) ---
package OrderProcessor;
use v5.36;
use Moo;

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

sub process ($self, $order_id, $amount, $address, $email) {
    say "=" x 50;
    say "注文処理開始: $order_id";
    say "=" x 50;

    my $payment = $self->factory->create_payment(amount => $amount);
    my $shipping = $self->factory->create_shipping(address => $address);
    my $notification = $self->factory->create_notification(email => $email);

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

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

    return { total => $total, delivery_info => $delivery_info };
}

# --- メイン処理 ---
package main;
use v5.36;

# 3市場の一覧
my %factories = (
    domestic => DomesticOrderFlowFactory->new,
    global   => GlobalOrderFlowFactory->new,
    eu       => EUOrderFlowFactory->new,
);

# 各市場の注文を処理
for my $market (qw(domestic global eu)) {
    my $processor = OrderProcessor->new(factory => $factories{$market});
    $processor->process(
        "ORD-$market-001",
        1000,
        "Address for $market",
        "$market\@example.com"
    );
}

実行結果の一部です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
==================================================
注文処理開始: ORD-eu-001
==================================================
【EU決済】Montant: €1000 + Frais: €40 = Total: €1040

【EU配送】Livraison à: Address for eu
  Transporteur: DHL Express
  Délai estimé: 3-5 jours ouvrés

【EU通知】Confirmation de commande ORD-eu-001 envoyée
  Destinataire: eu@example.com
  Langue: Français

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

OCPを満たしている証拠

新市場追加時の変更箇所をまとめます。

コンポーネント変更
OrderProcessorなし
OrderFlowFactory(ロール)なし
DomesticOrderFlowFactoryなし
GlobalOrderFlowFactoryなし
既存製品クラス(6個)なし
EUOrderFlowFactory新規追加
EU製品クラス(3個)新規追加

既存のコードに一切変更を加えずに、新市場対応を追加できました。これがOCPの効果です。

クラス数の推移

クラス数内訳
第1回3国内製品3
第5回10製品6 + ロール1 + Factory2 + Processor1
第7回14製品9 + ロール1 + Factory3 + Processor1

クラス数は増えましたが、既存コードの変更なしに拡張できることがポイントです。

まとめ

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

  • EU市場向けの製品クラスとFactoryを追加した
  • 既存のOrderProcessor/Factory/製品クラスは一切変更しなかった
  • Open-Closed Principle(OCP)を満たす設計であることを確認した

次回は「返品フロー」という新しい製品種を追加した場合に何が起きるかを検証し、このパターンの限界と適用判断について考えます。

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