演習の進め方

各演習は、小さなスケールから大きなスケールへと段階的に進んでいきます。まず問題のコードを分析し、責任分離の原則に従ってリファクタリングしてください。

演習のポイント

  • 責任の境界を明確に識別する
  • 各スケールに適した分離方法を選択する
  • テスタビリティの向上を意識する
  • 将来の変更に対する柔軟性を考慮する

Level 1: 関数レベルの演習

問題:データ処理関数のリファクタリング

以下の関数は複数の責任を持っています。責任ごとに分離してください。

function analyzeCustomerData(customers) {
    const results = {
        totalCustomers: 0,
        premiumCustomers: [],
        churnRisk: [],
        emailList: [],
        report: ''
    };
    
    // すべての処理が1つの関数に詰め込まれている
    for (const customer of customers) {
        // カウント
        results.totalCustomers++;
        
        // プレミアム顧客の判定と抽出
        if (customer.totalPurchases > 100000 && 
            customer.membershipYears > 2) {
            results.premiumCustomers.push({
                id: customer.id,
                name: customer.name,
                value: customer.totalPurchases
            });
        }
        
        // 離脱リスクの計算
        const daysSinceLastPurchase = 
            (Date.now() - new Date(customer.lastPurchaseDate)) / (1000 * 60 * 60 * 24);
        
        if (daysSinceLastPurchase > 180 && 
            customer.purchaseFrequency < 2) {
            results.churnRisk.push({
                id: customer.id,
                name: customer.name,
                risk: 'high',
                daysSinceLastPurchase
            });
        }
        
        // メールリストの作成(バリデーション含む)
        if (customer.email && 
            customer.email.includes('@') &&
            customer.emailOptIn) {
            results.emailList.push(customer.email.toLowerCase());
        }
    }
    
    // レポート生成
    results.report = `
Customer Analysis Report
========================
Total Customers: ${results.totalCustomers}
Premium Customers: ${results.premiumCustomers.length}
At Risk: ${results.churnRisk.length}
Email Subscribers: ${results.emailList.length}

Premium Customer List:
${results.premiumCustomers.map(c => 
    `- ${c.name}: ¥${c.value.toLocaleString()}`
).join('\n')}

High Risk Customers:
${results.churnRisk.map(c => 
    `- ${c.name}: ${c.daysSinceLastPurchase} days inactive`
).join('\n')}
    `;
    
    // ファイルに保存
    fs.writeFileSync('customer_report.txt', results.report);
    
    // メール送信
    if (results.churnRisk.length > 10) {
        emailService.send('manager@company.com', 
            'High Churn Risk Alert',
            `${results.churnRisk.length} customers at risk`
        );
    }
    
    return results;
}
解答例を見る

リファクタリング後

// 責任1: 顧客の分類
function classifyCustomers(customers) {
    return {
        all: customers,
        premium: customers.filter(isPremiumCustomer),
        atRisk: customers.filter(isAtRiskCustomer)
    };
}

function isPremiumCustomer(customer) {
    return customer.totalPurchases > 100000 && 
           customer.membershipYears > 2;
}

function isAtRiskCustomer(customer) {
    const daysSinceLastPurchase = calculateDaysSinceLastPurchase(customer);
    return daysSinceLastPurchase > 180 && 
           customer.purchaseFrequency < 2;
}

function calculateDaysSinceLastPurchase(customer) {
    const lastPurchase = new Date(customer.lastPurchaseDate);
    const now = Date.now();
    return Math.floor((now - lastPurchase) / (1000 * 60 * 60 * 24));
}

// 責任2: メールリストの作成
function createEmailList(customers) {
    return customers
        .filter(hasValidEmail)
        .filter(customer => customer.emailOptIn)
        .map(customer => customer.email.toLowerCase());
}

function hasValidEmail(customer) {
    return customer.email && customer.email.includes('@');
}

// 責任3: レポート生成
function generateReport(analysis) {
    const { all, premium, atRisk } = analysis.classified;
    const emailCount = analysis.emailList.length;
    
    return `
Customer Analysis Report
========================
Total Customers: ${all.length}
Premium Customers: ${premium.length}
At Risk: ${atRisk.length}
Email Subscribers: ${emailCount}

${generatePremiumSection(premium)}
${generateRiskSection(atRisk)}
    `.trim();
}

function generatePremiumSection(premiumCustomers) {
    if (premiumCustomers.length === 0) return '';
    
    const list = premiumCustomers
        .map(c => `- ${c.name}: ¥${c.totalPurchases.toLocaleString()}`)
        .join('\n');
    
    return `Premium Customer List:\n${list}`;
}

function generateRiskSection(atRiskCustomers) {
    if (atRiskCustomers.length === 0) return '';
    
    const list = atRiskCustomers
        .map(c => {
            const days = calculateDaysSinceLastPurchase(c);
            return `- ${c.name}: ${days} days inactive`;
        })
        .join('\n');
    
    return `High Risk Customers:\n${list}`;
}

// 責任4: 通知
function checkAndNotifyHighRisk(atRiskCustomers, notifier) {
    const threshold = 10;
    
    if (atRiskCustomers.length > threshold) {
        notifier.sendAlert({
            to: 'manager@company.com',
            subject: 'High Churn Risk Alert',
            message: `${atRiskCustomers.length} customers at risk`
        });
    }
}

