4
Help us understand the problem. What are the problem?

posted at

updated at

TypeScript(バニラJS)でストラテジーパターンを使って設計した話

この記事は何?

デザインパターンって理解できても使うシーンが難しい。
フロントやバックエンドも大体フレームワークでことが足りますし。
しかし、SSRのページではあるが一部だけシングルページっぽく表現した箇所があり
デザインパターンを使ったら、管理が楽になったシーンがあったので紹介します。

どういう想定?

  • シングルページでステップが複数個
  • 「戻る」「進む」ボタンある
  • 進捗ステップがあり自由に行き来しつつも、ちゃんと入力値はバリデーションする(ステップ2->ステップ4でもそれまでステップ2,3のバリデーションをする)

スクリーンショット 2022-03-21 1.09.57.png

やりたいこと

  • ステップごとに表示する内容を切り替えたいが各ステップで表示したい内容が異なる
  • ステップ切り替えだけのクラスで何とか表現したい。

何を使う?

ストラテジーパターンを使います。
PHPの参考URLにですが、かなりわかりやすいのと概念的には同じなので紹介させてもらいます。(普段はペチパーです。)

コード例

いきなりですが抽象クラスを作ってみよう

/**
 * ステップ共通抽象クラス
 *
 */
export abstract class StepAbstract {
    /**
     * コンストラクタ
     */
    public constructor() {
    }

    /**
     * ステップ開始時に実行される
     */
    abstract init(): void;

    /**
     * ステップ終了時に実行される
     */
    abstract finalize(): void;

    /**
     * バリデーション時に実行される
     * @return boolean
     */
    abstract valid(): boolean;
}

このStepAbstractクラスは各ステップのクラスで継承させます。
継承したクラスは上記のinit ・ finalize ・ validateは必ず実装しなければいけません。
コメント部分が各役割です。具体化されたクラスがまだないので、
まだイメージつきにくいですが今はこの3つを必ず使うんだという認識で大丈夫です。

具象クラスを作ってみよう

UI画像のステップ2を想定です。

import { StepAbstract } from "/StepAbstract";
/**
 * ステップ2クラス
 *
 * @extends StepAbstract
 */
export class Step2 extends StepAbstract {
    // フォーム要素
    private inputA: HTMLInputElement;
    private inputB: HTMLInputElement;

