演習・実践
実際のコードを使って、責任分離の原則を実践的に身につけましょう
演習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アプリケーションを、責任分離の原則に従って設計してください。
要件
- タスクの作成・更新・削除
- タスクの完了/未完了の切り替え
- 期限の設定と通知
- カテゴリーによる分類
- 複数ユーザーのサポート
- データの永続化
設計時に考慮すべき点:
- どのような責任(モジュール)に分割するか
- 各モジュール間の依存関係をどう管理するか
- 将来の拡張性をどう確保するか