LoginSignup
2
7

javascript デザインパターン

Last updated at Posted at 2023-11-19

参考にしたデザインパターン著書

Patterns.dev

シングルトンパターン

シングルトン (singleton) は、一度だけインスタンス化でき、グローバルにアクセスできるようなクラスのことです。この_単一のインスタンス_をアプリケーション全体で共有できることから、シングルトンはアプリケーションのグローバルな状態を管理するのに適しています。

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

上記のパターンだと、シングルトンパターンの条件を満たしておりません。
理由としては、↓

const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); 

// false

インスタンスが複数作成できてしまうからです。
ここからインスタンスを一つのみ生成できるようにするためにはinstanceという変数を生成してCounterというコンストラクタで新しいインスタンスが生成されたらinstanceという変数にそのインスタンスの参照をセットすることで新しいインスタンスが生成されないようにします。

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

さらに元のインスタンスが誤って変更されないように、Object.freezeを使って変更できないようにすることも大切です。

インスタンス化を一度に限定することで、使用されるメモリ容量を大幅に削減できる可能性があります。新しいインスタンスのためにメモリを毎回確保するのではなく、アプリケーション全体で参照される 1 つのインスタンスのためにメモリを確保すればよいからです。しかし、シングルトンは実際にはアンチパターンと考えられており、JavaScript ではこれを避けることができます (というよりも、避ける_べき_です)。

Java や C++ などの多くのプログラミング言語では、JavaScript のようにオブジェクトを直接作成することはできません。これらのオブジェクト指向プログラミング言語では、まずクラスを作成する必要があり、そのクラスがオブジェクトを作成します。作成されたオブジェクトは、上記の JavaScript の例における instance のように、クラスのインスタンスの値をもちます。

しかし、上記の例で示したクラスの実装は、実はやり過ぎなのです。JavaScript ではオブジェクトを直接作成できるため、通常のオブジェクトを使用するだけでまったく同じ結果を得ることができます

javascript におけるシングルトンパターンの作成

let count = 0;

const counter = {
  increment() {
    return ++count;
  },
  decrement() {
    return --count;
  }
};

Object.freeze(counter);
export { counter };

上記だけで同じ結果を得られる。オブジェクトを一つ作成するだけで参照先は常に同じになります。

シングルトンのデメリット

  • テスト観点

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

  • 依存関係の隠蔽

他のモジュール (ここでは superCounter.js) をインポートする際、そのモジュールがシングルトンをインポートしていることが明らかではない場合があります。他のファイル、たとえばこの場合は index.js などで、そのモジュールをインポートしてメソッドを呼び出すかもしれません。このようにして、誤ってシングルトンの値を変更してしまうことがあります。アプリケーション全体でシングルトンの複数のインスタンスが共有されており、それらがすべて変更されてしまうことから、予期せぬ挙動をもたらす可能性があります。

グローバルな動作

シングルトンの動作は、アプリケーション全体から参照できるようにする必要があります。
グローバル変数も本質的には同じ性質を持つ。

グローバル変数をもつことは、設計における悪い判断であると一般に考えられています。グローバルスコープの汚染は、誤ってグローバル変数の値を上書きしてしまい、その結果多くの予期せぬ動作につながる可能性があるためです。

ES2015において、varを使ったグローバル変数の定義は行わないようになっています。

vueにおける状態管理

vuex, piniaなどを使ってグローバルな状態管理を行っています。
この振る舞いはシングルトンと似ているが、シングルトンとは違ってミュータブルな値ではなくreadonlyな値になっています。また値を変更する際には、mutationを記載する必要がありその辺もシングルトンとは異なる。

これらのツールを使うことで、グローバルな状態をもつことの欠点が魔法のように消えるわけではありませんが、コンポーネントが状態を直接更新できないことにより、少なくともグローバルな状態が意図したとおりに変更されるようにできるのです。

proxyパターン

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

jsにおけるproxyパターン

jsにおけるプロキシの作成は、Proxyを呼び出すだけで作成することができる。
第二引数に、プロキシのハンドラに追加するメソッドを入れる。代表的なものは(get, set)

const person = {
	name: 'John Doe',
	age: 42,
	nationality: 'american'
}

const personProxy = new Proxy(person, { get: (obj, prop) => { console.log(`The value of ${prop} is ${obj[prop]}`); }, set: (obj, prop, value) => { console.log(`Changed ${prop} from ${obj[prop]} to ${value}`); obj[prop] = value; } });


![[Pasted image 20231119183521.png]]

