デザインパターン - GoF

3つの分類

  • 23のパターンはそれぞれ3つに分類される

1. 生成に関するパターン

デザインパターン概要
Abstract Factory関連する部品を組み合わせて製品を作る
Builder複雑なインスタンスを組み立てる
Factory Methodインスタンス作成をサブクラスにまかせる
Prototypeコピーしてインスタンスを作る
Singletonたった1つのインスタンス

2. 構造に関するパターン

デザインパターン概要
Adapter一皮かぶせて再利用
Bridge機能の階層と実装の階層を分ける
Composite容器と中身の同一視
Decorator飾り枠と中身の同一視
Facadeシンプルな窓口
Flyweight同じものを共有して無駄をなくす
Proxy必要になってから作る

3. 振る舞いに関するパターン

デザインパターン概要
Chain of Responsibility責任のたらい回し
Command命令をクラスにする
Interpreter文法規則をクラスで表現する
Iterator1つ1つ数え上げる
Mediator相手は相談役1人だけ
Memento状態を保存する
Observer状態の変化を通知する
State状態をクラスとして表現する
Strategyアルゴリズムをごっそり切り替える
Template Method具体的な処理をサブクラスに任せる
Visitor構造を渡り歩きながら仕事する

1. 生成に関するパターン

Abstract Factory

概要

  • 具象クラス(または具象クラスを内包する抽象クラス)抽象クラスを作成し、引数の値によって色々なアウトプットを生成する手法

playground

interface IPokemonProps { name: string; type: string; level: number; } abstract class Pokemon implements IPokemonProps { public name: string; public type: string; public level: number; constructor(props: IPokemonProps) { this.name = props.name; this.type = props.type; this.level = props.level; } abstract attack(): void; } class Pikachu extends Pokemon { constructor (level: IPokemonProps['level']) { super({ name: 'Pikachu', type: 'electric', level }); } private juumanBolt() { console.log(`${10 * this.level} のダメージ`) } attack () { return this.juumanBolt(); } } class PikachuFactory { public static create (): Pikachu { // ピカチュウの独自実装 const level = Math.floor( Math.random() * 5 + 1); return new Pikachu(level); } } class PokemonFactory { public static create(pokemonType: 'pikachu' | 'metamon' | 'polygon') { const pokemonTypes = { pikachu: PikachuFactory.create(), // @ts-ignore metamon: MetamonFactory.create(), // @ts-ignore polygon: PolygonFactory.crate(), } return pokemonTypes[pokemonType] || null; } }

Builder

概要

  • 「作成過程」を決定する Director
  • 「表現形式」を決定する Builder
  • と呼ばれるものを組み合わせることで、オブジェクトの生成をより柔軟にするためのパターン
  • コンストラクタに対して数多くのパラメータをセットする必要がある時に、使うことでオブジェクトの生成を柔軟にする

playground

class Rect { x: number; y: number; width: number; height: number; fill: boolean = false; fillColor: string = "transparent"; lineColor: string = "#000"; lineWidth: number = 1; label: string = ''; labelColor: string = "#000"; constructor(x: number, y: number, width: number, height: number) { this.x = x; this.y = y; this.width = width; this.height = height; } setFillColor = (color: string) => { this.fill = true; this.fillColor = color; }; setLabel = (label: string, labelColor?: string) => { this.label = label; if (labelColor) this.labelColor = labelColor; }; disp = () => { console.log(`position:`, `{${this.x},${this.y}}`); console.log(`fill:`, `${this.fill}`); console.log(`width:`, `${this.width}`); console.log(`height:`, `${this.height}`); console.log(`fill:`, `${this.fill}`); console.log(`fillColor:`, `${this.fillColor}`); console.log(`lineColor:`, `${this.lineColor}`); console.log(`lineWidth:`, `${this.lineWidth}`); console.log(`label:`, `${this.label}`); console.log(`labelColor:`, `${this.labelColor}`); }; } interface IBuilder { init(): void; setInfo(x: number, y: number, width: number, height: number): void; setFillColor(color: string): void; setLabel(label: string): void; getResult(): Rect; } class RedRectBuilder implements IBuilder { private rect: Rect; constructor() { this.rect = new Rect(0, 0, 0, 0); this.init(); } init = () => { this.rect = new Rect(0, 0, 0, 0); this.rect.fill = true; this.rect.fillColor = "red"; }; setInfo = (x: number, y: number, width: number, height: number) => { this.rect.x = x; this.rect.y = y; this.rect.width = width; this.rect.height = height; }; setFillColor = (color: string) => { this.rect.fill = true; this.rect.fillColor = color; }; setLabel = (label: string) => { this.rect.label = label; }; getResult = (): Rect => { const result = this.rect; this.init(); return result; }; } class Director { private _builder: IBuilder; constructor(builder: IBuilder) { this._builder = builder; } construct = () => { this._builder.setInfo(0, 0, 100, 100); }; } const redRectBuilder: RedRectBuilder = new RedRectBuilder(); const director: Director = new Director(redRectBuilder); director.construct(); const result: Rect = redRectBuilder.getResult(); result.disp();

Singleton

概要

