コンポーネント設計
関数・クラス・UIコンポーネントレベルでの責任分離を実践する
関数レベルの責任分離
単一責任の関数設計
良い関数は「1つのことを上手くやる」という原則に従います。関数名を読んだだけで何をするか明確にわかるべきです。
❌ 複数の責任を持つ関数
function processUserData(users) {
let validUsers = [];
let totalAge = 0;
// 検証、変換、集計が混在
for (let user of users) {
if (user.age >= 18 && user.email.includes('@')) {
user.name = user.name.toUpperCase();
user.joinDate = new Date();
validUsers.push(user);
totalAge += user.age;
}
}
const averageAge = totalAge / validUsers.length;
// ログ出力も含まれている
console.log(`Processed ${validUsers.length} users`);
console.log(`Average age: ${averageAge}`);
return { validUsers, averageAge };
}
✅ 単一責任の関数群
// 検証の責任
function isValidUser(user) {
return user.age >= 18 && user.email.includes('@');
}
// フィルタリングの責任
function filterValidUsers(users) {
return users.filter(isValidUser);
}
// 変換の責任
function transformUser(user) {
return {
...user,
name: user.name.toUpperCase(),
joinDate: new Date()
};
}
// 集計の責任
function calculateAverageAge(users) {
if (users.length === 0) return 0;
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
return totalAge / users.length;
}
// 統合関数(調整役)
function processUsers(users) {
const validUsers = filterValidUsers(users);
const transformedUsers = validUsers.map(transformUser);
const averageAge = calculateAverageAge(transformedUsers);
return { users: transformedUsers, averageAge };
}
関数設計のベストプラクティス
- 関数名は動詞で始める(calculateTotal, validateEmail)
- 引数は3つ以下に抑える
- 副作用を避け、純粋関数を目指す
- 早期リターンで入れ子を減らす
- 1つの抽象レベルで記述する
クラスレベルの責任分離
単一責任のクラス設計
クラスは関連するデータと振る舞いをカプセル化しますが、その責任は明確に定義されるべきです。
// Before: 神クラス(God Class)
class UserManager {
constructor() {
this.users = [];
this.emailService = new EmailService();
this.database = new Database();
}
// ユーザー管理
addUser(user) { /* ... */ }
removeUser(userId) { /* ... */ }
updateUser(userId, data) { /* ... */ }
// 認証
login(email, password) { /* ... */ }
logout(userId) { /* ... */ }
resetPassword(email) { /* ... */ }
// 通知
sendWelcomeEmail(user) { /* ... */ }
sendPasswordResetEmail(user) { /* ... */ }
// データ永続化
saveToDatabase() { /* ... */ }
loadFromDatabase() { /* ... */ }
// レポート
generateUserReport() { /* ... */ }
exportToCSV() { /* ... */ }
}
// After: 責任ごとに分離されたクラス群
class User {
constructor(id, email, name) {
this.id = id;
this.email = email;
this.name = name;
}
}
class UserRepository {
constructor(database) {
this.database = database;
}
save(user) { /* ... */ }
findById(id) { /* ... */ }
findByEmail(email) { /* ... */ }
delete(id) { /* ... */ }
}
class AuthenticationService {
constructor(userRepository, passwordHasher) {
this.userRepository = userRepository;
this.passwordHasher = passwordHasher;
}
login(email, password) { /* ... */ }
logout(userId) { /* ... */ }
resetPassword(email) { /* ... */ }
}
class UserNotificationService {
constructor(emailService) {
this.emailService = emailService;
}
sendWelcomeEmail(user) { /* ... */ }
sendPasswordResetEmail(user, token) { /* ... */ }
}
class UserReportService {
constructor(userRepository) {
this.userRepository = userRepository;
}
generateReport(criteria) { /* ... */ }
exportToCSV(report) { /* ... */ }
}
UIコンポーネントの責任分離
プレゼンテーションとロジックの分離
UIコンポーネントでは、表示責任とビジネスロジックを明確に分離することが重要です。
❌ 責任が混在したコンポーネント
// React コンポーネントの例
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
// データ取得ロジック
setLoading(true);
fetch('/api/products')
.then(res => res.json())
.then(data => {
// ビジネスロジック
const discountedProducts = data.map(p => ({
...p,
price: p.price * 0.9,
isOnSale: p.stock < 10
}));
// ソートロジック
discountedProducts.sort((a, b) =>
b.price - a.price
);
setProducts(discountedProducts);
setLoading(false);
});
}, []);
// 検索ロジック
const searchProducts = (term) => {
const filtered = products.filter(p =>
p.name.toLowerCase().includes(term.toLowerCase())
);
setProducts(filtered);
};
return (
{/* UI表示 */}
);
}
✅ 責任が分離されたコンポーネント
// ビジネスロジック(別ファイル)
class ProductService {
applyDiscount(products, rate = 0.9) {
return products.map(p => ({
...p,
price: p.price * rate
}));
}
markLowStock(products, threshold = 10) {
return products.map(p => ({
...p,
isOnSale: p.stock < threshold
}));
}
sortByPrice(products, order = 'desc') {
const sorted = [...products];
sorted.sort((a, b) =>
order === 'desc' ? b.price - a.price : a.price - b.price
);
return sorted;
}
}
// カスタムフック(データ取得の責任)
function useProducts() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchProducts = async () => {
setLoading(true);
try {
const response = await fetch('/api/products');
const data = await response.json();
setProducts(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchProducts();
}, []);
return { products, loading, error };
}
// プレゼンテーションコンポーネント(表示の責任)
function ProductListView({ products, onSearch }) {
return (
{products.map(product => (
))}
);
}
// コンテナコンポーネント(統合の責任)
function ProductListContainer() {
const { products, loading, error } = useProducts();
const productService = new ProductService();
const processedProducts = useMemo(() => {
let result = products;
result = productService.applyDiscount(result);
result = productService.markLowStock(result);
result = productService.sortByPrice(result);
return result;
}, [products]);
const handleSearch = (term) => {
// 検索ロジックを分離
};
if (loading) return ;
if (error) return ;
return (
);
}
インターフェース設計の原則
明確で最小限のインターフェース
コンポーネント間の境界を明確にし、必要最小限の情報のみを公開します。
インターフェース設計のガイドライン
- 必要なメソッドのみを公開する(最小インターフェース)
- 実装の詳細を隠蔽する(カプセル化)
- 一貫性のある命名規則を使用する
- 引数と戻り値の型を明確にする
- エラーハンドリングの方法を統一する
// 良いインターフェース設計の例 interface Repository{ findById(id: string): Promise ; save(entity: T): Promise ; delete(id: string): Promise ; } interface EmailService { send(to: string, subject: string, body: string): Promise ; } interface Logger { debug(message: string, context?: any): void; info(message: string, context?: any): void; warn(message: string, context?: any): void; error(message: string, error?: Error): void; } // 実装クラスは詳細を隠蔽 class FileLogger implements Logger { private fileWriter: FileWriter; constructor(logFilePath: string) { this.fileWriter = new FileWriter(logFilePath); } debug(message: string, context?: any): void { this.writeLog('DEBUG', message, context); } // 他のメソッドの実装... private writeLog(level: string, message: string, context?: any): void { // 実装の詳細は隠蔽 } }
コンポーネント間の協調
疎結合な連携パターン
コンポーネント同士は、明確なインターフェースを通じて協調動作します。
// イベント駆動による疎結合な連携
class EventBus {
private listeners = new Map();
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(callback => callback(data));
}
}
}
// 各コンポーネントは独立して動作
class ShoppingCart {
constructor(eventBus) {
this.eventBus = eventBus;
this.items = [];
}
addItem(item) {
this.items.push(item);
this.eventBus.emit('cart:item-added', { item, total: this.getTotal() });
}
}
class PriceDisplay {
constructor(eventBus) {
eventBus.on('cart:item-added', ({ total }) => {
this.updateDisplay(total);
});
}
updateDisplay(total) {
// 表示を更新
}
}