Closed19

フロントエンドのデザインパターンを読んでまとめてみる

high-ghigh-g

訳者序

Learning Patterns の日本語訳
フロントエンドに限った話ではないが、JSやReactを用いたサンプルが多いことから「フロントエンドのデザインパターン」としている。

high-ghigh-g

はじめに

デザインパターンは、ソフトウェア設計において繰り返し現れる問題に対する典型的解決策となるもの。
ソフトウェア開発の基礎、概念。

FacebookのJavaScriptライブラリであるReactは、過去5年間で大きな支持を集め、競合のJavaScriptライブラリと比較して、現在NPMで最もダウンロードされているフレームワークとなっている。

Reactの人気により、デザインパターンも修正・最適化されていっている。
React Hooksの導入のおかげで、従来のデザインパターンの多くを置き換えることができた。

現代のWeb開発には、多くの種類のパターンがある。
ここからは、ES2015+ を使った一般的なデザインパターン実装、React特有のデザインパターン実装をメリデメを踏まえた説明になる。

high-ghigh-g

シングルトンパターン

シングルトンは、一度だけインスタンス化でき、グローバルにアクセスできるようなクラスのこと。
アプリケーションのグローバルな状態を管理するのに適している。

以下の様なクラスがあった場合、何度もインスタンス化できるため、これはシングルトンではない。

let counter = 0

class Counter {
  getInstance() {
    return this
  }

  getCount() {
    return counter
  }

  increment() {
    return counter++
  }

  decrement() {
    return counter--
  }
}

new メソッドがニ度呼ばれたとしても、参照される値は一意であるべき。

let instance
let counter = 0

class Counter {
  constructor() {
    if (instance) {
      throw new Error('one instance!')
    }
    instance = this
  }

  getInstance() {
    return this
  }

  getCount() {
    return counter
  }

  increment() {
    return counter++
  }

  decrement() {
    return counter--
  }
}

上記のコードで、Counterクラスのインスタンスをexportすればok。
ただし、インスタンスを凍結する必要がある。
そのために Object.freeze を利用する。
これを利用することで、値を変更できない凍結状態を作ることが出来る。

const singletonCounter = Object.freeze(new Counter())
export default singletonCounter

※Object.freezeについて、下記のように凍結したオブジェクトがあった場合、

const student = Object.freeze({
  id: "0001",
  name: "山田",
  age: 18,
  // こちらを追加
  address: {
    preference: "Tokyo",
    block: "Akabane",
  },
});

以下の様に書き換えても

student.id = '0002'
console.log(student)

// 以下の様に表示される
{
    "id": "0001",
    "name": "山田",
    "age": 18,
    "address": {
        "preference": "Tokyo",
        "block": "Akabane"
    }
}

こうすることで、誤ってシングルトンを上書きする心配がなくなる。

利点と欠点

インスタンスを一度だけに限定することで、メモリ容量を大幅に削減できる可能性がある。
しかし、シングルトンは実際にはアンチパターンと考えられており、JSでは避けることが出来る。
上記のClassを用いたシングルトンはJSではやりすぎな実装。
JSの場合、オブジェクトを直接作成することが出来る為、クラスを利用しなくても、
Object.freeze() + object で再現することが可能。

シングルトンに依存するコードのテストは厄介な場合がある。
新しいインスタンスを作ることが出来ないため、すべてのテストは直前のグローバルインスタンスの変更に依存する。
そのため、ちょっとした修正でもテスト全体が失敗する可能性があるため、テスト終了後はインスタンス全体をリセットする必要がある。

グローバルな動作

シングルトンのインスタンスはアプリケーション全体から参照できる必要がある。
グローバル変数は一般的に悪い設計と考えられている。
誤ってグローバル変数の値を上書きすることで、予期せぬ動作に繋がる可能性がある為。
ES2015において、グローバル変数を作ることはほとんどない。

Reactの場合