  • アプリケーションで唯一のインスタンスのであることを保証するパターンのこと
  • 複数のインスタンスを作ることができないパターン

playground

class MySingleton { private static _instance: MySingleton; // プライベートに閉じることによって、クラス内でのみアクセス可能なコンストラクターになる private constructor() {} public static get getInstance(): MySingleton { if(!this._instance) { console.log('インスタンスを作成するよ') this._instance = new MySingleton(); } else { console.log('既存のインスタンスを返すよ') } return this._instance; } } const instance1 = MySingleton.getInstance; // インスタンスを作成するよ const instance2 = MySingleton.getInstance // 既存のインスタンスを返すよ const instance3 = new MySingleton(); // コンストラクターはプライベートな為コンパイルエラー, Constructor of class 'MySingleton' is private and only accessible within the class declaration.ts(2673)

2. 構造に関するパターン

Adapter

  • adapt => 「適合させる」
  • adapter => 「適合させるもの」
  • インターフェースに互換性のないクラス同士を組み合わせることを目的としたパターン
  • 既存のクラスに対して修正を加えることなくインターフェースを変更する手法
  • 目的を果たすための方法として、継承と委譲の2つがある

継承を用いたパターン

利用したいクラスのサブクラスを作成し、そのサブクラスに対して必要なインタフェースを実装する

interface ICar { startEngine(): void } class Vehicle { go() { console.log('走る') } } class CarAdapter extends Vehicle implements ICar { startEngine() { console.log('エンジン起動完了') } } class Car { carAdapter = new CarAdapter(); prepare() { this.carAdapter.startEngine(); } }

委譲を用いたパターン

利用したいクラスのインスタンスを生成し、そのインスタンスを他クラスから利用する

interface ICar { startEngine(): void } class Vehicle { go() { console.log('走る') } } class CarAdapter implements ICar { private vehicle = new Vehicle(); startEngine() { console.log('エンジン起動完了') } } class Car { carAdapter = new CarAdapter(); prepare() { this.carAdapter.startEngine(); } }

メリデメ

メリット

  • 既存のクラスに手を加えることなく、追加ロジックを実装できる
  • エンバグしても既存のクラスのロジックの正しさが保証されていれば、Adapterが怪しいことになり、調査を早くなる

デメリット

  • 他重継承になる場合が多いため、全体を掴み辛くなる

Bridge

  • 「橋渡し」のクラスを用意することによって、クラスを複数の方向に拡張させることを目的とする。

  • 「機能のクラス階層」

    • 機能を追加する目的で、あるクラスからサブクラスを作った場合の階層のこと(abstract class)
    • 機能追加を目的とした継承
  • 「実装のクラス階層」

    • 親クラスの抽象メソッドを具体的に実装する目的でサブクラスを作った場合の階層のこと(concrete class)
    • 抽象クラスやインターフェースクラスを実装する場合
  • 責務や性質で疎結合なクラスを作りそれを組み合わせて使う

  • Wiki様の説明がわかりやすい

Composite

容器と中身を同一視するとは

  • フォルダ構成をイメージした時に
    • 「容器」がフォルダ(Composite)
    • 「中身」がフォルダの中に内包されたフォルダ or ファイル(Leaf)ということになる
    • 再起的にフォルダが入りえる
    • これらを同一のものとして扱いたいということ(同一視)
  • Composite(容器)の中にComposite(容器)を含めることができる(再起的な処理ができるようになる)

メリット

  • 再起的なデータ構造を扱うことができる
  • 同一視する部分を規定クラスとしておくことでコードの重複を削除できる
  • Compositeは直下のコンポーネントを管理するだけで良くなる

デメリット

  • Compositeクラスでは実装が必要なメソッドであっても、Leafクラスには 実装の必要のないメソッドが、インターフェイスに定義されてしまう Leafクラスは子ノードを保持しないので、LeafクラスのAddメソッドやRemoveメソッドでは例外をスローするなどの考慮が必要になる

Facade

