実践・演習
各スケールでの責任分離を実践的に身につける演習問題
演習の進め方
各演習は、小さなスケールから大きなスケールへと段階的に進んでいきます。まず問題のコードを分析し、責任分離の原則に従ってリファクタリングしてください。
演習のポイント
- 責任の境界を明確に識別する
- 各スケールに適した分離方法を選択する
- テスタビリティの向上を意識する
- 将来の変更に対する柔軟性を考慮する
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つに限定されているか?
- □ 依存関係は適切に管理されているか?
- □ テストが書きやすい構造になっているか?
- □ 将来の拡張に対して開いているか?
- □ 既存の実装に対して閉じているか?
- □ インターフェースは安定しているか?
- □ 循環依存は存在しないか?
- □ 各部分は独立して理解可能か?