シングルトンではなく、Redux, コンテクストなどの状態管理ツールを使ってグローバルな状態を利用することが一般的。
振る舞いはシングルトンと似ているように見えるかもですが、これらのツールはリードオンリーな状態を提供する。
Reduxの場合、コンポーネントがdispatcherを介して、actionを送信し、純粋関数のreducerのみが状態を更新することができる。

これらのツールでグローバルな状態をもつことの欠点が消えるわけではないが、コンポーネントで直接的に値を更新できないようにすることで、クリーンな更新方法でグローバルな状態を保つことが出来る。

high-ghigh-g

プロキシパターン

Proxyオブジェクト

プロキシオブジェクトを使うと、特定のオブジェクトとのやり取りをより自由にコントロールできるようになる。
プロキシオブジェクトは、例えば値の取得、セット、オブジェクトの操作における挙動を決定する。

※プロキシ→代役を意味する

ターゲットとなるオブジェクトを直接操作する代わりにプロキシオブジェクトとやり取りする。
JSの場合Proxyオブジェクトが存在する為、下記のような実装でプロキシオブジェクトを作ることができる。

const person = {
  name: 'John',
  age: 30,
  nationality: 'American',
}

const personProxy = new Proxy(person, {})

プロキシで代表的なものはget, set
Proxyオブジェクトの第二引数にget, setメソッドを指定すると、
プロキシオブジェクトに対して値の参照や追加が行われた際にトラップが実行される。

const personProxy = new Proxy(person, {
  get(obj, prop) {
    console.log(`get property obj: ${obj[prop]}, prop: ${prop}`)
  },
  set(obj, prop, value) {
    console.log(`set property obj: ${obj[prop]}, prop: ${prop}, value: ${value}`)
    obj[prop] = value
  },
})

以下の処理を実行すると、

const person = {
  name: 'John',
  age: 30,
  nationality: 'American',
}

const personProxy = new Proxy(person, {
  get(obj, prop) {
    console.log(`get property obj: ${obj[prop]}, prop: ${prop}`)
  },
  set(obj, prop, value) {
    console.log(`set property obj: ${obj[prop]}, prop: ${prop}, value: ${value}`)
    obj[prop] = value
  },
})

personProxy.name
personProxy.name = 20

結果は以下のように表示される

get property obj: John, prop: name
set property obj: John, prop: name, value: 20

プロキシは、バリデーションを追加する際に便利。

Proxyオブジェクトの各トラップの理解を深めるには以下に目を通す。
https://zenn.dev/yend724/articles/20220915-t0ws2v8agxiaa8gi

Reflectオブジェクト

JSの組み込みオブジェクトであるReflectオブジェクトを利用することで、プロキシオブジェクトを簡単に操作できるようになる。

ReflectオブジェクトはProxyのハンドラーと同じメソッド名を保有している。

// obj[prop]
Reflect.get(obj, prop)

// obj[prop] = value
Reflct.set(obj, prop, value)

Proxyオブジェクトを使いすぎたり、handlerメソッドを呼び出すたびに重い処理を実行したりすると、アプリケーションのパフォーマンスに悪影響を及ぼす可能性がある。
パフォーマンスが重要なコードにはプロキシを使用しないのが一番。

バリデーションとか局所的に使う感じかな。
セッター時に複雑な処理が入る場合は、プロキシパターンが有効かも。

high-ghigh-g

プロバイダパターン

アプリケーション内の多くのコンポーネントからデータを利用できるようにしたい場合、propsを使用してコンポーネントにデータを渡すことは出来るが、アプリケーション内のすべてのコンポーネントがそのpropsの値にアクセスするのは困難に近い。

コンポーネントツリーのずっと下の方にバケツリレー(prop drilling)で渡していくこともできるが、propsに依存するコードのリファクタも難しく、どこから来たpropsなのかを把握することもが難しい。

prop drillingなしで各レイヤーのコンポーネントから直接propsを参照できるとコードがシンプルになってよい。
ここでプロバイダパターンが役に立つ。

Reactだと、ContextオブジェクトやRedux, Recoil, Jotaiなどがこれにあたる。

