ストーリー
ミッション概要
| ミッション | テーマ | 目安時間 |
|---|---|---|
| Mission 1 | コードレビュー:問題の発見と言語化 | 20分 |
| Mission 2 | 設計の改善:SOLID原則の適用 | 20分 |
| Mission 3 | パターンの適用:適切なパターンの選択 | 20分 |
| Mission 4 | 総合リファクタリング:完成形を実装 | 30分 |
対象コード:オンラインショップの注文システム
class OnlineShop {
private orders: any[] = [];
private inventory: Map<string, number> = new Map();
processOrder(
customerId: string,
customerEmail: string,
customerType: string,
items: { productId: string; name: string; price: number; quantity: number }[],
paymentMethod: string,
cardNumber: string,
shippingAddress: string,
couponCode: string | null
): string {
// 在庫チェック
for (const item of items) {
const stock = this.inventory.get(item.productId) ?? 0;
if (stock < item.quantity) {
return `ERROR: ${item.name} is out of stock`;
}
}
// 小計計算
let subtotal = 0;
for (const item of items) {
subtotal += item.price * item.quantity;
}
// 割引計算
let discount = 0;
if (couponCode === 'SAVE10') {
discount = subtotal * 0.1;
} else if (couponCode === 'SAVE20') {
discount = subtotal * 0.2;
} else if (couponCode === 'FLAT1000') {
discount = 1000;
}
// 顧客タイプ別割引
if (customerType === 'vip') {
discount += subtotal * 0.05;
} else if (customerType === 'premium') {
discount += subtotal * 0.03;
}
// 送料計算
let shippingFee = 0;
if (subtotal - discount < 5000) {
if (shippingAddress.includes('沖縄') || shippingAddress.includes('北海道')) {
shippingFee = 1500;
} else {
shippingFee = 800;
}
}
// 税金計算
const taxableAmount = subtotal - discount + shippingFee;
const tax = Math.round(taxableAmount * 0.1);
const total = taxableAmount + tax;
// 決済処理
if (paymentMethod === 'credit_card') {
if (cardNumber.length !== 16) {
return 'ERROR: Invalid card number';
}
console.log(`Charging credit card ****${cardNumber.slice(-4)}: ${total}`);
} else if (paymentMethod === 'bank_transfer') {
console.log(`Bank transfer request: ${total}`);
} else if (paymentMethod === 'cod') {
if (total > 300000) {
return 'ERROR: COD not available for orders over 300,000';
}
console.log(`COD order: ${total}`);
}
// 在庫更新
for (const item of items) {
const current = this.inventory.get(item.productId) ?? 0;
this.inventory.set(item.productId, current - item.quantity);
}
// 注文記録
const order = {
id: `ORD-${Date.now()}`,
customerId,
items,
subtotal,
discount,
shippingFee,
tax,
total,
status: 'confirmed',
createdAt: new Date(),
};
this.orders.push(order);
// メール送信
console.log(`Sending confirmation email to ${customerEmail}`);
console.log(`Subject: Order ${order.id} Confirmed`);
console.log(`Body: Thank you for your order! Total: ${total}`);
// ポイント付与
if (customerType !== 'guest') {
const points = Math.floor(total * 0.01);
console.log(`Adding ${points} points to customer ${customerId}`);
}
return `SUCCESS: Order ${order.id} created. Total: ${total}`;
}
}
Mission 1: コードレビュー — 問題の発見と言語化(20分)
要件
上記のコードを分析し、以下の観点で問題点を一覧にしてください。
- コードスメル — どのコードスメルが見られるか
- SOLID原則違反 — どの原則に違反しているか
- 品質上の問題 — テスト容易性、保守性、信頼性の問題
各問題について「何が悪いか」「なぜ悪いか」「どう直すべきか」を説明してください。
解答
コードスメル
| スメル | 箇所 | 説明 |
|---|---|---|
| 長すぎるメソッド | processOrder | 80行以上の巨大メソッド |
| パラメータが多すぎる | processOrder の引数 | 8個の引数、意味のまとまりがない |
| プリミティブ執着 | customerType, paymentMethod | string で表現、型安全でない |
| スイッチ文の乱用 | couponCode, customerType, paymentMethod | if-else の連鎖が3箇所 |
| 特性の横恋慕 | 割引計算、送料計算 | 本来別のオブジェクトが持つべきロジック |
| 神クラス | OnlineShop | 在庫、注文、決済、通知を全て担当 |
SOLID原則違反
| 原則 | 違反内容 |
|---|---|
| SRP | 在庫管理、価格計算、決済処理、メール送信、ポイント付与が1メソッドに |
| OCP | 新しいクーポン・決済方法の追加で既存メソッドの修正が必要 |
| DIP | console.log による直接依存、外部サービスのインターフェースなし |
品質上の問題
- テスト容易性: console.log による副作用、外部依存の注入不可
- 信頼性: エラー処理が文字列返却、例外なし
- 保守性: 変更理由が多すぎ、影響範囲が不明
Mission 2: 設計の改善 — SOLID原則の適用(20分)
要件
以下のインターフェースとクラス構造を設計してください(実装は不要、インターフェース定義のみ)。
- 責任を分離したクラス構造(SRP)
- 拡張可能なクーポン・決済の仕組み(OCP)
- 抽象に依存するサービス間の関係(DIP)
解答
// === 値オブジェクト ===
type CustomerType = 'guest' | 'regular' | 'premium' | 'vip';
type OrderStatus = 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled';
interface OrderItem {
productId: string;
name: string;
price: number;
quantity: number;
}
interface ShippingAddress {
fullAddress: string;
region: string;
}
// === ポート(インターフェース) ===
interface InventoryService {
checkAvailability(items: OrderItem[]): boolean;
reserveItems(items: OrderItem[]): void;
releaseItems(items: OrderItem[]): void;
}
interface PricingService {
calculateSubtotal(items: OrderItem[]): number;
calculateDiscount(subtotal: number, customerType: CustomerType, couponCode: string | null): number;
calculateShipping(subtotal: number, discount: number, address: ShippingAddress): number;
calculateTax(taxableAmount: number): number;
}
interface PaymentGateway {
charge(amount: number, paymentDetails: PaymentDetails): PaymentResult;
}
interface NotificationService {
sendOrderConfirmation(email: string, order: Order): void;
}
interface LoyaltyService {
addPoints(customerId: string, amount: number): void;
}
interface OrderRepository {
save(order: Order): void;
findById(id: string): Order | null;
}
// === クーポン(OCP) ===
interface CouponStrategy {
readonly code: string;
calculateDiscount(subtotal: number): number;
}
// === 決済(OCP) ===
interface PaymentMethod {
readonly name: string;
validate(details: PaymentDetails): boolean;
process(amount: number, details: PaymentDetails): PaymentResult;
}
// === ユースケース ===
interface PlaceOrderUseCase {
execute(request: PlaceOrderRequest): PlaceOrderResult;
}
Mission 3: パターンの適用 — 適切なパターンの選択(20分)
要件
Mission 2 の設計に基づいて、以下のパターンを具体的に適用してください。
- Strategy パターン — クーポン割引の種類切り替え
- Factory パターン — 決済方法の生成
- Observer パターン — 注文完了後の通知
解答
// === Strategy: クーポン割引 ===
class PercentageCoupon implements CouponStrategy {
constructor(readonly code: string, private rate: number) {}
calculateDiscount(subtotal: number): number {
return subtotal * this.rate;
}
}
class FixedAmountCoupon implements CouponStrategy {
constructor(readonly code: string, private amount: number) {}
calculateDiscount(subtotal: number): number {
return Math.min(this.amount, subtotal);
}
}
class CouponRegistry {
private coupons: Map<string, CouponStrategy> = new Map();
register(coupon: CouponStrategy): void {
this.coupons.set(coupon.code, coupon);
}
find(code: string): CouponStrategy | undefined {
return this.coupons.get(code);
}
}
// 登録
const couponRegistry = new CouponRegistry();
couponRegistry.register(new PercentageCoupon('SAVE10', 0.1));
couponRegistry.register(new PercentageCoupon('SAVE20', 0.2));
couponRegistry.register(new FixedAmountCoupon('FLAT1000', 1000));
// === Factory: 決済方法 ===
class CreditCardPayment implements PaymentMethod {
readonly name = 'credit_card';
validate(details: PaymentDetails): boolean {
return details.cardNumber !== undefined && details.cardNumber.length === 16;
}
process(amount: number, details: PaymentDetails): PaymentResult {
return { success: true, transactionId: `cc-${Date.now()}` };
}
}
class BankTransferPayment implements PaymentMethod {
readonly name = 'bank_transfer';
validate(details: PaymentDetails): boolean { return true; }
process(amount: number, details: PaymentDetails): PaymentResult {
return { success: true, transactionId: `bt-${Date.now()}` };
}
}
class CodPayment implements PaymentMethod {
readonly name = 'cod';
validate(details: PaymentDetails): boolean { return true; }
process(amount: number, details: PaymentDetails): PaymentResult {
if (amount > 300000) return { success: false, error: 'COD limit exceeded' };
return { success: true, transactionId: `cod-${Date.now()}` };
}
}
class PaymentMethodFactory {
private methods: Map<string, PaymentMethod> = new Map();
register(method: PaymentMethod): void {
this.methods.set(method.name, method);
}
create(name: string): PaymentMethod {
const method = this.methods.get(name);
if (!method) throw new Error(`Unknown payment method: ${name}`);
return method;
}
}
// === Observer: 注文完了通知 ===
interface OrderEventHandler {
onOrderPlaced(order: Order): void;
}
class EmailNotificationHandler implements OrderEventHandler {
onOrderPlaced(order: Order): void {
console.log(`Sending confirmation email for ${order.id}`);
}
}
class LoyaltyPointHandler implements OrderEventHandler {
onOrderPlaced(order: Order): void {
const points = Math.floor(order.total * 0.01);
console.log(`Adding ${points} points`);
}
}
class OrderEventDispatcher {
private handlers: OrderEventHandler[] = [];
subscribe(handler: OrderEventHandler): void {
this.handlers.push(handler);
}
dispatchOrderPlaced(order: Order): void {
for (const handler of this.handlers) {
handler.onOrderPlaced(order);
}
}
}
Mission 4: 総合リファクタリング — 完成形を実装(30分)
要件
Mission 1-3 の成果を統合し、PlaceOrderUseCase を完成させてください。元のコードと同じ振る舞いを、SOLID原則とデザインパターンに基づいた美しい設計で実現してください。
解答
interface PlaceOrderRequest {
customerId: string;
customerEmail: string;
customerType: CustomerType;
items: OrderItem[];
paymentMethodName: string;
paymentDetails: PaymentDetails;
shippingAddress: ShippingAddress;
couponCode: string | null;
}
interface PlaceOrderResult {
success: boolean;
orderId?: string;
total?: number;
error?: string;
}
class PlaceOrderService implements PlaceOrderUseCase {
constructor(
private inventoryService: InventoryService,
private pricingService: PricingService,
private paymentFactory: PaymentMethodFactory,
private orderRepository: OrderRepository,
private eventDispatcher: OrderEventDispatcher
) {}
execute(request: PlaceOrderRequest): PlaceOrderResult {
// 在庫チェック
if (!this.inventoryService.checkAvailability(request.items)) {
return { success: false, error: 'One or more items are out of stock' };
}
// 価格計算
const subtotal = this.pricingService.calculateSubtotal(request.items);
const discount = this.pricingService.calculateDiscount(
subtotal, request.customerType, request.couponCode
);
const shippingFee = this.pricingService.calculateShipping(
subtotal, discount, request.shippingAddress
);
const taxableAmount = subtotal - discount + shippingFee;
const tax = this.pricingService.calculateTax(taxableAmount);
const total = taxableAmount + tax;
// 決済処理
const paymentMethod = this.paymentFactory.create(request.paymentMethodName);
if (!paymentMethod.validate(request.paymentDetails)) {
return { success: false, error: 'Invalid payment details' };
}
const paymentResult = paymentMethod.process(total, request.paymentDetails);
if (!paymentResult.success) {
return { success: false, error: paymentResult.error };
}
// 在庫確保
this.inventoryService.reserveItems(request.items);
// 注文保存
const order: Order = {
id: `ORD-${Date.now()}`,
customerId: request.customerId,
customerEmail: request.customerEmail,
items: request.items,
subtotal,
discount,
shippingFee,
tax,
total,
status: 'confirmed',
createdAt: new Date(),
};
this.orderRepository.save(order);
// イベント発行(通知、ポイント付与など)
this.eventDispatcher.dispatchOrderPlaced(order);
return { success: true, orderId: order.id, total };
}
}
達成度チェック
| ミッション | テーマ | 完了 |
|---|---|---|
| Mission 1 | コードレビュー:問題の発見と言語化 | |
| Mission 2 | 設計の改善:SOLID原則の適用 | |
| Mission 3 | パターンの適用:適切なパターンの選択 | |
| Mission 4 | 総合リファクタリング:完成形を実装 |
まとめ
| ポイント | 内容 |
|---|---|
| コードレビュー | 問題を発見し、根拠を持って説明する |
| 設計改善 | SOLID原則に基づいてクラス構造を設計する |
| パターン適用 | 適切なパターンを選択して実装する |
| 総合力 | 知識を組み合わせて実践的な設計ができる |
チェックリスト
- コードスメルを発見し、名前で指摘できる
- SOLID原則違反を特定し、改善方法を提案できる
- 適切なデザインパターンを選択して適用できる
- レガシーコードを段階的にリファクタリングできる
次のステップへ
最後に卒業クイズです。Month 1 で学んだすべての知識を確認しましょう。
推定読了時間: 90分