// 責任5: 永続化
function saveReport(report, filename) {
    fs.writeFileSync(filename, report);
}

// 統合関数
function analyzeCustomers(customers, { notifier, filename }) {
    // 各責任を組み合わせる
    const classified = classifyCustomers(customers);
    const emailList = createEmailList(customers);
    const report = generateReport({ classified, emailList });
    
    // 副作用は最後に実行
    saveReport(report, filename);
    checkAndNotifyHighRisk(classified.atRisk, notifier);
    
    return {
        classified,
        emailList,
        report
    };
}

Level 2: クラス/コンポーネントレベルの演習

問題:ショッピングカートコンポーネントの再設計

以下のReactコンポーネントは多くの責任を抱えています。適切に分離してください。

function ShoppingCart() {
    const [items, setItems] = useState([]);
    const [user, setUser] = useState(null);
    const [coupon, setCoupon] = useState('');
    const [loading, setLoading] = useState(false);
    
    useEffect(() => {
        // ユーザー情報の取得
        fetch('/api/user')
            .then(res => res.json())
            .then(setUser);
            
        // カートアイテムの取得
        const savedCart = localStorage.getItem('cart');
        if (savedCart) {
            setItems(JSON.parse(savedCart));
        }
    }, []);
    
    const addItem = async (productId) => {
        setLoading(true);
        try {
            // 商品情報の取得
            const product = await fetch(`/api/products/${productId}`)
                .then(res => res.json());
            
            // 在庫確認
            if (product.stock < 1) {
                alert('在庫がありません');
                return;
            }
            
            // カートに追加
            const newItems = [...items, {
                ...product,
                quantity: 1,
                addedAt: new Date()
            }];
            
            setItems(newItems);
            localStorage.setItem('cart', JSON.stringify(newItems));
            
            // アナリティクス送信
            gtag('event', 'add_to_cart', {
                value: product.price,
                currency: 'JPY',
                items: [product]
            });
        } finally {
            setLoading(false);
        }
    };
    
    const calculateTotal = () => {
        let subtotal = items.reduce((sum, item) => 
            sum + (item.price * item.quantity), 0
        );
        
        // 会員割引
        if (user && user.membershipLevel === 'gold') {
            subtotal *= 0.9;
        }
        
        // クーポン割引
        if (coupon === 'SAVE10') {
            subtotal *= 0.9;
        } else if (coupon === 'SAVE20') {
            subtotal *= 0.8;
        }
        
        // 税金
        const tax = subtotal * 0.1;
        
        // 送料
        const shipping = subtotal > 5000 ? 0 : 500;
        
        return {
            subtotal,
            tax,
            shipping,
            total: subtotal + tax + shipping
        };
    };
    
    const checkout = async () => {
        if (!user) {
            alert('ログインしてください');
            return;
        }
        
        if (items.length === 0) {
            alert('カートが空です');
            return;
        }
        
        const totals = calculateTotal();
        
        try {
            const order = await fetch('/api/orders', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    items,
                    totals,
                    userId: user.id,
                    coupon
                })
            }).then(res => res.json());
            
            // カートをクリア
            setItems([]);
            localStorage.removeItem('cart');
            
            // 確認メール送信
            await fetch('/api/email/order-confirmation', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    orderId: order.id,
                    email: user.email
                })
            });
            
            // リダイレクト
            window.location.href = `/orders/${order.id}/complete`;
        } catch (error) {
            alert('注文処理中にエラーが発生しました');
        }
    };
    
    return (
        
{/* UI rendering... */}
); }

Level 3: モジュールレベルの演習

問題:認証モジュールの設計

以下の要件を満たす認証モジュールを、適切な責任分離で設計してください。

要件

  • ユーザーの登録・ログイン・ログアウト
  • パスワードのハッシュ化と検証
  • JWTトークンの発行と検証
  • リフレッシュトークンの管理
  • 2要素認証のサポート
  • ログイン試行回数の制限
  • パスワードリセット機能

モジュールの構造と、各部分の責任を明確に定義してください。

Level 4: システムレベルの演習

問題:ECサイトのマイクロサービス設計

中規模ECサイトをマイクロサービスアーキテクチャで設計してください。

機能要件

  • 商品カタログの管理と検索
  • 在庫管理
  • ショッピングカート
  • 注文処理
  • 決済処理
  • 配送管理
  • ユーザー管理と認証
  • レビューと評価
  • 推薦システム
  • 通知(メール、SMS)

各サービスの責任範囲、API設計、データの所有権、サービス間の通信方法を設計してください。

設計レビューのチェックリスト

責任分離の確認項目

  • □ 各単位(関数/クラス/モジュール/サービス)の責任は明確か?
  • □ 責任の粒度は適切か?(細かすぎず、大きすぎず)
  • □ 変更理由は1つに限定されているか?
  • □ 依存関係は適切に管理されているか?
  • □ テストが書きやすい構造になっているか?
  • □ 将来の拡張に対して開いているか?
  • □ 既存の実装に対して閉じているか?
  • □ インターフェースは安定しているか?
  • □ 循環依存は存在しないか?
  • □ 各部分は独立して理解可能か?