プロバイダパターンは、グローバルなデータ共有に非常に有効。
また、UIテーマに関するステートを多くのコンポーネントで共有することが挙げられる。

Reactの場合、プロバイダーパターンのロジックを各コンポーネントに記述しなくてもいいようにカスタムフックをプロキシ的に利用し、各コンポーネントにはコンテクスト周辺のロジックが介在しない様にすると関心の分離ができ、再利用性が向上し、コードもシンプルになる。

しかし、プロバイダーパターンを利用しすぎると、特定のパターンでパフォーマンスの問題が発生する。
例えば、グローバルステートと各コンポーネントのレンダリングが紐づいている場合など。
小規模アプリケーションでは、そこまで問題にならないが、大規模アプリケーションでは問題になるケースがあるので、個別のユースケースごとにプロバイダを作成するのが良い。

high-ghigh-g

プロトタイプパターン

複数の同じ型のオブジェクト間でプロパティを共有する為に便利な方法
プロトタイプはJSのネイティブオブジェクトであり、プロトタイプチェーンでオブジェクトからアクセス可能

アプリケーション内で同じ方のオブジェクトを複数作る場合、
シンプルに考えるなら欲しい分だけnewでインスタンスを作ればいい。

class Dog {
  constructor(name) {
    this.name = name
  }

  bark() {
    console.log('woof woof')
  }
}

const dog1 = new Dog('Rex1')
const dog2 = new Dog('Rex2')
const dog3 = new Dog('Rex3')

JSでクラスを扱う場合、 prototype というプロパティが自動的に追加される。
prototypeは、コンストラクタのprototypeプロパティ、もしくは、インスタンスの__proto__プロパティで参照可能

console.log(Dog.prototype)
console.log(dog1.__proto__)

__proto__はコンストラクタのprototypeへの参照になる

その為、インスタンス作成後にコンストラクタのprototypeへ新しいプロパティやメソッドを追加すると、
作成済みのインスタンスへその内容を反映することができる。

const dog1 = new Dog('Rex1')
Dog.prototype.play = () => console.log('playyyyy')

dog1.play()

継承

Dogクラスを継承したSuperDogクラスの場合、
SuperDogのインスタンスの__proto__に入っているのは、Dog.prototypeの参照

class SuperDog extends Dog {
  constructor(name) {
    super(name)
  }

  fly() {
    console.log('flyyyyy')
  }
}

const superdog1 = new SuperDog('SuperRex1')
superdog1.bark()

prototypeチェーンと呼ばれる由来はここから

Object.create

Object.createメソッドは、オブジェクトのシャローコピーを行うメソッド。
新しいオブジェクトを作成し、明示的にプロトタイプの値を渡すことができる。
今まではclassをnewすることで、インスタンスを作成していたが、
こちらの方がシンプルで、いかにもJSらしいインスタンスの作成方法である。

const dog = {
  bark() {
    return 'woof woof'
  },
}

const pet1 = Object.create(dog)

console.log(pet1.bark())

プロトタイプパターンは、あるオブジェクトが他のオブジェクトのプロパティにアクセスしたり、それを継承したりすることを容易に可能とする。
プロトタイプを通してメソッドやプロパティにアクセスできる為、定義の重複を避け、使用メモリを削減することができる。

high-ghigh-g

コンテナ・プレゼンテーションパターン

Reactにおいて、関心の分離を実現する方法の一つとして、コンテナ・プレゼンテーションパターンが存在する。
※ReactHooksが存在しなかった時代の知識

プレゼンテーションコンポーネント

propsでデータを受け取り、それをデータを変更することなく、スタイルを含めて意図通りに表示する為だけのコンポーネント。
自身のステートは持たない。

↓こんなレベル感

import React from "react";

export default function DogImages({ dogs }) {
  return dogs.map((dog, i) => <img src={dog} key={i} alt="Dog" />);
}

プレゼンテーションコンポーネントは、コンテナコンポーネントからデータを受け取る。