proxyは、データ変更に対するバリデーションの作成などに便利

javascriptには、Proxyの変数を作るのにReflectという組み込みオブジェクトがある。

const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    console.log(`The value of ${prop} is ${Reflect.get(obj, prop)}`);
  },
  set: (obj, prop, value) => {
    console.log(`Changed ${prop} from ${obj[prop]} to ${value}`);
    Reflect.set(obj, prop, value);
  }
});

proxyは、オブジェクトの振る舞いを制御するための強力な手段となります。
バリーデーション、書式設定、通知、デバックなど様々なユースケースがあります。

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

プロバイダパターン

プロジェクト全体からは、アクセスをする必要がないが複数コンポーネントから値を参照したい場合のデザインパターン

全てpropsで管理してしまうとpropsリレーが発生してしまい可読性・運用保守性にかけるコードになってしまうため reactで言えばuseContext,vueで言えば provide, injectを使用してpropsリレーを避けるようにする

プロトタイプパターン

プロトタイプパターンは、複数の同じ方のオブジェクト間でプロパティを共有するために便利な方法

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

  bark() {
    return `Woof!`;
  }
}

const dog1 = new Dog("Daisy");
const dog2 = new Dog("Max");
const dog3 = new Dog("Spot");

dogクラスに対して新しいメソッドを追加してあげる

Dog.prototype.play = () => console.log('playing now')

既存のDogクラスを継承してさらにメソッドを追加してあげる

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

  fly() {
    return "Flying!";
  }
}

オブジェクトから直接利用できないプロパティにアクセスしようとすると__proto__が指すすべてのオブジェクトをそのプロパティが見つかるまで再起的にたどって実行する