  • Facade正面 という意味
  • 既存のクラスを複数組み合わせて使う手順を、「窓口」となるクラス・関数を作ってシンプルに再利用できるようにするパターン
  • 意識的にこう実装しているパターンは多い

ベース

type User = { name: string; } class UserDB { // ユーザIDからユーザ情報を取得する static fetchUser = (userId: string): User => { // 本来はDBに問い合わせるが簡単のため固定データを返す return { name: 'テスト太郎' }; } } class HeaderBuilder { // タイトルと著者名を出力する static write = (title: string, author: string) => { console.log(`題名:${title}`); console.log(`著者:${author}`); } } class BodyBuilder { // セクションを順番に出力する static write = (sections: string[]) => { sections.forEach(s => console.log(s + '\n')); } }

Facadeパターン適用前

// ユーザ情報を取得 const user: User = UserDB.fetchUser('100'); // ヘッダを記載 HeaderBuilder.write('10月3日の日記', user.name); // 本文を記載 BodyBuilder.write([ 'ファサードパターンのお勉強', '今日はいい天気ですね', 'ところで飯食いに行きません?', ]); /* * 題名:10月3日の日記 * 著者:テスト次郎 * ファサードパターンのお勉強 * 今日はいい天気ですね * ところで飯食いに行きません? */

Facadeパターン適用後

  • ドキュメントを作成する「窓口」を作成する
// ドキュメントを作成処理 class DocumentBuilder { static write = (userId: string, title: string, body: string[]) => { // ユーザ情報を取得 const user: User = UserDB.fetchUser(userId); // ヘッダを記載 HeaderBuilder.write(title, user.name); // 本文を記載 BodyBuilder.write(body); } } DocumentBuilder.write( '100', '10月3日の日記', [ 'ファサードパターンのお勉強', '今日はいい天気ですね', 'ところで飯食いに行きません?', ] );

メリット

  • 結合度が下がる
  • 呼び出し側で制御を記載することがないのでバグが減らせる
  • テスタブル

Flyweight

  • fliweight とは「フライ級」のことで、ボクシングで体重が軽い階級を表す
  • オブジェクトを「軽く」するためのもの
    • 軽い = メモリ使用量を軽くする
  • クラスのインスタンスが数多く必要な時に、インスタンスをたくさん作るのではなく、「できるだけインスタンスを共有させて、無駄に new しない」というもの

サンプル

NG

class Char { private _char: string; constructor(char: string) { this._char = char; } disp() { console.log(this._char); } } const value = 'すもももももももものうち' for(let i = 0; i < value.length; i++) { const char = new Char(value[i]); char.disp(); } /* */

これだと「も」が毎回違うインスタンスとして保持されてしまう

OK

新しく生成したインスタンスはプールと呼ばれる保管庫にセットしておき、そこを生成時に同じものがないかをチェックし、同じものであればプールの中からインスタンスを返すという形に変える

class Char { private _char: string; constructor(char: string) { this._char = char; } disp() { console.log(this._char); } } class CharFactory { private _charMap: Map<string, Char> = new Map(); private static _instance: CharFactory // シングルトンパターンとする private constructor() {} static get instance() { if(!this._instance) { this._instance = new CharFactory() } return this._instance } getChar(char: string) { // プールにない場合はプールにセットする if(!this._charMap.has(char)) { this._charMap.set(char, new Char(char)) } return this._charMap.get(char); } } const value = 'すもももももももものうち' const factory = CharFactory.instance; for(let i = 0; i < value.length; i++) { const char = factory.getChar(value[i]); char.disp(); }

メリット

  • 何万とインスタンスを生成するようなケースで一定のパターン制が発生する場合に置いて、メモリの節約ができる

デメリット

  • 生成されるインスタンスはimmutableでないと生合成が崩れてしまう
  • 普遍的なものが保証されていなくてはいけなく、シングルトンであるため、変更が複数箇所に影響が及ぶ場合がある

Proxy

  • 必要になってから作る
  • proxy とは「代理人」という意味
  • 本人の代わりに何か処理を行う
  • 例えば、インスタンスの初期化に時間がかかるものがあったときに、同じインタフェースを共有したproxy(代理人)から先に値を返す
  • Subject(主体)と Proxy(代理人)

サンプル

(javaのコード を ts で書いてたのだけどjavascriptの言語仕様的に結果の値が期待通りじゃないので概念だけ伝われば...)

// Subject(主体) interface Subject { request(): void } // RealSubject(実際の主体) class RealSubject implements Subject { public request(): void { console.log('RealSubject - request') } } // Proxy(代理人) class Proxy implements Subject { private realSubject: RealSubject constructor(realSubject: RealSubject) { this.realSubject = realSubject } public request(): void { if (this.checkAccess()) { this.realSubject.request() this.logAccess() } } private checkAccess(): boolean { console.log('Proxy - checkAccess') return true } private logAccess(): void { console.log('Proxy - logAccess') } } function clientRequest(subject: Subject) { subject.request() } // Client(依頼人) console.log('Client - subjectでの実行') const realSubject = new RealSubject() clientRequest(realSubject) console.log('') console.log('Client - proxyでの実行') const proxy = new Proxy(realSubject) clientRequest(proxy) /* "Client - subjectでの実行" "RealSubject - request" "---" "Client - proxyでの実行" "Proxy - checkAccess" "RealSubject - request" "Proxy - logAccess" */

登場人物

  • Subject(主体)
    • ProxyとRealSubjectを同一視するためのインターフェースを定める
    • SubjectがあるおかげでClientはProxyとRealSubjectの違いを意識する必要がない
  • Proxy(代理人)
    • clientからの要求を処理する
    • 自分だけでは処理できない時にRealSubjectにタスクを任せる
    • 本当にRealSubjectが必要になった時にRealSubjectを生成する動きをする
  • RealSubject(実際の主体)
    • Proxyで手に負えなくなってきた時に登場するのがRealSubject
  • Client(依頼人)
    • proxyを利用する側

メリット

  • インスタンス生成にコストがかかる場合に、proxyで賄える依頼であればコストをかけずに依頼目的を果たすことができる

デメリット

  • インターフェースを共有しているとはいえ、同じようなロジックが重複してしまうのではと思ってる
  • 複雑性が増す

3. 振る舞いに関するパターン

Chain of Responsibility

概要

  • keyword: たらい回し
  • 特定の処理を担当するオブジェクトまで処理をたらい回す
  • 複数のオブジェクトを鎖のように繋いでおき、そのオブジェクトの鎖を順次わたり歩いて目的のオブジェクトを決定する

chain of responsibility

  • aliceが処理できなかったらbobに回す
  • bobが処理できなかったらjohnに回す
  • 誰か処理できる人がいたらたらい回した人達を通ってmainに向けて結果を返す

役割

  • Handler(処理者)
    • 要求を処理するインターフェースを定める役
    • 自分で処理ができない要求が来たら、次の人(次のhandler)にたらいまわす
  • ConcreteHandler(具体的処理者)
    • 要求を処理する具体的な役
  • Client(要求者)
    • 最初のConcreteHandlerに要求を出す役

コード

interface Handler { setNext(handler: Handler): Handler; handle(request: string): string; } // Handler abstract class AbstractHandler implements Handler { private nextHandler: Handler; // 次に依頼するconcrete handlerをセットする public setNext(handler: Handler): Handler { this.nextHandler = handler; return handler; } // 実際の処理 public handle(request: string): string { if (this.nextHandler) { return this.nextHandler.handle(request); } return null; } } // ConcreteHandler class MonkeyHandler extends AbstractHandler { public handle(request: string): string { if (request === 'Banana') { return `Monkey: I'll eat the ${request}.`; } return super.handle(request); } } // ConcreteHandler class SquirrelHandler extends AbstractHandler { public handle(request: string): string { if (request === 'Nut') { return `Squirrel: I'll eat the ${request}.`; } return super.handle(request); } } // ConcreteHandler class DogHandler extends AbstractHandler { public handle(request: string): string { if (request === 'MeatBall') { return `Dog: I'll eat the ${request}.`; } return super.handle(request); } } // Client function client(handler: Handler) { const foods = ['Nut', 'Banana', 'Cup of coffee']; for (const food of foods) { console.log(`Client: Who wants a ${food}?`); const result = handler.handle(food); if (result) { console.log(` ${result}`); } else { console.log(` ${food} was left untouched.`); } } } const monkey = new MonkeyHandler(); const squirrel = new SquirrelHandler(); const dog = new DogHandler(); monkey.setNext(squirrel).setNext(dog); // 連鎖を作る console.log('Chain: Monkey > Squirrel > Dog\n'); client(monkey); console.log(''); /* Chain: Monkey > Squirrel > Dog Client: Who wants a Nut? Squirrel: I'll eat the Nut. Client: Who wants a Banana? Monkey: I'll eat the Banana. Client: Who wants a Cup of coffee? Cup of coffee was left untouched. */ console.log('Subchain: Squirrel > Dog\n'); client(squirrel); /* Subchain: Squirrel > Dog Client: Who wants a Nut? Squirrel: I'll eat the Nut. Client: Who wants a Banana? Banana was left untouched. Client: Who wants a Cup of coffee? Cup of coffee was left untouched. */

メリット

  • 「要求する側」と「処理する側」の結びつきを弱めることができ、それぞれを部品として独立させることができる
  • チェーンの順番を変えることも可能
  • 各concreteHandlerは自分の仕事に集中でき、concreteHandler内で条件が変わった際も他の処理に影響を与えず、変更がしやすい

デメリット

  • 数々な処理を通るので処理速度が低下する(変更のしやすさとトレードオフ)

Command

概要

  • keyword: 命令
  • 命令を表現するクラスを作り、そこに命令の集まりを管理する
  • 複数の命令をまとめたものを新しい命令として再利用することもできるようになる
  • Eventと呼ばれる場合もある
  • 例えば、レストランのシェフを想像した時に、お客さんから注文が入ります。シェフはウェイターから注文表を受け取ります。その注文表がコマンドとして機能します。シェフはそれを提供する準備ができるまでキューに残ります。注文には、食事を調理するために必要なすべての関連情報が含まれています。これにより、シェフはあなたからの注文の詳細を直接明確にはせずともすぐに料理を始めることができます。

役割

  • Command(命令)
    • 命令のインターフェース
  • ConcreteCommand(具体的命令)
    • Commandのインターフェースを実際に実装している役
  • Receiver(受信者)
    • Commandが命令を実行する時に対象となる役
    • 命令の受け取り手
  • Client(依頼者)
    • ConcreteCommandを生成し、その際にReceiverを割り当てる役
  • Invoker(起動者)
    • 命令の実行を開始する役

コード

// Command interface Command { execute(): void; } // Concrete Command class SimpleCommand implements Command { private payload: string; constructor(payload: string) { this.payload = payload; } public execute(): void { console.log(`SimpleCommand: ${this.payload} の注文が入りました`); } } // Concrete Command class ComplexCommand implements Command { private receiver: Receiver; private stuff: string; constructor(receiver: Receiver, stuff: string) { this.receiver = receiver; this.stuff = stuff; } public execute(): void { console.log('ComplexCommand: 調理中...'); this.receiver.cut(this.stuff); this.receiver.grill(this.stuff); this.receiver.serve(this.stuff); } } // Receiver class Receiver { public cut(stuff: string): void { console.log(`Receiver: ${stuff} をカットする`); } public grill(stuff: string): void { console.log(`Receiver: ${stuff} を焼く`); } public serve(stuff: string): void { console.log(`Receiver: ${stuff} を盛り付ける`); } } // Invoker class Invoker { private order: Command; private cook: Command; public setOrder(command: Command): void { this.order = command; } public setCookingMethod(command: Command): void { this.cook = command; } public start(): void { console.log('Invoker: 注文を受けに行きます'); if (this.isCommand(this.order)) { this.order.execute(); } if (this.isCommand(this.cook)) { this.cook.execute(); } } private isCommand(object): object is Command { return object.execute !== undefined; } } // Client const invoker = new Invoker(); invoker.setOrder(new SimpleCommand('ステーキ')); const receiver = new Receiver(); invoker.setCookingMethod(new ComplexCommand(receiver, '肉')); invoker.start(); /* Invoker: 注文を受けに行きます SimpleCommand: ステーキ の注文が入りました ComplexCommand: 調理中... Receiver: 肉 をカットする Receiver: 肉 を焼く Receiver: 肉 を盛り付ける */

メリット

  • 単一責任の原則に沿った形になる
    • 操作を呼び出すクラスを、これらの操作を実行するクラスから切り離すことができる
  • オープン/クローズド原則に沿った形になる
    • 既存のクライアントコードを壊すことなく、アプリに新しいコマンドを導入できる
    • 例えば、調理方法にFry(揚げる)処理があった時に新しいconcrete commandを作成しそこで調理方法、順番を指定させてあげれば、他のコードに影響を与えずに別の方法を作成できる

デメリット

  • コードが複雑になる

Iterator

概要

  • コレクション(データの集まり)を探索する際にその探索方法を抽象化するパターン
    • ex: 特定の順番で探索させたい
    • ex: 特定の条件(特定のラベルがついたものだけなど)で、探索させたい

iterator

ref: https://refactoring.guru/design-patterns/iterator

役割

  • Itarator(反復子)
    • 要素を順番にスキャンしていくインターフェースを定める役
  • ConcreteIterator(具体的な反復子)
    • Itaratorが定めたインターフェースを実際に実装する役
  • Aggregate(集合体)
    • Iterator役を作り出すインターフェースを定める役
  • ConcreteAggregate(具体的な集合体)
    • Aggregateが定めたインターフェースを実際に実装する役

コード

  • 以下コードは2つの機能を持つ
    • collection を順番に探索させる
    • collection を逆順で探索させる
// Iterator interface IIterator<T> { next(): T; hasNext(): boolean; } // Aggregate interface Aggregate { getIterator(): IIterator<string> } // Concrete Iterator class AlphabeticalOrderIterator implements IIterator<string> { private collection: WordsCollection; private position: number = 0; private reverse: boolean = false; constructor(collection: WordsCollection, reverse: boolean = false) { this.collection = collection; this.reverse = reverse; if (reverse) { this.position = collection.getCount() - 1; } } valid (): boolean { throw new Error("Method not implemented."); } public next(): string { const item = this.collection.getItems()[this.position]; this.position += this.reverse ? -1 : 1; return item; } public hasNext(): boolean { if (this.reverse) { return this.position >= 0; } return this.position < this.collection.getCount(); } } // Concrete Aggregate class WordsCollection implements Aggregate { private items: string[] = []; public getItems(): string[] { return this.items; } public getCount(): number { return this.items.length; } public addItem(item: string): void { this.items.push(item) } public getIterator(): IIterator<string> { return new AlphabeticalOrderIterator(this) } public getReverseIterator(): IIterator<string> { return new AlphabeticalOrderIterator(this, true); } } const collection = new WordsCollection(); collection.addItem('one'); collection.addItem('Two'); collection.addItem('Three'); const iterator = collection.getIterator(); console.log('normal Iterate'); while (iterator.hasNext()) { console.log(iterator.next()); } console.log(''); console.log('reverse Iterate'); const reverseIterator = collection.getReverseIterator(); while (reverseIterator.hasNext()) { console.log(reverseIterator.next()); } /* * normal Iterate * one * two * three * * reverse Iterate * three * two * three */

メリット

  • 単一責任原則が守られる
  • オープン/クローズド原則が守られ、既存のコードを壊さずに新しいコレクションとイテレーターを追加できる

デメリット

  • アプリが単純なコレクションでのみ機能する場合の適用はtoo muchになる可能性がある

所感

  • 要はコレクションを探索や抽出するときは、それ用の役割を別で切り出してそっちに責務は任せろってことだと理解した
  • コレクションの中で一つづつ表示していくUIのときに、VueとかReactだったらhooksや合成関数に分ける時にこんな考え方の書き方すると思う

Mediator

  • keyword: 相談役、仲介役
  • モジュール同士の関係を、仲介役を通して処理を送信 or 受け取ることでモジュール同士の依存関係を減らす設計のこと
  • 例えばそのサービスを知った理由を入力するセレクトボックスがあり、「広告を見て」や「知人からの紹介」などが並んでいたとします。「その他」を選択した場合のみ自由入力のテキストフィールドが表示され入力することができます。こういった処理を実現するのにモジュール同士で連携をするのではなく、仲介役が受けた指示を関係するモジュールに伝えるような流れになる。

役割

  • Mediator(調停者、仲介者)
    • Colleague役と通信を行って調整を行うためのインターフェースを定める役
  • ConcreteMediator(具体的な調停者、仲介者)
    • Mediatorのインターフェースを実装する役
  • Colleague(同僚)
    • ColleagueとはMediatorに仲介を任せる独立したモジュールのこと
      • 上記の例でいう、セレクトボックスやテキストフィールドのこと
    • Mediatorと通信を行うインターフェースを定める
  • CocreteColleague(具体的な同僚)
    • Colleagueのインターフェースを実装する役

コード

type SelectboxValue = '' | '広告を見て' | '知人の紹介' | 'その他'; // Mediator interface Mediator { changeSelectbox(value: SelectboxValue): void; reset(): void; } // ConcreteMediator class ConcreteMediator implements Mediator { private selectbox: Selectbox; private textInput: TextInput; private resetButton: ResetButton; constructor(selectbox: Selectbox, textInput: TextInput, resetButton: ResetButton) { this.selectbox = selectbox; this.selectbox.setMediator(this); this.textInput = textInput; this.textInput.setMediator(this); this.resetButton = resetButton; this.resetButton.setMediator(this); } public changeSelectbox(value: SelectboxValue) { if (value === 'その他') { this.textInput.show(); } else { this.textInput.hide(); } } public reset() { this.selectbox.initialize(); this.textInput.initialize(); } } // Colleague class BaseComponent { protected mediator: Mediator; constructor(mediator: Mediator = null) { this.mediator = mediator; } public setMediator(mediator: Mediator): void { this.mediator = mediator; } } // ConcreteColleague class Selectbox extends BaseComponent { value: SelectboxValue = ''; public change(selectedValue: SelectboxValue): void { this.value = selectedValue; console.log(`selectBoxの値を ${selectedValue} に変更`); this.mediator.changeSelectbox(selectedValue); } public initialize(): void { this.value = ''; console.log('Selectbox が初期化されました'); } } // ConcreteColleague class TextInput extends BaseComponent { isShown: Boolean; public show(): void { this.isShown = true; console.log('TextInput が表示されました'); } public hide(): void { this.isShown = false; console.log('TextInput が非表示になりました'); } public initialize(): void { this.isShown = false; console.log('TextInput が初期化(非表示)されました'); } } class ResetButton extends BaseComponent { public click(): void { console.log('reset ボタンをクリック') this.mediator.reset(); } } // Client(実際の実行) const selectbox = new Selectbox(); const textInput = new TextInput(); const resetButton = new ResetButton(); const mediator = new ConcreteMediator(selectbox, textInput, resetButton); console.log('---パターンA---'); selectbox.change('知人の紹介'); /* * ---パターンA--- * SelectBoxの値を 知人の紹介 に変更 * TextInput が非表示になりました */ console.log('---パターンB---'); selectbox.change('その他'); /* * ---パターンB--- * SelectBoxの値を その他 に変更 * TextInput が表示されました */ console.log('---パターンC---'); resetButton.click(); /* * ---パターンC--- * reset ボタンをクリック * Selectbox が初期化されました * TextInput が初期化(非表示)されました */

メリット

  • 単一責任を守れる
    • 様々なコンポーネント間の通信を1つの場所で管理できる
  • オープンクローズドの原則を守れる
    • 実際のコンポーネントの修正はせず、新しいメディエーターを追加できる
  • 結合度を下げられる
    • コンポーネントは他のコンポーネント事情を知らなくて良くなるため結合度が低くなる
  • 再利用がしやすくなる
    • コンポーネント自身には自らの振る舞いしか持たないため再利用がしやすくなる

デメリット

  • 時間の経過とともにメディエーターが肥大化する

Memento

概要

  • 編集に対してundoやredoなど変更の履歴を保存する必要がある際にmementoクラスにそういった情報をまとめていき実現する手法

役割

  • Originator(作成者)
    • 自分の現在の状態を保存したい時にMemento役を作る
    • 以前のMementoが渡されるとそのMementoの状態に戻る処理を行う
  • Memento(記念品)
    • Originatorの状態をまとめる役
    • その情報は誰にも公開するわけではない
    • 2種類のインターフェースに分かれる
      • Wide Interface
        • オブジェクトの状態を元に戻すための必要な情報が得られるメソッドの集合
        • Mementoの内部情報をさらけ出す
      • Narrow Interface
        • 外部のCaretakerに見せるもの
        • 内部状態が外部に公開されるのを防ぐ
  • Caretaker(世話をする役)
    • 現在のOriginator役の状態を保存したい旨をOriginatorに伝える役
    • Originatorはそれを受けてMementoを作りCaretakerに渡す
    • CaretackerはそのMemento役を保存する
    • アンドゥや履歴の参照など、保管している履歴に対するアクションを定義する

コード

// Originatorは履歴の管理をしない class Originator { private state: string; constructor(state: string) { this.state = state; console.log(`Originator: 初期の状態は ${state} です`); } public changeState(value: string): void { this.state = value; console.log(`Originator: 状態が ${this.state} に変更されました`); } public save(): Memento { return new ConcreteMemento(this.state); } public restore(memento: Memento): void { this.state = memento.getState(); console.log(`Originator: 状態が ${this.state} にリストアされました`); } } interface Memento { getState(): string; getName(): string; getDate(): string; } // 1つの履歴に対しての情報、振る舞いを持つ class ConcreteMemento implements Memento { private state: string; private date: string; constructor(state: string) { this.state = state; this.date = new Date().toISOString().slice(0, 19).replace('T', ' '); } public getState(): string { return this.state; } public getName(): string { return `${this.state.substr(0, 9)} - ${this.date}`; } public getDate(): string { return this.date; } } // 履歴(memento)の保管と管理 // どういった履歴を Originator に伝えるかのロジックを持つ class Caretaker { private mementos: Memento[] = []; private originator: Originator; constructor(originator: Originator) { this.originator = originator; } public backup(): void { console.log('Caretaker: 現在の状態を保存します'); this.mementos.push(this.originator.save()); } public undo(): void { if (!this.mementos.length) { return; } const memento = this.mementos.pop(); console.log(`Caretaker: ${memento.getName()} を保存します`); this.originator.restore(memento); } public showHistory(): void { console.log('Caretaker: 履歴を表示します'); for (const memento of this.mementos) { console.log(memento.getName()); } } } // Client const originator = new Originator('one'); const caretaker = new Caretaker(originator); // Originator: 初期の状態は one です caretaker.backup(); originator.changeState('two'); // Caretaker: 現在の状態を保存します // Originator: 状態が two に変更されました caretaker.backup(); originator.changeState('three'); // Caretaker: 現在の状態を保存します // Originator: 状態が three に変更されました caretaker.backup(); originator.changeState('four'); // Caretaker: 現在の状態を保存します // Originator: 状態が four に変更されました caretaker.showHistory(); // Caretaker: 履歴を表示します // one - 2022-02-13 10:07:47 // two - 2022-02-13 10:07:47 // three - 2022-02-13 10:07:47 console.log('Client: アンドゥします(1回目)'); caretaker.undo(); // Client: アンドゥします(1回目) // Caretaker: three - 2022-02-13 10:07:47 を保存します // Originator: 状態が three にリストアされました console.log('Client: アンドゥします(2回目)'); caretaker.undo(); // Client: アンドゥします(2回目) // Caretaker: two - 2022-02-13 10:07:47 を保存します // Originator: 状態が two にリストアされました

メリット

  • Caretakerに履歴の責務を持たせることによって、Originatorが単純化できる
  • 履歴操作周りのロジックに対して、Originatorには手を加えず変更ができる

デメリット

  • クライアントがメモリを頻繁に作成する場合、アプリは大量のメモリを消費する可能性がある

所感

  • 仕事でページ遷移時の履歴を残し、変更がされた時だけ離脱時にアラート出したい。みたいな事が要求されることがあったので、勉強になった
    • ちなみにそのときは同じstateにぶち込んでいたが、確かに履歴関係は別で分けた方が管理しやすいし、Originator側も意識しないで良くなるなと

Observer

概要

  • keyword: 観察者
  • 一部のモジュールが他のモジュールの状態の変化について通知できるようにするパターン

役割

  • Subject(被験者)
    • インターフェース
    • 観察される側
    • 観察者であるObserverを登録するメソッドと削除するメソッドを持っている
    • また「現在の状態を取得する」メソッドも持っている
  • ConcreteSubject(具体的な被験者)
    • 具体的なSubjectを実装する側
  • Observer(観察者)
    • インターフェース
    • Subjectから状態の変化を教えてもらう
  • ConcreteObserver(具体的な被験者)
    • 具体的なObserverを実装する役

コード

  1. Observer群をSubjectモジュールに追加していく
  • ex: SubjectにObserver A と Observer B を登録する
  1. Subjectの状態変更時に各Observerに変更を毎回知らせる
  2. Observerは受け取った変更内容を見て、何かをする or しないを選択していく

↓はPullRequestdでラベルの変更によって通知をさせたいパターンの時

// Subject interface Subject { attach(observer: Observer): void; detach(observer: Observer): void; notify(): void; } type Label = '' | 'In review' | 'Request changes' | 'Approved'; // ConcreteSubject class PullRequest implements Subject { public label: Label = ''; private observers: Observer[] = []; // --- Observerに関係するロジック --- public attach(observer: Observer): void { const isExist = this.observers.includes(observer); if (isExist) { return console.log('Subject: そのオブザーバーはもう登録されています'); } this.observers.push(observer); console.log('Subject: オブザーバーを登録しました') } public detach(observer: Observer): void { const observerIndex = this.observers.indexOf(observer); if (observerIndex === -1) { return console.log('Subject: そのオブザーバーは登録されていません'); } this.observers.splice(observerIndex, 1); console.log('Subject: オブザーバーの登録を外しました') } public notify(): void { for (const observer of this.observers) { observer.update(this); } } // ------ // --- 純粋なアプリケーションロジック --- public changeLabel(label: Label): void { this.label = label; this.notify(); } } // Observer interface Observer { update(subject: Subject): void; } // ConcreteObserver class requestChangeLabelSubscriber implements Observer { public update(subject: Subject): void { if (subject instanceof PullRequest && subject.label === 'Request changes') { console.log('requestChangeLabelSubscriber: レビューが返されたよ!'); } } } // Client const subject = new PullRequest(); const observer = new requestChangeLabelSubscriber(); subject.attach(observer); console.log('Client: レビューします') subject.changeLabel('In review') console.log('Client: レビュー返しました'); subject.changeLabel('Request changes'); /* Subject: オブザーバーを登録しました Client: レビューします Client: レビュー返しました requestChangeLabelSubscriber: レビューが返されたよ! */

メリット

  • オープン/クローズド原則を保てる。Subjectのコードを変更せず新しいObserverを導入できる

デメリット

  • Observerの実行順序をコントロールがかなり難しい

State

概要

  • 状態をクラスで表現する
  • 状態クラスを差し替えることによって、各処理の分岐をなくすことができる
  • 'member' | 'manager' | 'ceo' と権限があったときに switch文で各処理を切り替えるのではなく、その処理の内容を各権限のものに丸ごと変えてしまうイメージ

役割

  • State(状態)
    • 状態ごとに振る舞いをインターフェースに定める
    • 状態に依存した振る舞いの集まり
  • ConcreteState(具体的な状態)
    • Stateで定めたインターフェースを実際に実装する役
  • Context(状況、前後関係、文脈)
    • 現在の状態を表すConcreteStateを持つ

コード

  • APIドキュメントを題材に
    • 権限が「Admin」と「Member」の2つ
    • 編集できるのは「Admin」のみ
    • 閲覧はどちらもできる
// Context class APIDoc { private state: State; constructor(state: State) { this.transitionTo(state); } public transitionTo(state: State): void { console.log(`Context: 権限を ${(<any>state).constructor.name} に変更しました`); this.state = state; } public render(): void { if (this.state.canView()) { console.log('Context: Docを表示') } else { console.log('Context: あなたの権限ではDocを表示できません') } } public change(text: string): void { if (this.state.canEdit()) { console.log(`Context: テキスト「${text}」を追加`) } else { console.log('Context: あなたの権限ではDocを表示できません') } } } // State abstract class State { protected context: APIDoc; public abstract canView(): boolean; public abstract canEdit(): boolean; } // ConcreteState class Member extends State { public canView() { return true; } public canEdit() { return false; } } // ConcreteState class Admin extends State { public canView() { return true; } public canEdit() { return true; } } // Client console.log('--- Member権限で実行 ---') const context = new APIDoc(new Member()); context.render(); context.change('hoge'); console.log('--- Admin権限で実行 ---') context.transitionTo(new Admin()) context.render(); context.change('hoge'); /* --- Member権限で実行 --- Context: 権限を Member に変更しました Context: Docを表示 Context: あなたの権限ではDocを表示できません --- Admin権限で実行 --- Context: 権限を Admin に変更しました Context: Docを表示 Context: テキスト「hoge」を追加 */

メリット

  • 単一責任の原則を保てる
  • オープン/クローズド原則を保てる
    • 既存のStateクラスやコンテキストを変更せずに、新しい状態を導入できる
  • 条件分岐を少なくでき、コンテキストのコードをシンプルに保てる

デメリット

  • 状態が少ない場合はオーバーエンジニアリングになる可能性がある

参考