コンテナコンポーネント

自身が内包しているプレゼンテーションコンポーネントに対し、データを受け渡しすることが目的。
コンテナコンポーネント自体は何もレンダリングを行うことがないため、通常はスタイルは持たない。

import React from "react";
import DogImages from "./DogImages";

export default class DogImagesContainer extends React.Component {
  constructor() {
    super();
    this.state = {
      dogs: []
    };
  }

  componentDidMount() {
    fetch("https://dog.ceo/api/breed/labrador/images/random/6")
      .then(res => res.json())
      .then(({ message }) => this.setState({ dogs: message }));
  }

  render() {
    return <DogImages dogs={this.state.dogs} />;
  }
}

コンテナコンポーネントとプレゼンテーションコンポーネントを組み合わせることでアプリケーションのロジックとビューを分離することができる。

フック

上記で触れた内容は多くの場合、ReactHooksで置き換える事ができる。
カスタムフックでロジック部分を切り出し、プレゼンテーションコンポーネントに当たるコンポーネントでフックをimportすればよいだけ。

フックを利用すれば、コンテナコンポーネントという余分なレイヤーを省くことができる。

関心の分離という点では、参考にできるデザインパターン。
小規模なアプリケーションでは複雑になりがちなので、不要になりがちなパターン。

high-ghigh-g

オブザーバパターン

あるオブジェクト(Observer)を別のオブジェクト(Observable)にSubscribeすることができる。
イベントが発生すると、Observableは自身のObserverに通知します。

Observableオブジェクトは通常3つの重要パーツから構成される

  • Observers → 特定のイベントが発生するたびに通知を受けるObserver配列
  • subscribe(), unsubscribe() → Observerのリストに追加・削除するメソッド
  • notify() → 特定イベント発生時にすべてのObserverに通知するメソッド

Observerクラス

class Observable {
  constructor() {
    this.observers = []
  }

  subscribe(func) {
    this.observers.push(func)
  }

  unsubscribe(func) {
    this.observers = this.observers.filter((observer) => observer !== func)
  }

  notify(data) {
    this.observers.forEach((observer) => observer(data))
  }
}

const observable = new Observable()


作成した関数をobservableにsubscribeすることで、observersに追加される。
DOMのイベントハンドラ内でnotifyを実行する形にすることで、observersにある関数をすべて実行する事ができる。


const logger = (data) => {
  console.log(`${Date.now()} ${data}`)
}

const toastify = (data) => {
  toast(data, {
    position: toast.POSITION.BOTTOM_RIGHT,
    closeButton: false,
    autoclose: 2000,
  })
}

observable.subscribe(logger)
observable.subscribe(toastify)

const handleClick = () => {
  observable.notify('User clicked button!')
}

const handleToggle = () => {
  observable.notify('User toggled switch!')
}

コンポーネント内では、notifyが記述されたイベントハンドラを各イベントに添えるだけ。

export default function App() {
  return (
    <div className="App">
      <Button onClick={handleClick}>Click</Button>
      <FormControlLabel control={<Switch name="" onChange={handleToggle} />} label="Toggle me!" />
      <ToastContainer />
    </div>
  )
}

↓こういったイメージになる

Observerパターンには様々な利用方法がありますが、非同期のイベントベースのデータを扱うときが便利。
あるデータのダウンロードが完了時に特定のコンポーネントに通したい場合、
メッセージの通知完了時に他のメンバー全員への通知など

オブザーバパターンを使用する人気ライブラリにRxJSがある。

https://rxjs.dev/
https://www.codegrid.net/articles/2017-rxjs-1/
https://zenn.dev/mikakane/articles/rxjs_2_tutorial

high-ghigh-g

モジュールパターン

コードを再利用可能な小さな部品へと分割するパターン。

モジュールはファイル内の特定の値を非公開にすることを可能。
エクスポートしなければ、モジュール外では使えない。
グローバルスコープを利用できなくする為、名前衝突などの危険性を減らすことができる。