const superDog = new superDog('john')`

superDog.bark()
┗ 'Woof'

Object.create(クラス名)を使用すると明示的にオブジェクトにプロトタイプを渡すことができる

const pet1 = Object.create(dog);

義されていないプロパティにアクセスできるため、メソッドやプロパティの重複を避け、使用するメモリ容量を削減することができます。

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

関心の分離を実現する方法の一つとして、コンテナプレゼンテーションパターンがあります。
このパターンを取り入れることでビューをアプリケーションロジックから切り離すことができる

EX. 犬の画像をユーザーにランダムに表示するアプリケーションの作成

上記のアプリをビューをアプリケーションロジックから切り離すと

  1. プレゼンテーションコンポーネント
    1. データがユーザーにどのように表示されるかを管理するコンポーネント上記の例で言えば犬の画像のリストをレンダリングすること
  2. コンテナコンポーネント
    1. 何の画像がユーザーに表示されるかを管理するコンポーネント上記の例で言えばAPIコールの部分を担当するコンポーネント

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

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} />;
  }
}

上記のように実装を行えばビューとロジックを切り離すことができるようになります。

多くの場合コンテナ・プレゼンテーションパターンはReact hooksに置き換えることができる。
hooksの導入によって開発者はステートを提供するコンテナコンポーネントを必要とせずに簡単にアプリケーションをステートフルにすることができるようになった

下記がhooksに置き換えた例↓

hooks

export default function useDogImages() {
  const [dogs, setDogs] = useState([]);

  useEffect(() => {
    fetch("https://dog.ceo/api/breed/labrador/images/random/6")
      .then(res => res.json())
      .then(({ message }) => setDogs(message));
  }, []);

  return dogs;
}

view

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

export default function DogImages() {
  const dogs = useDogImages();

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

まとめるとコンテナ・プレゼンテーションパターンを取り入れると関心の分離を促進することができプレゼンテーションコンポーネントはUIを担当する純粋関数となりコンテナコンポーネントはアプリケーションのステートやデータを担当することができる。

UIとロジックを完全に分離することによってデザイナーのようなコードベースの知識がない人でも簡単にプレゼンテーションコンポーネントの外観を変更することができる

またプレゼンテーションコンポーネントは単純関数になるのでテストを行うことが容易になる。
入力となるコンポーネントから何をレンダリングするのかを知ることができデータストアをモックする必要がない

オブサーバーパターン

オブザーバーパターンはあるオブジェクトのObserverを別のオブジェクトObserverに登録することができるイベントが発生するとObservableは自分のobserverに通知します。

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

  • Observers
    • 特定のイベントが発生するたびに通知を受け取る通知の配列
  • subscribe()
    • 通知を通知のリストに格納するためのメソッド
  • unsubscribe()
    • 通知の配列から通知を削除するメソッド
  • notify()
    • 特定のイベントが発生した時にすべての変数に通知するメソッド

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));
  }
}

オブザーバパターンにはさまざまな使用方法がありますが、非同期のイベントベースのデータを扱うときに非常に便利です。たとえば、あるデータのダウンロードが終了したときに特定のコンポーネントに通知したい場合や、ユーザーが掲示板に新しいメッセージを送ったときに他のメンバー全員に通知したい場合などが考えられます。

オブザーバパターンは、関心の分離と単一責任の原則を実現するための素晴らしい方法です。Observer オブジェクトは Observable オブジェクトと密結合しておらず、いつでも結合 (あるいは疎結合化) することができます。Observable オブジェクトはイベントの監視に責任をもつのに対し、Observer は受け取ったデータを処理するだけとなります。

モジュールパターン

モジュールパターン (module pattern) は、コードを再利用可能な小さな部品へと分割することを可能にします。

モジュールパターンは、アトミックデザインとか$utilの切り出しに似ていると思います。

moduleとして切り出した関数

const privateValue = "This is a value private to the module!";

export function add(x, y) {
  return x + y;
}

export function multiply(x) {
  return x * 2;
}

export function subtract(x, y) {
  return x - y;
}

export function square(x) {
  return x * x;
}

importして利用する

const { add, multiply } from ~~~

ミックスインパターン

ミックスイン (mixin) は、継承をおこなわずに他のオブジェクトやクラスに再利用可能な機能を追加することができるオブジェクトです。ミックスインを単独で使用することはできません。ミックスインの唯一の目的は、継承を使用せずにオブジェクトやクラスに_機能を追加する_ことだからです。

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

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

Object.assign(Dog.prototype, dogFunctionality);

上記の例では、dogFunctionlalityというミックスインを用いしてDogクラスに対して機能追加を行なっている。ミックスイン自体に値を付与することも可能

ミックスインは、オブジェクトのプロトタイプに機能を注入することで、継承をおこなわずにオブジェクトに機能を追加することを可能にします。オブジェクトのプロトタイプを変更することは、プロトタイプ汚染や関数の出所が明らかでなくなる原因となるため、バッドプラクティスと考えられています。

Reactでも非推奨とされている

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

メディエータパターン (mediator pattern) は、中央にあるメディエータという存在を通してコンポーネントどうしがやり取りすることを可能にします。コンポーネントどうしが直接対話するのではなく、メディエータがリクエストを受け取り、それを転送します。JavaScript では、メディエータは単なるオブジェクトリテラルや関数であることが多いです。

つまり、オブジェクトなどの状態を持った値を変更するときにオブジェクトからオブジェクトを変更するのではなく、middlewareや関数などを通して値を変更することによってオブジェクト同士の依存関係やリファクタリングのしやすさの向上を測ることができ流ようになることです

image.png

メディエータパターンは、すべての通信が中央のメディエータを経由して流れるようにすることで、オブジェクト間の多対多の関係を単純化することができます。

HOCパターン

アプリケーションの中で、同じロジックを複数のコンポーネントにおいて使いたいことがよくあります。このロジックには、コンポーネントに特定のスタイルを適用すること、認可を要求すること、グローバルな状態を追加することなどが含まれます。

複数のコンポーネントで同じロジックを再利用する方法の 1 つとして HOC パターンがあります。

高階コンポーネント (HOC、Higher Order Component) は、他のコンポーネントを受け取るコンポーネントです。

例えば、Loading処理など共通して使用することができるコンポーネントなどをHOCとしてまとめて渡す

HOCはただ単なるコンポーネントではなくロジック付きのコンポーネントになっている

import React, { useEffect, useState } from "react";

export default function withLoader(Element, url) {
  return (props) => {
    const [data, setData] = useState(null);

    useEffect(() => {
      async function getData() {
        const res = await fetch(url);
        const data = await res.json();
        setData(data);
      }

      getData();
    }, []);

    if (!data) {
      return <div>Loading...</div>;
    }

    return <Element {...props} data={data} />;
  };
}

ロジック付きのコンポーネントを作ると上記のようになる。

高階コンポーネントを使用することで、多くのコンポーネントに同じロジックを提供しながらも、そのロジックをすべて一つの場所で管理することができます。フックにより、コンポーネント内にカスタマイズされた動作を追加することができますが、複数のコンポーネントがこの動作に依存している場合、HOC パターンと比較してバグを生むリスクが高まる可能性があります。

HOC が最適なユースケース:

  • 同一の、_カスタマイズ不要な_ロジックが、アプリケーション全体で多くのコンポーネントによって使われる
  • コンポーネントは、追加のカスタムロジックなしで、独立して動作する

フックが最適なユースケース:

  • 各コンポーネントごとにロジックをカスタマイズしたい
  • ロジックはアプリケーション全体で使われず、1 つまたはいくつかのコンポーネントだけが使用する
  • ロジックによってコンポーネントに多くのプロパティが追加される

HOC パターンを使うと、再利用したいロジックを一カ所にまとめておくことができます。つまり、バグを発生させる可能性のあるコードがアプリケーション内で重複した結果、意図せずしてアプリケーション全体にバグを拡散してしまうようなリスクを減らすことができるのです。また、ロジックを一箇所にまとめることは、DRY なコードを維持し、関心の分離を実現しやすくすることにもつながります。

hookパターン

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

hookが使えるようになるまでは、クラスコンポーネントがステートやライフサイクルメソッドを扱うことができる唯一のコンポーネントでした。その結果、機能追加のために、関数コンポーネントをクラスコンポーネントへとリファクタリングしなければならないことがよくありました。

クラスの問題点としては、分離して書くことができないため行数が多くなりやすくリファクタリングが難しくなってしまう問題があった。

フックは、コンポーネントの状態やライフサイクルメソッドを管理するための関数です。フックを使うと、以下のようなことが可能になります。

  • 関数コンポーネントにステートを追加する
  • componentDidMount や componentWillUnmount などのライフサイクルメソッドを使用せずに、コンポーネントのライフサイクルを管理する
  • アプリ内の複数のコンポーネントで同じステートフルなロジックを再利用する

ステートフック(値を更新するhooks)

function Input() {
  const [input, setInput] = React.useState("");

  return <input onChange={(e) => setInput(e.target.value)} value={input} />;
}

副作用hooks(値をwatchしてコールバック関数を実行する)

componentDidMount() { ... }
useEffect(() => { ... }, [])

componentWillUnmount() { ... }
useEffect(() => { return () => { ... } }, [])

componentDidUpdate() { ... }
useEffect(() => { ... })

その他カスタムhookを作成することもできる

hooksを使うメリット
フックを使うと、ライフサイクルではなく、関心事や機能によってコードをグループ化することができます。その結果、コードがすっきりと簡潔になるだけでなく、記述量も少なくなります。

hooksを使わない例(class)

class TweetSearchResults extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: '',
      inThisLocation: false
    };

    this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
    this.handleInThisLocationChange = this.handleInThisLocationChange.bind(this);
  }

  handleFilterTextChange(filterText) {
    this.setState({
      filterText: filterText
    });
  }

  handleInThisLocationChange(inThisLocation) {
    this.setState({
      inThisLocation: inThisLocation
    })
  }

  render() {
    return (
      <div>
        <SearchBar
          filterText={this.state.filterText}
          inThisLocation={this.state.inThisLocation}
          onFilterTextChange={this.handleFilterTextChange}
          onInThisLocationChange={this.handleInThisLocationChange}
        />
        <TweetList
          tweets={this.props.tweets}
          filterText={this.state.filterText}
          inThisLocation={this.state.inThisLocation}
        />
      </div>
    );
  }
}

hooksを使った例

const TweetSearchResults = ({tweets}) => {
  const [filterText, setFilterText] = useState('');
  const [inThisLocation, setInThisLocation] = useState(false);
  return (
    <div>
      <SearchBar
        filterText={filterText}
        inThisLocation={inThisLocation}
        setFilterText={setFilterText}
        setInThisLocation={setInThisLocation}
      />
      <TweetList
        tweets={tweets}
        filterText={filterText}
        inThisLocation={inThisLocation}
      />
    </div>
  );
}

フライウェイトパターン

フライウェイトパターンは、類似のオブジェクトを大量に作るときに、メモリを節約するための便利な方法です。

アプリケーションで、ユーザーが本を追加できるようにしたいとします。すべての本は、titleauthor、そして isbn 番号をもちます。しかし、図書館には通常、ある本が 1 冊だけあるわけではなく、同じ本が複数冊あります。

まったく同じ本が複数ある場合、毎回新しい本のインスタンスを作成するのはあまり意味がありません。その代わり、1 冊の本を表わす Book コンストラクタのインスタンスを複数作成するようにします。

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

新しい本をリストに追加する機能を作ってみましょう。同じ ISBN 番号の本、つまりまったく同じ種類の本であれば、 新規に Book のインスタンスを作成する必要はありません。その代わり、この本がすでに存在するかどうかをまず確認します。

const books = new Map();

const createBook = (title, author, isbn) => {
  const existingBook = books.has(isbn);

  if (existingBook) {
    return books.get(isbn);
  }
};

もしまだその本の ISBN 番号がなければ、新しい本を作り、その ISBN 番号を isbnNumbers セットに追加します。

const createBook = (title, author, isbn) => {
  const existingBook = books.has(isbn);

  if (existingBook) {
    return books.get(isbn);
  }

  const book = new Book(title, author, isbn);
  books.set(isbn, book);

  return book;
};

createBook 関数は、ある本の新しいインスタンスを作成するのに役立ちます。しかし、図書館には同じ本が複数冊あるのが普通です!そこで、同じ本を複数追加する addBook 関数を作成しましょう。この関数は、createBook 関数を呼び出して、新しく作成した Book インスタンスを返すか、すでに存在するインスタンスを返します。

また、本の総量を記録するために、図書館内のすべての本を格納する bookList 配列を作成しましょう。

const bookList = [];

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

  bookList.push(book);
  return book;
};

完璧です!本を追加するたびに新しい Book インスタンスを作成する代わりに、その本に対応する既存の Book インスタンスを効果的に使用することができるようになりました。ハリー・ポッター (Harry Potter)、アラバマ物語 (To Kill a Mockingbird)、そしてグレート・ギャツビー (The Great Gatsby) という 3 種類の本を 5 冊作ってみましょう。

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);

本は 5 冊ありますが、作成された Book インスタンスは 3 つだけです。

※ 同じ値を参照させるから新規インスタンスは作成されない

フライウェイトパターンは、膨大な数のオブジェクトを作成することで、使用可能な RAM をすべて消費してしまうような場合に役に立ちます。消費されるメモリの量を最小限に抑えることができるのです。

JavaScript ではプロトタイプ継承によって、この問題を簡単に解決することができます。現在では、ハードウェアは GB 単位の RAM をもっているため、フライウェイトパターンの重要性は低くなっています。

ファクトリパターン

ファクトリパターンは、オブジェクトによって新しい値を生成するもの

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" });

user1fullname()という値を引き継ぐことができている。
ファクトリパターンは受け取った値から、変更可能なオブジェクトを設定する場合に有効

const createObjectFromArray = ([key, value]) => ({
  [key]: value
});

createObjectFromArray(["name", "John"]); // { name: "John" }

上記のように動的に値を設定したい場合に有効となる場合が多いです

ファクトリパターンは、同じプロパティを共有する小さなオブジェクトを複数作成する場合に便利です。ファクトリ関数により、現在の環境やユーザー固有の設定に依存するカスタムオブジェクトを返すことができます。

ファクトリ関数はnewを使わずにオブジェクトを返す関数にしかすぎません。
また毎回新しいオブジェクトを作成するよりも、新しいインスタンスを作成する方が、メモリ効率が良い場合が多いです。

コマンダーパターン

コマンドパターン (command pattern) を用いると、あるタスクを実行するオブジェクトと、そのメソッドを呼び出すオブジェクトを_切り離す_ことができことができます。

たとえば、オンラインフードデリバリーのプラットフォームがあったとします。ユーザーは注文をしたり (place)、追跡したり (track)、キャンセルしたり (cancel) することができます。

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}`
  }
}

ここでは OrderManager クラスの placeOrdertrackOrdercancelOrder メソッドにアクセスすることができる。

しかし、manager インスタンスのメソッドを直接呼び出すことにはデメリットがあります。それは、あるメソッドの名前をあとで変更することになったり、メソッドの機能が変更されたりした場合に発生します。たとえば、placeOrder という名前を addOrder へと変更したとしましょう。このとき、placeOrder メソッドがコードベースのどこからも呼び出されないようにしなければなりませんが、これは大規模なアプリケーションでは非常に厄介なことです。

代わりに、manager オブジェクトからメソッドを切り離し、各コマンドに対応する個別のコマンド関数を作成します。

OrderManager クラスをリファクタリングしましょう。placeOrdercancelOrdertrackOrder メソッドの代わりに、execute という単一のメソッドをもつようにします。このメソッドは、与えられた任意のコマンドを実行します。

各コマンドは、OrderManager の orders にアクセスする必要があるため、これを最初の引数として渡します。

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.`);
}

コマンドパターンにより、ある操作を実行するオブジェクトから、メソッドを切り離すことができます。これは、特定の寿命をもつコマンドや、特定の時間にキューに入れられ実行されるようなコマンドを扱う場合に、より細かな制御を可能とします。

2
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
7