関数レベルの責任分離

単一責任の関数設計

良い関数は「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) {
        // 表示を更新
    }
}