ES2015だと、exportで細かく関数などをモジュール内の関数を外部公開することができる。
importでexportされた値や関数を利用することができる。

importしたファイル内でexportした値との命名衝突があった場合は、asキーワードでexportに別名をつける。

モジュールからexportしている値をすべて扱いたい場合は、*を利用しasで別名をつける。

import * as math from './math'

math.add(9)

Reactでは、コンポーネントごとにファイル分割をするが、個々のコンポーネントのモジュールを作成している。

モジュールとコンポーネントの違い

どちらも最小単位の部品という意味で他と組み合わせて利用する。似たような意味。

厳密には以下のような違い
モジュール→それだけで独立しており、単体で利用ができる
コンポーネント→独立はしているが、呼び出される前提のもの

ダイナミックインポート

通常のインポート記述がある場合、初期ロードですべてロードされるが、
ダイナミックインポート野場合は、そのコードが読み込まれて初めてインポートが動作する。
コード例は以下、イベントと組み合わせることで動的なインポートが可能。
ページの初回読み込み時間を短縮することができる。

import("module").then(module => {
  module.default();
  module.namedExport();
});

// Or with async/await
(async () => {
  const module = await import("module");
  module.default();
  module.namedExport();
})();

high-ghigh-g

ミックスインパターン

ミックスインとは、継承を行わずに他のオブジェクトやクラスに再利用可能な機能が追加可能なオブジェクト。
ミックスインは単体で利用することができない。
あくまで、継承無しでオブジェクトやクラスに機能を追加することが目的。

class Dog {
  constructor(name) {
    this.name = name
  }
}

const dugFunctionality = {
  bark: () => console.log('Woof!'),
  wagTail: () => console.log('Wagging my tail!'),
  play: () => console.log('Playing!'),
}

Object.assign(Dog.prototype, dugFunctionality)

const dog = new Dog('Buddy')

dog.bark() // Woof!

JSでミックスインを扱う場合はObject.assignを利用する。

Windowオブジェクトは WindowOrWorkerGlobalScope と WindowEventHandlers ミックスインからそのプロパティの多くを実装しており、これにより setTimeout や setInterval、indexedDB、isSecureContext などのプロパティにアクセスすることができるようになっている。

ES6クラスが導入される前は、Reactコンポーネントへの機能追加にミックスインが利用されていたが、現在は使われていない。
ミックスインはコンポーネントを不要に複雑にしがちで、保守・再利用性を困難にするため。
ReactHooksで置き換えられている。

オブジェクトのプロトタイプを変更することは、プロトタイプ汚染や関数の出所が明らかでなくなる原因となるため、バッドプラクティスと考えられています。

high-ghigh-g

メディエータ・ミドルウェアパターン

メディエータという存在を通してコンポーネント同士がやり取りすることを可能とする。

コンポーネント同士が直接対話するのではなく、メディエータがリクエストを受け取り、それを転送する。
JSの場合、メディエータは単なるオブジェクトリテラルや関数であることが多い。

コンポーネント同士が直接やり取りを行う構造を取ってしまうと、構造がカオスになる。

各コンポーネントやオブジェクトが直接やりとりするのを防ぎ、メディエータを介すことで、やりとりをシンプルにする。

ObserverパターンとMediatorパターンが似ていると感じたので少し調査。

Observerパターンの主眼はあくまでも「 状態変化を通知すること 」にあり、観察対象が何クラスあるかや、それをもとに何か処理をするかどうかはパターンの範囲外でした。
逆に、Mediatorパターンは「 複数のクラスからの通知を元に判断を行い、各クラスへ指示を出すこと 」に主眼があるため、クラスは複数ある前提で、通知を元に何らかの処理を行うところまでがパターンです。Observerパターンは片方向、Mediatorパターンは双方向に通信を行うパターンであるということですね。

これもまたReactHooksでuseEffect, useStateを使ったカスタムフックでうまく吸収できるなと思った。

high-ghigh-g

https://zenn.dev/morinokami/books/learning-patterns-1/viewer/hoc-pattern

