📑

クラスのコンストラクタをブロックして公称型を活用しよう

2023/09/23に公開

危険なユースケース

早速ですが、以下のようなログインのユースケースがあったとします。

function login(id: string, password: string) {
    // ...
}

この処理が危険である点は、passwordが任意の文字列である点です。

しかしながら、大抵は任意のパスワードが登録できるわけではないわけです。

ただ、そんなこと言うと

「バリデーションチェックしてあるから大丈夫!」

と言われるかもしれませんが、それはユースケースの外で行われていることであり、保証はされていないのです。

ユースケースの中でバリデーションを書こうにも、忘れてしまう上に、ドメインロジックの漏洩が起きてしまいます。

では、以下のように変更してみるとどうでしょう。

function login(id: string, password: Password) {
    // ...
}

文字列ではなく、Passwordオブジェクトを受け取るようにしました。

ここで公称型のメリットが生きてきます。

Passwordオブジェクトを公称型にすることで、文字列をPasswordオブジェクトに変換できた場合のみlogin関数が呼べるようになります。

公称型を作るクラスの定義の仕方

では、次にPasswordオブジェクトをインスタンスとして生成するクラスを定義していきましょう。

試しに単純にクラスを定義してみるとどうでしょう。

export class Password {
    constructor(public data: string) {}
}

これを呼び出すには、

import { Password } from "./model/Password";

const password = new Password("password1234");

login('id', password);

response(200);

となるわけです。

しかし、これではバリデーションが通っていなくてもPasswordオブジェクトを生成できてしまいます。

よって、コンストラクタの中でオブジェクトの生成を阻止します。

const PASSWORD_REGEX = /何らかのパスワードの形式/;

export class Password {
    constructor(public data: string) {
        if (!PASSWORD_REGEX.test(data)) throw new ValidationError('パスワードが不正です');
    }
}

呼び出し側は、以下のようにハンドルすればよいわけです。

import { Password } from "./model/Password";

try {
    const password = new Password("password1234");
    login('id', password);

    response(200);
} catch (e: unknown) {
    if (e instanceof ValidationError) {
        response(400, e.message);
    } else {
        throw e;
    }
}

これでうまくいったように思えますが、TypeScriptは構造的部分型を採用しているため、何かしらのprivateプロパティを定義しないと公称型になりません。

よって、もう少し工夫をする必要があります。

コンストラクタを外部から呼べないようにブロックする

以下のように変更を加えました。

const PASSWORD_REGEX = /何らかのパスワードの形式/;

const typeSym = Symbol();

export class Password {
    private type = typeSym;

    constructor(public data: string) {
        if (!PASSWORD_REGEX.test(data)) throw new Error('パスワードが不正です');
    }
}

これで上手くいったような気がしますが、そうではありません。

コンストラクタの中で例外を発生させて大域脱出をする処理は、保守性を大きく阻害するからです。

それを解決していきます。

コンストラクタをブロックする

解決方法は簡単で、外部からコンストラクタを呼べないようにして、staticなメソッドを用意します。

const PASSWORD_REGEX = /何らかのパスワードの形式/;

const typeSym = Symbol();

export class Password {
    private type: typeof typeSym;

    // コンストラクタの第一引数を公開していないシンボル型にすることで、外部から呼べないようにする
    constructor(type: typeof typeSym, public data: string) {
        this.type = type;
    }

    static create(data: string) {
        // バリデーションに失敗した場合は未定義(undefined)や、Result型などのオブジェクトを返す
        if (!PASSWORD_REGEX.test(data)) return undefined;

        // コンストラクタによる生成は内部のメソッドに隠ぺいする
        return new Password(typeSym, data);
    }
}

さて、これを使っていきましょう。

import { Password } from "./model/Password";

const password = Password.create("password1234");

if (password !== undefined) {
    login('id', password);

    response(200);
} else {
    response(400, "パスワードが不正です");
}

大域脱出が無くなり、細やかなハンドリングがしやすくなりました。

Password.createメソッドを使わない限りPasswordオブジェクトを生成できないわけですから、不正な文字列がlogin関数に渡ることも無くなったわけです。

おまけ

コンストラクタの危険な点は何かということを考えると、「例外を発生させないと問答無用でインスタンスが生成できてしまう」という点ではないでしょうか?

今回は、その問題を「外部から参照・生成できない値(JavaScriptだとシンボル型)を第一引数に置く」ことで解決しました。

さらに、今回 string -> Passwordの変換をしたわけですが、パスワード文字列というのは文字列のサブセットになるわけですから、いわばダウンキャストをしたような形になります。

そして、ダウンキャストは場合によっては危険な操作ですが、安全なダウンキャストをするためのメソッドがPassword.createであり、それによってのみ生成されるオブジェクトを使うことで安全なプログラミングができるわけです。

他にも例えば、整合性確保の観点で語られる"集約"も、ルートエンティティからのダウンキャストによって作られると言えます。

集約までダウンキャストすることで、整合性が確保された安全な永続化処理に到達できるようになると言えます。

この辺の話は別の記事にするかもしれません。

ここまで読んでいただきありがとうございました。

Discussion