今回はGoFのデザインパターンのうち、
「振る舞い」に関するパターンをTypeScriptで実装してみます。
GoFのデザインパターンとは
そもそもデザインパターンとは何でしょうか。
Wikipediaには下記のように記述されています。
ソフトウェア開発におけるデザインパターンまたは設計パターン(英: design pattern)とは、過去のソフトウェア設計者が発見し編み出した設計ノウハウを蓄積し、名前をつけ、再利用しやすいように特定の規約に従ってカタログ化したものである。
つまり、数々の経験によって生み出された「このように設計しておくと便利である」という設計をパターン化したものです。
GoFのデザインパターンとは、
Gang of Four と呼ばれる4人組によって設計されたデザインパターンのことを指します。
このデザインパターンは役割に基づいてさらに3つのパターンに分類されています。
今回はそのうちの「振る舞い」に関するパターンをTypeScriptで実装していきます。
「振る舞い」デザインパターン実装
Chain of Responsibility
Chain of Responsibility
は、リクエストを複数の処理オブジェクト(ハンドラー)の連鎖に沿って渡します。
各ハンドラーは、リクエストを処理するか、次のハンドラーに渡すかを決定します。
// ハンドラーのインターフェース
interface Handler {
setNext(handler: Handler): Handler;
handle(request: string): string | null;
}
// 基本のハンドラー
abstract class AbstractHandler implements Handler {
private nextHandler: Handler | null = null;
setNext(handler: Handler): Handler {
this.nextHandler = handler;
return handler;
}
handle(request: string): string | null {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null;
}
}
// 具体的なハンドラー1
class DogHandler extends AbstractHandler {
handle(request: string): string | null {
if (request === "Bone") {
return `Dog: I'll eat the ${request}.`;
}
return super.handle(request);
}
}
// 具体的なハンドラー2
class CatHandler extends AbstractHandler {
handle(request: string): string | null {
if (request === "Fish") {
return `Cat: I'll eat the ${request}.`;
}
return super.handle(request);
}
}
// 具体的なハンドラー3
class MonkeyHandler extends AbstractHandler {
handle(request: string): string | null {
if (request === "Banana") {
return `Monkey: I'll eat the ${request}.`;
}
return super.handle(request);
}
}
// 使用例
const dog = new DogHandler();
const cat = new CatHandler();
const monkey = new MonkeyHandler();
dog.setNext(cat).setNext(monkey);
const foods = ["Banana", "Bone", "Fish", "Nut"];
for (const food of foods) {
const result = dog.handle(food);
console.log(result || `${food} was left untouched.`);
}
// Mondkey: I'll eat the Banana.
// Dog: I'll eat the Bone.
// Cat: I'll eat the Fish.
// Nut was left untouched.
Command
Command
は、リクエストをオブジェクトとしてカプセル化します。
これにより、リクエストをパラメータとして渡したり、キューに入れたりすることができます。
// コマンドのインターフェース
interface Command {
execute(): void;
}
// 具体的なコマンド
class LightOnCommand implements Command {
execute(): void {
console.log("The light is ON.");
}
}
class LightOffCommand implements Command {
execute(): void {
console.log("The light is OFF.");
}
}
// Invoker(呼び出し元)
class RemoteControl {
private command: Command | null = null;
setCommand(command: Command): void {
this.command = command;
}
pressButton(): void {
if (this.command) {
this.command.execute();
}
}
}
// 使用例
const remote = new RemoteControl();
const lightOn = new LightOnCommand();
const lightOff = new LightOffCommand();
remote.setCommand(lightOn);
remote.pressButton(); // "The light is ON."
remote.setCommand(lightOff);
remote.pressButton(); // "The light is OFF."
Iterator
Iterator
は、コレクションの内部構造を露出せずに、その要素に順次アクセスする方法を提供します。
// コレクションのインターフェース
interface Iterator<T> {
next(): T | null;
hasNext(): boolean;
}
// コレクション
class NumberCollection {
private numbers: number[] = [];
addNumber(num: number): void {
this.numbers.push(num);
}
createIterator(): Iterator<number> {
return new NumberIterator(this.numbers);
}
}
// 具体的なイテレータ
class NumberIterator implements Iterator<number> {
private collection: number[];
private position = 0;
constructor(collection: number[]) {
this.collection = collection;
}
next(): number | null {
if (this.hasNext()) {
return this.collection[this.position++];
}
return null;
}
hasNext(): boolean {
return this.position < this.collection.length;
}
}
// 使用例
const collection = new NumberCollection();
collection.addNumber(1);
collection.addNumber(2);
collection.addNumber(3);
const iterator = collection.createIterator();
while (iterator.hasNext()) {
console.log(iterator.next());
}
// 1
// 2
// 3
Mediator
Mediator
は、オブジェクト間の複雑な依存関係を減らすために使用されます。
これによりシステムのコンポーネント間の結合度を低減できます。
// Mediatorのインターフェース
interface Mediator {
notify(sender: string, event: string): void;
}
// 具体的なMediator
class ConcreteMediator implements Mediator {
private component1: Component1;
private component2: Component2;
constructor(c1: Component1, c2: Component2) {
this.component1 = c1;
this.component2 = c2;
c1.setMediator(this);
c2.setMediator(this);
}
notify(sender: string, event: string): void {
if (event === "A") {
console.log("Mediator reacts on A and triggers B.");
this.component2.doC();
}
if (event === "D") {
console.log("Mediator reacts on D and triggers A.");
this.component1.doA();
}
}
}
// コンポーネント
class BaseComponent {
protected mediator: Mediator | null = null;
setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
}
class Component1 extends BaseComponent {
doA(): void {
console.log("Component 1 does A.");
this.mediator?.notify("Component1", "A");
}
}
class Component2 extends BaseComponent {
doC(): void {
console.log("Component 2 does C.");
this.mediator?.notify("Component2", "D");
}
}
// 使用例
const c1 = new Component1();
const c2 = new Component2();
const mediator = new ConcreteMediator(c1, c2);
c1.doA(); // Mediator triggers Component 2's action
c2.doC(); // Mediator triggers Component 1's action
Memento
Memento
は、オブジェクトの内部状態を保存し、後でその状態に復元することを可能にします。
class Memento {
private state: string;
constructor(state: string) {
this.state = state;
}
getState(): string {
return this.state;
}
}
class Originator {
private state: string = "";
setState(state: string): void {
this.state = state;
console.log(`State changed to: ${state}`);
}
save(): Memento {
return new Memento(this.state);
}
restore(memento: Memento): void {
this.state = memento.getState();
console.log(`State restored to: ${this.state}`);
}
}
class Caretaker {
private mementos: Memento[] = [];
private originator: Originator;
constructor(originator: Originator) {
this.originator = originator;
}
backup(): void {
console.log("Saving state...");
this.mementos.push(this.originator.save());
}
undo(): void {
if (!this.mementos.length) return;
const memento = this.mementos.pop()!;
console.log("Restoring state...");
this.originator.restore(memento);
}
}
// 使用例
const originator = new Originator();
const caretaker = new Caretaker(originator);
originator.setState("State1");
caretaker.backup();
originator.setState("State2");
caretaker.backup();
originator.setState("State3");
caretaker.undo(); // "State restored to: State2"
caretaker.undo(); // "State restored to: State1"
Observer
Observer
は、オブジェクト間の1対多の依存関係を定義します。
あるオブジェクトの状態が変化したときに、それに依存するすべてのオブジェクトに自動的に通知し、更新を行います。
// Observer インターフェース
interface Observer {
update(state: string): void;
}
// Subject クラス
class Subject {
private observers: Observer[] = [];
private state: string = "";
attach(observer: Observer): void {
this.observers.push(observer);
}
detach(observer: Observer): void {
this.observers = this.observers.filter(o => o !== observer);
}
notify(): void {
for (const observer of this.observers) {
observer.update(this.state);
}
}
setState(state: string): void {
this.state = state;
console.log(`Subject: State changed to "${state}"`);
this.notify();
}
}
// 具体的な Observer
class ConcreteObserver implements Observer {
constructor(private name: string) {}
update(state: string): void {
console.log(`${this.name} received state: ${state}`);
}
}
// 使用例
const subject = new Subject();
const observer1 = new ConcreteObserver("Observer 1");
const observer2 = new ConcreteObserver("Observer 2");
subject.attach(observer1);
subject.attach(observer2);
subject.setState("State A"); // Observer 1, 2 が通知を受け取る
subject.setState("State B");
State
State
は、オブジェクトの内部状態が変化したときにその振る舞いを変更させます。
状態毎に別々のクラスを作江精することで、条件分岐を減らし、コードの可読性と保守性を向上させます。
// 状態インターフェース
interface State {
handle(context: Context): void;
}
// 具体的な状態
class ConcreteStateA implements State {
handle(context: Context): void {
console.log("State A: Handling request.");
context.setState(new ConcreteStateB());
}
}
class ConcreteStateB implements State {
handle(context: Context): void {
console.log("State B: Handling request.");
context.setState(new ConcreteStateA());
}
}
// コンテキスト
class Context {
private state: State;
constructor(state: State) {
this.state = state;
}
setState(state: State): void {
this.state = state;
console.log(`Context: State changed.`);
}
request(): void {
this.state.handle(this);
}
}
// 使用例
const context = new Context(new ConcreteStateA());
context.request(); // State A -> State B
context.request(); // State B -> State A
Template Method
Template Method
は、アルゴリズムの骨格をスーパークラスで定義し、具体的なステップの実装をサブクラスに委ねます。
これにより、アルゴリズムの構造を変更せずに、特定のステップをオーバーライドすることができます。
// 抽象クラス
abstract class Template {
// テンプレートメソッド
public templateMethod(): void {
this.baseOperation1();
this.requiredOperation1();
this.baseOperation2();
this.requiredOperation2();
}
private baseOperation1(): void {
console.log("Template: Base operation 1");
}
private baseOperation2(): void {
console.log("Template: Base operation 2");
}
protected abstract requiredOperation1(): void;
protected abstract requiredOperation2(): void;
}
// 具体的なクラス
class ConcreteClass1 extends Template {
protected requiredOperation1(): void {
console.log("ConcreteClass1: Required operation 1");
}
protected requiredOperation2(): void {
console.log("ConcreteClass1: Required operation 2");
}
}
class ConcreteClass2 extends Template {
protected requiredOperation1(): void {
console.log("ConcreteClass2: Required operation 1");
}
protected requiredOperation2(): void {
console.log("ConcreteClass2: Required operation 2");
}
}
// 使用例
const instance1 = new ConcreteClass1();
instance1.templateMethod();
const instance2 = new ConcreteClass2();
instance2.templateMethod();
Visitor
Visitor
は、アルゴリズムをそれが動作するオブジェクト構造から分離します。
このパターンを使用すると、オブジェクト構造を変更せずに新しい操作を追加することができます。
// Visitor インターフェース
interface Visitor {
visitElementA(element: ElementA): void;
visitElementB(element: ElementB): void;
}
// 要素インターフェース
interface Element {
accept(visitor: Visitor): void;
}
// 具体的な要素
class ElementA implements Element {
accept(visitor: Visitor): void {
visitor.visitElementA(this);
}
operationA(): string {
return "ElementA operation";
}
}
class ElementB implements Element {
accept(visitor: Visitor): void {
visitor.visitElementB(this);
}
operationB(): string {
return "ElementB operation";
}
}
// 具体的な Visitor
class ConcreteVisitor implements Visitor {
visitElementA(element: ElementA): void {
console.log(`ConcreteVisitor: ${element.operationA()}`);
}
visitElementB(element: ElementB): void {
console.log(`ConcreteVisitor: ${element.operationB()}`);
}
}
// 使用例
const elements: Element[] = [new ElementA(), new ElementB()];
const visitor = new ConcreteVisitor();
for (const element of elements) {
element.accept(visitor);
}
// ConcreteVisitor: ElementA operation
// ConcreteVisitor: ElementB operation
おわりに
全3回で各デザインパターンをTypeScriptで実装しました。
バックエンドをNode.jsで実装する際に、
適した処理があれば工夫して使用していきたいと思います。
それでは。
参考文献