Decoratorパターン

概要

既存の機能に対して、それを変更することなく、前後に機能を追加するためのデザインパターン。 ラッパーを作りまくるとラッパークラスが増えていくので、ラッパーの数が多くなりすぎると管理が大変になる。

インターフェースを統一して、ベース機能 x ラッパー機能の組み合わせで、クラスの数を増やさずに機能を追加できる。

使い方

bad

無限ラッパー編

interface UserRepositoryInterface {
  find(id: number): Promise<User>;
}

class UserRepository implements UserRepositoryInterface {
  async find(id: number): Promise<User> {
    return await db.findUser(id);
  }
}

class UserRepositoryWithLog implements UserRepositoryInterface {
  async find(id: number): Promise<User> {
    logger.info(`Finding user with id: ${id}`);
    const user = await db.findUser(id);
    logger.info(`Found user: ${user}`);
    return user;
  }
}

class UserRepositoryWithCache implements UserRepositoryInterface {
  async find(id: number): Promise<User> {
    const cachedUser = cache.get(id);
    if (cachedUser) {
      return cachedUser;
    }
    const user = await db.findUser(id);
    cache.set(id, user);
    return user;
  }
}

class UserRepositoryWithLogAndCache implements UserRepositoryInterface {
  async find(id: number): Promise<User> {
    logger.info(`Finding user with id: ${id}`);
    const cachedUser = cache.get(id);
    if (cachedUser) {
      return cachedUser;
    }
    const user = await db.findUser(id);
    logger.info(`Found user: ${user}`);
    cache.set(id, user);
    return user;
  }
}

純粋なUserRepositoryとLog付きのUserRepositoryWithLog、Cache付きのUserRepositoryWithCache、LogとCache両方付きのUserRepositoryWithLogAndCacheといった具合に、ラッパーが増えていく。

責務も増えていくし、ラッパーの数が増えすぎると管理が大変になる。(それぞれのUserRepositoryの実装も重複してしまう)

good

interface UserRepositoryInterface {
  find(id: number): Promise<User>;
}

abstract class UserRepositoryDecorator extends UserRepositoryInterface {
  constructor(userRepository: UserRepositoryInterface);
  find(id: number): Promise<User>;
}

class UserRepository implements UserRepositoryInterface {
  async find(id: number): Promise<User> {
    return await db.findUser(id);
  }
}

class UserRepositoryDecorator implements UserRepositoryDecorator {
  constructor(private userRepository: UserRepositoryInterface) {}

  async find(id: number): Promise<User> {
    return await this.userRepository.find(id);
  }
}

class LoggerDecorator extends UserRepositoryDecorator {
  async find(id: number): Promise<User> {
    logger.info(`Finding user with id: ${id}`);
    const user = await super.find(id);
    logger.info(`Found user: ${user}`);
    return user;
  }
}

class CacheDecorator extends UserRepositoryDecorator {
  async find(id: number): Promise<User> {
    const cachedUser = cache.get(id);
    if (cachedUser) {
      return cachedUser;
    }
    const user = await super.find(id);
    cache.set(id, user);
    return user;
  }
}

const userRepository = new CacheDecorator(new LoggerDecorator(new UserRepository()));

Decoratorに注入することによって機能を追加できるようになった。 LogやCacheの機能を追加するごとにラッパークラスを作る必要がなくなった。

使い所

  • 追加する機能が本来の処理の前後に挟みたいとき
  • 基本の機能はそのままにして、複数の機能を追加したいとき
  • 機能を追加するときに、継承が難しいパターン

参考