HOCパターン

High Order Componentの略。
高階コンポーネント。

同じロジックやスタイルを複数のコンポーネントにおいて使いたいことがよくあるが、そういった共通部分の使い回しの一つの方法としてHOCパターンがある。

HOCは、他のコンポーネントを受け取るコンポーネントのこと。

styled-components的なスタイルの適用もHOC

function withStyles(Component) {
  return props => {
    const style = { padding: '0.2rem', margin: '1rem' }
    return <Component style={style} {...props} />
  }
}

const Button = () = <button>Click me!</button>
const Text = () => <p>Hello World!</p>

const StyledButton = withStyles(Button)
const StyedText = withStyles(Text)

React Hooksが出る前にrecomposeが存在し、HOCを利用してクラスベースのReactをシンプルに書く方法が存在した。

HOCの基本的な考えは今でも利用できるが、利用しすぎると、無駄なラッパー構造が発生するため辛くなる。
React Hooksを利用することで、極力無駄なラッパーコンポーネントをなくすことができる。

https://zenn.dev/kazizi55/scraps/2313391b31a83b

high-ghigh-g

https://zenn.dev/morinokami/books/learning-patterns-1/viewer/render-props-pattern

レンダープロップパターン

propsにコンポーネントを渡し、渡されたコンポーネント側でrenderするパターン
コンポーネントにProxyパターン、コンテナ・プレゼンテーション・パターンを用いる感覚

<Title render={() => <h1>I am a render prop!</h1>} />
import React, { useState } from "react";
import "./styles.css";

function Input() {
  const [value, setValue] = useState("");

  return (
    <input
      type="text"
      value={value}
      onChange={e => setValue(e.target.value)}
      placeholder="Temp in °C"
    />
  );
}

一見 childrenに渡せばいいのでは?と思ったが、メモ化の関係上、レンダープロップを利用する場合があるとのこと。
childrenを利用する場合、あたかもコンポーネントを通常のhtmlのようにラップしているようにも見えるが、これもprops.childrenを利用した一種のレンダープロップパターンである。

メモ化を利用する場合、childrenに渡す値もuseMemoでメモ化しないといけない。
https://azukiazusa.dev/blog/react-memoized-component-children/

いっときはHOCをレンダープロップで置き換えられるほど優秀なものと考えられていたという記事もあるが欠点もある。
こちらも多用しすぎるとネストが深くなる。

HOCやレンダープロップからは、関心の分離をメモ化などをシンプルなコードで実現する方法を考えられていたように感じとれる。

high-ghigh-g

フックパターン

React 16.8からフック(Hook)と呼ばれる新機能が導入された。
このおかげでクラスコンポーネントを利用することなく、関数コンポーネントでステートやライフサイクルが利用できるようになった。

フックはデザインパターンというわけではないが、Reactにおけるアプリケーション設計において非常に重要な役割を果たす。
従来のデザインパターンの多くは、フックによって置き換えることができる。

知っていることが多いため、解説を省略。

high-ghigh-g

https://zenn.dev/morinokami/books/learning-patterns-1/viewer/flyweight-pattern

フライウェイトパターン

類似オブジェクトを大量に作るときにメモリを節約するための便利な方法。

下記のクラスがあったとする。
図書館を考えた場合、本は1冊だけでなく、同じ本が複数冊ある。

全く同じ本が複数冊ある場合、毎回新しい本のインスタンスを作成するのはあまり意味がないため、
存在の判定処理を行い、インスタンスが存在する場合は、既存のインスタンスを利用し、
インスタンスが存在しない場合にだけ、新しくインスタンスを作成するようにする。

現在では、ハードウェアはGB単位のRAMを持っているため、Flyweightパターンの重要性は低くなっている。

class Book {
  constructor(title, author, isbn) {
    this.title = title
    this.author = author
    this.isbn = isbn
  }
}

const isbnNumbers = new Set()
const bookList = []

