🥺
TypeScriptでOOP FizzBuzz
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を作って採用担当者をビビらせよう。
コードだけさっさと見たい人はこちら
仕様を表現するクラスを定義する
値が仕様を満たしているかどうかを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