🥺

TypeScriptでOOP FizzBuzz

2022/07/12に公開

FizzBuzzとは

1から順に100まで数を数えていき、
3の倍数ならFizz
5の倍数ならBuzz
3の倍数かつ5の倍数ならFizzBuzz
それ以外の場合は数字そのままを出力する。

コーディング試験とかで使う企業もあるっぽい。

よくある回答

for (let i = 1; i <= 100; i++) {
  if (i % 3 == 0 && i % 5 == 0) {
    console.log('FizzBuzz');
  } else if (i % 3 == 0) {
    console.log('Fizz');
  } else if (i % 5 == 0) {
    console.log('Buzz');
  } else {
    console.log(i);
  }
}

OOPで最強のFizzBuzzにする

普通によくある回答のようなコード書いてもつまらない。
OOPの知識をゴリゴリに利用して最強のFizzBuzzを作って採用担当者をビビらせよう。

コードだけさっさと見たい人はこちら
https://github.com/Karibash/fizz-buzz-well-architected/blob/master/src/object-oriented-programming.ts

仕様を表現するクラスを定義する

値が仕様を満たしているかどうかをbooleanで返すメソッドを持つインターフェースを定義する。

interface Specification<Input> {
  isSatisfiedBy(value: Input): boolean;
}

FizzBuzzの仕様をチェックする為に、Specificationを実装したクラスを幾つか定義する。

class TypeSpecification implements Specification<any> {
  constructor(
    private readonly type: string,
  ) {}

  public isSatisfiedBy(value: unknown): boolean {
    return typeof value === this.type;
  }
}

class DivisibleSpecification implements Specification<number> {
  constructor(
    private readonly divisor: number,
  ) {}

  public isSatisfiedBy(value: number): boolean {
    return value % this.divisor === 0;
  }
}

これで型のチェックと、割り切れるかのチェックが行えるようになる。

const typeSpecification = new TypeSpecification('number');

console.log(typeSpecification.isSatisfiedBy(1));
// => true

console.log(typeSpecification.isSatisfiedBy('1'));
// => false

const divisibleSpecification = new DivisibleSpecification(3);

console.log(divisibleSpecification.isSatisfiedBy(3));
// => true

console.log(divisibleSpecification.isSatisfiedBy(5));
// => false

複数の仕様を組み合わせれるようにする、CompositeSpecificationを定義する。

class CompositeSpecification<Input> implements Specification<Input> {
  private constructor(
    private readonly predicate: (value: Input) => boolean,
  ) {}

  public static when<Input>(specification: Specification<Input>): CompositeSpecification<Input> {
    return new CompositeSpecification(value => specification.isSatisfiedBy(value));
  }

  public isSatisfiedBy(value: Input): boolean {
    return this.predicate(value);
  }

  public and(specification: Specification<Input>): CompositeSpecification<Input> {
    return new CompositeSpecification(value => this.predicate(value) && specification.isSatisfiedBy(value));
  }

  public or(specification: Specification<Input>): CompositeSpecification<Input> {
    return new CompositeSpecification(value => this.predicate(value) || specification.isSatisfiedBy(value));
  }
}

下記のようにメソッドチェーンする事で複数条件の判定を行うことが出来るようになる。

const fizzBuzzSpecification = CompositeSpecification
  .when(new TypeSpecification('number'))
  .and(new DivisibleSpecification(3))
  .and(new DivisibleSpecification(5)),

console.log(fizzBuzzSpecification.isSatisfiedBy(1));
// => false

console.log(fizzBuzzSpecification.isSatisfiedBy(15));
// => true

数字を文字列に変換するクラスを定義する

数字を文字列に変換するNumberToStringOperationクラスを定義する。

interface Operation<Input, Output> {
  invoke(value: Input): Output;
}

class NumberToStringOperation implements Operation<number, string> {
  constructor(
    private readonly converter: (value: number) => string,
  ) {}

  public invoke(value: number): string {
    return this.converter(value);
  }
}

下記のように使用する。

const numberToStringOperation = new NumberToStringOperation(value => value.toString());

console.log(numberToStringOperation.invoke(1);
// -> 1

const fizzOperation = new NumberToStringOperation(() => 'Fizz');

console.log(fizzOperation.invoke(3));
// => Fizz

仕様を満たすOperationを実行するクラスを定義する

Specificationを保持している事を表すインターフェースと、仕様を満たすOperationを実行するクラスを定義する。

interface HasSpecification<Input> {
  readonly specification: Specification<Input>;
}

class Operator<Input, Output> {
  constructor(
    private readonly operations: Array<Operation<Input, Output> & HasSpecification<Input>>,
  ) {}

  public invoke(value: Input): Output {
    const operation = this.operations.find(operation => operation.specification.isSatisfiedBy(value));
    if (!operation) throw new Error('Operation does not found');
    return operation.invoke(value);
  }
}

Operatorで使用できるように、NumberToStringOperationにHasSpecificationを実装する。

- class NumberToStringOperation implements Operation<number, string> {
+ class NumberToStringOperation implements Operation<number, string>, HasSpecification<number> {
  constructor(
    private readonly converter: (value: number) => string,
+   public readonly specification: Specification<number>,
  ) {}

  public invoke(value: number): string {
    return this.converter(value);
  }
}

完成

FizzBuzzの仕様を定義したOperatorを実装して完成。
これでFizzBuzzの仕様が唐突に変わっても安心ですね。

const operator = new Operator([
  new NumberToStringOperation(() => 'FizzBuzz',
    CompositeSpecification
      .when(new TypeSpecification('number'))
      .and(new DivisibleSpecification(3))
      .and(new DivisibleSpecification(5)),
  ),
  new NumberToStringOperation(() => 'Fizz',
    CompositeSpecification
      .when(new TypeSpecification('number'))
      .and(new DivisibleSpecification(3)),
  ),
  new NumberToStringOperation(() => 'Buzz',
    CompositeSpecification
      .when(new TypeSpecification('number'))
      .and(new DivisibleSpecification(5)),
  ),
  new NumberToStringOperation(value => value.toString(),
    CompositeSpecification
      .when(new TypeSpecification('number')),
  ),
]);

for (let i = 1; i <= 100; i++) {
  console.log(operator.invoke(i));
}

おまけ (世界のナベアツ)

3の倍数と3が付く数字でアホになるバージョンを作る。
3が付く数字を判定するSpecificationを追加するだけ。

class NumberIncludesSpecification implements Specification<number> {
  constructor(
    private readonly value: number,
  ) {}

  public isSatisfiedBy(value: number): boolean {
    return value.toString().indexOf(this.value.toString()) !== -1;
  }
}

FizzBuzzのロジック部だけ変えればナベアツの出来上がり。

const operator = new Operator([
  new NumberToStringOperation(value => `( ᐛ ) > ${value}`,
    CompositeSpecification
      .when(new TypeSpecification('number'))
      .and(new IncludesSpecification(3)),
  ),
  new NumberToStringOperation(value => `( ᐛ ) > ${value}`,
    CompositeSpecification
      .when(new TypeSpecification('number'))
      .and(new DivisibleSpecification(3)),
  ),
  new NumberToStringOperation(value => `( ˙-˙ ) > ${value}`,
    CompositeSpecification
      .when(new TypeSpecification('number')),
  ),
]);

for (let i = 1; i <= 100; i++) {
  console.log(operator.invoke(i));
}

Discussion