const addBook = (title, author, isbn, availibility, sales) => {
  const book = {
    ...createBook(title, author, isbn),
    sales,
    availibility,
    isbn,
  }

  bookList.push(book)
  return book
}

const createBook = (title, author, isbn) => {
  const book = isbnNumbers.has(isbn)
  if (book) {
    return book
  }

  const bookInstance = new Book(title, author, isbn)
  isbnNumbers.add(isbn)
  return bookInstance
}

addBook('Harry Potter', 'JK Rowling', 'AB123', false, 100)
addBook('Harry Potter', 'JK Rowling', 'AB123', true, 50)
addBook('To Kill a Mockingbird', 'Harper Lee', 'CD345', true, 10)
addBook('To Kill a Mockingbird', 'Harper Lee', 'CD345', false, 20)
addBook('The Great Gatsby', 'F. Scott Fitzgerald', 'EF567', false, 20)

console.log('Total amount of copies: ', bookList.length)
console.log('Total amount of books: ', isbnNumbers.size)

high-ghigh-g

https://zenn.dev/morinokami/books/learning-patterns-1/viewer/factory-pattern

ファクトリパターン

新しいオブジェクトを作成する為にファクトリ関数を使用する方法。
関数がnewを使用せずに新しいオブジェクトを返す場合、その関数はファクトリ関数であるといえる。

const createUser = ({ firstName, lastName, email }) => ({
  firstName,
  lastName,
  email,
  fullName() {
    return `${this.firstName} ${this.lastName}`
  },
})


const user1 = createUser({
  firstName: "John",
  lastName: "Doe",
  email: "john@doe.com"
});

const user2 = createUser({
  firstName: "Jane",
  lastName: "Doe",
  email: "jane@doe.com"
});

JSのFactoryパターンはnewキーワードを使わずにオブジェクトを返す関数のため、記述的にはシンプルだが、メモリ効率を考えた場合は、クラスから新しいインスタンスを作成する方が良い。

high-ghigh-g

https://zenn.dev/morinokami/books/learning-patterns-1/viewer/command-pattern

コマンドパターン

Commanderにコマンドを送信することでタスクを実行するメソッドを分離する。

このパターンは前職で利用されているのを見たことがある。


class OrderManager() {
  constructor() {
    this.orders = []
  }

  placeOrder(order, id) {
    this.orders.push(id)
    return `You have successfully ordered ${order} (${id})`;
  }

  trackOrder(id) {
    return `Your order ${id} will arrive in 20 minutes.`
  }

  cancelOrder(id) {
    this.orders = this.orders.filter(order => order.id !== id)
    return `You have canceled your order ${id}`
  }
}

というクラスがあったとき、利用するときは以下の様になる。

const manager = new OrderManager();

manager.placeOrder("Pad Thai", "1234");
manager.trackOrder("1234");
manager.cancelOrder("1234");

しかし、もしplaceOrderメソッドにメソッド名の変更があった場合、呼び出し箇所も変更しないといけなくなる。

しかし、Commandクラスを作成し、関数実行の際は必ずCommandクラスに実行を追加する方法にすると、処理を疎結合にすることができる。


class OrderManager {
  constructor() {
    this.orders = [];
  }

  execute(command, ...args) {
    return command.execute(this.orders, ...args);
  }
}

class Command {
  constructor(execute) {
    this.execute = execute;
  }
}

function PlaceOrderCommand(order, id) {
  return new Command(orders => {
    orders.push(id);
    return `You have successfully ordered ${order} (${id})`;
  });
}

function CancelOrderCommand(id) {
  return new Command(orders => {
    orders = orders.filter(order => order.id !== id);
    return `You have canceled your order ${id}`;
  });
}

function TrackOrderCommand(id) {
  return new Command(() => `Your order ${id} will arrive in 20 minutes.`);
}

コマンドパターンの利用で、特定の寿命を持つコマンド、キューに入れられ実行される様なコマンドを扱う場合など、細かい制御を可能とする。

このスクラップは2024/04/15にクローズされました