    /**
     * コンストラクタ
     *
     */
    constructor() {
        super();
        this.inputA = <HTMLInputElement>document.querySelector('#inputA');
        this.inputB = <HTMLInputElement>document.querySelector('#inputB');

    /**
     * ステップ開始時に実行される
     */
    public init() {
        // スクロールを上に持ってくるとかこのステップ特有の要素にaddEventListenerするとか
        // ステップが開始時、必ずするものを記載
    }

    /**
     * ステップ終了時に実行される
     */
    public finalize() {
        // removeEventListenerするとか
        // ステップが終了時、必ずするものを記載
    }

    /**
     * バリデーション
     * @return boolean
     */
    public validate(): boolean {
        // ここは単純にフォームがどちらも空ではないことをtrue、falseで返す
        return this.inputA.value !== "" && this.inputB.value !== ""
    }

これを似た感じでステップ分クラスを作成します。
実際自分の作成したものは、さらに具象クラスに各ステップで使う共通要素クラスをコンストラクタで噛ませたりしてますし、ステップより小さい単位のコンポーネントクラスをコンストラクタでnewしたりしたり、もっと自由に書いても大丈夫です。とりあえず、init ・ finalize ・ validateがあればOKです。

ステップをコントロールするクラスを作ってみよう

import { StepAbstract } from "/StepAbstract";

/**
 * ステップ制御管理クラス
 */
export class StepController {
    // StepAbstractを継承したクラスの配列
    private _steps: Array<StepAbstract>;
    // 戻るボタン
    private backButton: HTMLButtonElement;
    // 進むボタン
    private forwardButton: HTMLButtonElement;
    // 画像で言うと上部の😀マークです。
    private stepIcons: NodeListOf<HTMLElement>;


    /**
     * コンストラクタ
     * @param steps Array<StepAbstract>
     */
    constructor(steps: Array<StepAbstract>) {
        this._steps = stepPages;
        this.backButton = <HTMLButtonElement>document.querySelector("#back");
        this.forwardButton = <HTMLButtonElement>document.querySelector("#forward");
        this.stepIcons = <NodeListOf<HTMLElement>>document.querySelectorAll(".😀");
    }


    /**
     * コールされる時のトリガー
     */
    public init() {
        this.backButton.addEventListener('click', this.stepBackHandler);
        this.forwardButton.addEventListener('click', this.stepForwardHandler);

        for (let i: number = 0, len: number = this.stepIcons.length; len > i; i++) {
            this.stepIcons[i].addEventListener('click', this.stepIconClickHandler);
        }
    }

    /**
     * 進むボタンを押したときのイベントハンドラ
     */
    private stepForwardHandler = () => {
        const nextStepIndex: number  = 本来はここで閾値考慮など色々してるが本筋から逸脱するので省略するが前のステップの順番を取得
        this.moveStep(nextStepIndex);
    }

    /**
     * 戻るボタンを押したときのイベントハンドラ
     */
    private stepBackHandler = () => {
        const nextStepIndex: number  = 本来はここで閾値考慮など色々してるが本筋から逸脱するので省略するが次のステップの順番を取得
        this.moveStep(nextStepIndex);
    }
    /**
     * 😀をクリックしたときのイベントハンドラ
     *
     * @param e HTMLElementEvent<HTMLInputElement>
     */
    private stepIconClickHandler = (e: HTMLElementEvent<HTMLInputElement>) => {
         const currentStepIndex: number = 😀のデータ属性なりからステップの順番を取得
         this.moveStep(nextStepIndex);
    }

    /**
     * 現在のステップの順番を返す
     *
     * @example ステップ1なら0, ステップ2なら1みたいな感じ
     * @returnnumber
     */
    private resolveCurrentStepIndex(): number {
        return 現在のステップの順番返すコード(DOMとかに持たせたりclass変数に持たせたりご自由に)
    }

    /**
     * 現在のステップから選択されたステップまでをバリデーションし
     * バリデーションでNGだったステップのIndex番号を返す
     * 全てのバリデーションをクリアした場合は選択されたステップのIndex番号を返す
     *
     * @param currentStepIndex number
     * @param selectedStepIndex number
     * @return number
     */
    private getValidatedStepIndex(currentStepIndex: number, selectedStepIndex: number): number {
        for (let i: number = currentStepIndex, len: number = selectedStepIndex; len > i; i++) {
            if (!this._steps[i].validate()) {
                return i;
            }
        }
        return selectedStepIndex;
    }

    /**
     * ステップに応じた制御を行う
     *
     * @param selectedStepIndex number
     */
    private moveStep(selectedStepIndex: number) {
        // 現在のステップを取得
        const currentStepIndex: number = this.resolveCurrentStepIndex();
        // 次のステップを取得
        let nextStepIndex: number;
        if (currentStepIndex < selectedStepIndex) {
            // 選択したステップのバリデーション
            nextStepIndex = this.getValidatedStepIndex(currentStepIndex, selectedStepIndex); 本題のコード
        } else {
            nextStepIndex = selectedStepIndex;
        }

        // 現在のステップfinalize
        this._steps[currentStepIndex].finalize(); ⇦本題のコード
        // 次のステップinit
        this._steps[nextStepIndex].init(); 本題のコード
    }
}

最後のメソッドのmoveStepを見てください。
⇦本題のコードを3箇所入れました。
コンストラクタで、StepAbstractの抽象クラスを継承した具象クラスを配列で受け取っており、
それぞれ今どのステップで次どのステップに行くかで、具象クラスのinit, finalize, valideが実行されてます。
こちらがストラテジーパターンとなります。

最後にステップをコントロールするクラスをの呼び出し元を作ってみよう

import { StepController } from '/StepController';
import { Step1 } from '/Step1';
import { Step1 } from '/Step2';
import { Step1 } from '/Step3';
import { Step1 } from '/Step4';

/**
 * ステップをコントロールするクラスを呼び出すクラス
 */
export class Client {
    // ステップ制御管理クラス
    private _stepController: StepController;

    /**
     * コンストラクタ
     */
    constructor() {
        this._stepController = new StepController(
            [
                new Step1,
                new Step2,
                new Step3,
                new Step4,
            ]
        );
        this._stepController.init()
    }
}

各ステップの順番も入れ替えしやすいですし、ステップの追加もこのクラスに差し込むだけで済むかと思います。

あとがき

2年ぶりくらいににQiita書きました。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
4
Help us understand the problem. What are the problem?