演習1:責任混在の識別とリファクタリング

問題のコード

以下のコードには複数の責任が混在しています。問題点を識別し、リファクタリングしてください。

class UserManager {
    constructor() {
        this.users = [];
    }
    
    async createUser(userData) {
        // 入力検証
        if (!userData.email || !userData.email.includes('@')) {
            throw new Error('Invalid email');
        }
        if (!userData.password || userData.password.length < 8) {
            throw new Error('Password must be at least 8 characters');
        }
        if (userData.age && userData.age < 18) {
            throw new Error('Must be 18 or older');
        }
        
        // パスワードのハッシュ化
        const salt = crypto.randomBytes(16).toString('hex');
        const hash = crypto.pbkdf2Sync(userData.password, salt, 1000, 64, 'sha512').toString('hex');
        
        // ユーザーオブジェクトの作成
        const user = {
            id: Date.now().toString(),
            email: userData.email.toLowerCase(),
            passwordHash: hash,
            passwordSalt: salt,
            age: userData.age,
            createdAt: new Date()
        };
        
        // データベースへの保存
        const connection = await mysql.createConnection({
            host: 'localhost',
            user: 'root',
            password: 'password',
            database: 'myapp'
        });
        
        await connection.execute(
            'INSERT INTO users (id, email, password_hash, password_salt, age, created_at) VALUES (?, ?, ?, ?, ?, ?)',
            [user.id, user.email, user.passwordHash, user.passwordSalt, user.age, user.createdAt]
        );
        
        await connection.end();
        
        // ウェルカムメールの送信
        const transporter = nodemailer.createTransport({
            host: 'smtp.gmail.com',
            port: 587,
            auth: {
                user: 'noreply@myapp.com',
                pass: 'emailpassword'
            }
        });
        
        await transporter.sendMail({
            from: 'noreply@myapp.com',
            to: user.email,
            subject: 'Welcome to MyApp!',
            html: `

Welcome ${user.email}!

Thank you for joining us.

` }); // 管理者への通知 console.log(`New user registered: ${user.email}`); // 統計の更新 const stats = JSON.parse(fs.readFileSync('stats.json', 'utf8')); stats.totalUsers++; stats.lastSignup = new Date(); fs.writeFileSync('stats.json', JSON.stringify(stats, null, 2)); return user; } }

識別すべき責任

  • 入力検証
  • パスワード処理
  • ユーザーエンティティの生成
  • データベース操作
  • メール送信
  • ロギング
  • 統計更新
解答例を見る

リファクタリング後のコード

// 1. バリデーション責任
class UserValidator {
    validate(userData) {
        const errors = [];
        
        if (!userData.email || !this.isValidEmail(userData.email)) {
            errors.push('Invalid email format');
        }
        
        if (!userData.password || userData.password.length < 8) {
            errors.push('Password must be at least 8 characters');
        }
        
        if (userData.age && userData.age < 18) {
            errors.push('Must be 18 or older');
        }
        
        if (errors.length > 0) {
            throw new ValidationError(errors);
        }
    }
    
    private isValidEmail(email) {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
    }
}

// 2. パスワード処理責任
class PasswordService {
    async hash(password) {
        const salt = crypto.randomBytes(16).toString('hex');
        const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
        return { hash, salt };
    }
    
    async verify(password, hash, salt) {
        const verifyHash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
        return hash === verifyHash;
    }
}

// 3. ユーザーエンティティ
class User {
    constructor(data) {
        this.id = data.id || this.generateId();
        this.email = data.email.toLowerCase();
        this.passwordHash = data.passwordHash;
        this.passwordSalt = data.passwordSalt;
        this.age = data.age;
        this.createdAt = data.createdAt || new Date();
    }
    
    private generateId() {
        return Date.now().toString() + Math.random().toString(36).substr(2, 9);
    }
}

// 4. リポジトリ責任
interface UserRepository {
    save(user: User): Promise;
    findByEmail(email: string): Promise;
}

class MySQLUserRepository implements UserRepository {
    constructor(private connectionPool) {}
    
    async save(user) {
        await this.connectionPool.execute(
            'INSERT INTO users (id, email, password_hash, password_salt, age, created_at) VALUES (?, ?, ?, ?, ?, ?)',
            [user.id, user.email, user.passwordHash, user.passwordSalt, user.age, user.createdAt]
        );
    }
    
    async findByEmail(email) {
        const [rows] = await this.connectionPool.execute(
            'SELECT * FROM users WHERE email = ?',
            [email]
        );
        return rows.length > 0 ? new User(rows[0]) : null;
    }
}

// 5. 通知責任
interface NotificationService {
    sendWelcomeEmail(user: User): Promise;
}

class EmailNotificationService implements NotificationService {
    constructor(private emailTransporter) {}
    
    async sendWelcomeEmail(user) {
        await this.emailTransporter.sendMail({
            from: 'noreply@myapp.com',
            to: user.email,
            subject: 'Welcome to MyApp!',
            html: `

Welcome ${user.email}!

Thank you for joining us.

` }); } } // 6. 統計責任 class UserStatisticsService { constructor(private statsRepository) {} async incrementUserCount() { const stats = await this.statsRepository.get(); stats.totalUsers++; stats.lastSignup = new Date(); await this.statsRepository.save(stats); } } // 7. ユースケース(調整役) class CreateUserUseCase { constructor( private validator: UserValidator, private passwordService: PasswordService, private userRepository: UserRepository, private notificationService: NotificationService, private statisticsService: UserStatisticsService, private logger: Logger ) {} async execute(userData) { // 検証 this.validator.validate(userData); // 重複チェック const existingUser = await this.userRepository.findByEmail(userData.email); if (existingUser) { throw new Error('Email already registered'); } // パスワードハッシュ化 const { hash, salt } = await this.passwordService.hash(userData.password); // ユーザー作成 const user = new User({ email: userData.email, passwordHash: hash, passwordSalt: salt, age: userData.age }); // 保存 await this.userRepository.save(user); // 通知(エラーがあっても処理は続行) try { await this.notificationService.sendWelcomeEmail(user); } catch (error) { this.logger.error('Failed to send welcome email', error); } // 統計更新 await this.statisticsService.incrementUserCount(); // ログ this.logger.info(`New user registered: ${user.email}`); return user; } }

演習2:依存関係の逆転

問題のコード

以下のコードは具体的な実装に強く依存しています。依存性逆転の原則を適用してください。

class ProductService {
    getDiscountedProducts() {
        // MySQLに直接依存
        const mysql = new MySQLDatabase();
        const products = mysql.query('SELECT * FROM products WHERE active = true');
        
        // Redisに直接依存
        const redis = new RedisCache();
        const discountRules = redis.get('discount_rules');
        
        // 具体的な計算ロジック
        const discountedProducts = products.map(product => {
            const applicableRule = discountRules.find(rule => 
                rule.category === product.category
            );
            
            if (applicableRule) {
                product.discountedPrice = product.price * (1 - applicableRule.rate);
            }
            
            return product;
        });
        
        // Elasticsearchに直接依存
        const elasticsearch = new ElasticsearchClient();
        elasticsearch.index('discounted_products', discountedProducts);
        
        return discountedProducts;
    }
}

演習3:設計レビュー

コードレビューの観点

実際のプロジェクトでコードレビューを行う際のチェックリストです。

責任分離のチェックポイント

  • □ 各クラス/関数の責任は明確か?
  • □ 単一責任の原則に従っているか?
  • □ 依存関係は適切に管理されているか?
  • □ インターフェースは適切に分離されているか?
  • □ テストが書きやすい構造になっているか?
  • □ 変更理由が複数存在しないか?
  • □ 循環依存は存在しないか?
  • □ 各モジュールは独立して理解可能か?

実践課題

TODOアプリケーションの設計

以下の要件を満たすTODOアプリケーションを、責任分離の原則に従って設計してください。

要件

  • タスクの作成・更新・削除
  • タスクの完了/未完了の切り替え
  • 期限の設定と通知
  • カテゴリーによる分類
  • 複数ユーザーのサポート
  • データの永続化

設計時に考慮すべき点:

  • どのような責任(モジュール)に分割するか
  • 各モジュール間の依存関係をどう管理するか
  • 将来の拡張性をどう確保するか