0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TypeScriptでわかる】Strategyパターン

0
Posted at

Strategyパターンとは

設計パターンの一種。1つのinterfaceを定義して、これをimplementsしたクラスを複数用意するやり方。
共通のinterfaceをimplementsしているため、実装したクラスは簡単に交換可能である。

用語

コード例は以下の記事で紹介されていたPythonをTypeScriptに修正したものを使わせていただいた。
https://qiita.com/hankehly/items/1848f2fd8e09812d1aaf

Context

Strategy(interface)を実装したオブジェクトを持ち、処理の一部をStrategyに委譲するクラス。

// Job.ts
import type { StorageStrategy } from "./StorageStrategy";

export class Job {
  private inputStorageStrategy: StorageStrategy;
  private inputPath: string;

  private outputStorageStrategy: StorageStrategy;
  private outputPath: string;

  constructor(
    inputStorageStrategy: StorageStrategy,
    inputPath: string,
    outputStorageStrategy: StorageStrategy,
    outputPath: string
  ) {
    // Strategyをコンストラクターで渡して、インスタンス変数にする
    this.inputStorageStrategy = inputStorageStrategy;
    this.inputPath = inputPath;
    this.outputStorageStrategy = outputStorageStrategy;
    this.outputPath = outputPath;
  }

  async run(): Promise<void> {
    const data = await this.getData();
    await this.saveData(data);
  }

  private async getData(): Promise<Uint8Array> {
    // ロジックをStrategyに委託する
    return await this.inputStorageStrategy.get(this.inputPath);
  }

  private async saveData(data: Uint8Array): Promise<void> {
    // ロジックをStrategyに委託する
    await this.outputStorageStrategy.save(data, this.outputPath);
  }
}

Strategy

interface。これをimplementsしたクラスは、interfaceに書かれているものを必ず実装しなければならない。

// StorageStrategy.ts
export interface StorageStrategy {
  get(path: string): Promise<Uint8Array> | Uint8Array;
  save(data: Uint8Array, path: string): Promise<void> | void;
}

Concrete Strategy

Strategyをimplementsしたクラス。各メソッドの内容はテキトーです。

// LocalStorageStrategy.ts
import { readFile, writeFile } from "fs/promises";
import type { StorageStrategy } from "./StorageStrategy";

export class LocalStorageStrategy implements StorageStrategy {
  async get(path: string): Promise<Uint8Array> {
    return await readFile(path);
  }

  async save(data: Uint8Array, path: string): Promise<void> {
    await writeFile(path, data);
  }
}
// S3StorageStrategy.ts
import type { StorageStrategy } from "./StorageStrategy";

type S3ClientLike = {
  getObject(key: string): Promise<Uint8Array>;
  putObject(key: string, data: Uint8Array): Promise<void>;
};

export class S3StorageStrategy implements StorageStrategy {
  constructor(private s3: S3ClientLike) {}

  async get(path: string): Promise<Uint8Array> {
    return await this.s3.getObject(path);
  }

  async save(data: Uint8Array, path: string): Promise<void> {
    await this.s3.putObject(path, data);
  }
}

使用例

localStorageを使う場合の使用例。

import { Job } from "./Job";
import { LocalStorageStrategy } from "./LocalStorageStrategy";

const local = new LocalStorageStrategy();

const job = new Job(
  local,
  "./input.bin",
  local,
  "./output.bin"
);

await job.run();

S3を使う場合の使用例。

import { Job } from "./Job";
import { S3StorageStrategy } from "./S3StorageStrategy";

// getS3Client()の実装は省略
const s3Client = getS3Client();

const s3 = new S3StorageStrategy(s3Client);

const job = new Job(
  s3,
  "s3://my-bucket/input.bin",
  s3,
  "s3://my-bucket/output.bin"
);

await job.run();

メリット

コードを拡張しやすくなる。仕様変更が起きても、少ない労力で対応できる。

Jobクラスが持っておかないといけない情報が減る

Strategyパターンを採用しない場合、Jobクラスは、どのストレージサービスを使う可能性があるのか、という情報を持っておく必要があった。

class Job {
  run() {
    if (this.type === "s3") {
      // S3取得
    } else if (this.type === "local") {
      // ローカル取得
    }
    // 今後はGoogle Cloud Storageも使うかも...?
    // もしそうなら、以下にどんどんコードが増えることになる
  }
}

strategyパターンを採用することで、Jobクラスのrun()は以下のようになった。

export class Job {
  async run(): Promise<void> {
    const data = await this.getData();
    await this.saveData(data);
  }
}

とてもスッキリしているし、今後扱いたいストレージサービスが増えたとしても、ConcreteStrategyを追加で実装すればOK。Jobクラスは変わらない。

このメリットは、可読性や保守性、拡張性に大きく貢献している。

Jobクラスから排除された分岐はContextを呼び出す前などで行わなければならない。

const strategy =
  storageType === "s3"
    ? new S3StorageStrategy()
    : new LocalStorageStrategy();

const job = new Job(strategy, ...);

テストがしやすくなる

Jobクラスの役割が明確になったことで、Jobクラスのテストがしやすくなる。
なぜなら、他のストレージサービスを使いたくなったとしても、Jobクラスは変わらないからである。
(新たに追加するConcreteStrategyのテストは追加実装が必要)

デメリット

クラス/ファイル数が増える

新たにinterfaceやConcreteStrategyとしてクラスを実装する必要があるので、その分ファイル数は増える。

処理が追いにくくなる(かもしれない)

ファイル数が増えることで、処理を追いにくくなることがあるかもしれない。
(個人的には、1つのファイルにやたらめったら書くよりは、ファイル数が増える方がまし派)

使いどき

似たような機能ではあるが、差異がある機能を実装したいとき。

例えば、S3とファイルをやり取りしていたが、Google Cloud Storageともやりとりする必要が出てきたとき、Strategyパターンが有効。

putとgetについてのinterfaceを実装し、これをimplementsしたclass S3class GoogleCloudStorageを実装。

このようにすれば、もしS3は不要になった時や、Azureともやり取りしたくなった時に、影響範囲を小さく抑えることができる。

参考

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?