🤨

TypeScript で private な constractor を実装する

2023/06/30に公開

保守性の高いコード

最近、保守性の高いコードってなんだろう... と考えることが多いです🤔🤔🤔

なので、ゴニョゴニョ考えてみたことをアウトプットしてみたいと思います!

以下のような class があったとします。

class Mountain {
    id: number;
    name: string;
    elevation: number;
    description?: string;
}

ベテランエンジニアの田中さんの実装。

const mountain = new Mountain();
mountain.id = 1;
mountain.name = 'Everest';
mountain.elevation = 8848;
mountain.description = 'エベレストは世界で一番高い山です。';

console.log(`${mountain.name} の標高は ${mountain.elevation} mです。`);

出力結果

[LOG]: "Everest の標高は 8848 mです。" 

一方、新人エンジニアの佐藤さんの実装。

const mountain = new Mountain();
mountain.id = 2;
mountain.name = 'K2';

console.log(`${mountain.name} の標高は ${mountain.elevation} mです。`);

出力結果

[LOG]: "K2 の標高は undefined mです。" 

undefined メートルという謎すぎる結果が出力されました...

これは良くない...

まず第一に、このような実装ができてしまうこと自体が問題ですよね。

ファクトリーメソッドでインスタンスの生成を制御する

上記 class を以下のように書き換えてみました。

type MountainType = {
    id: number;
    name: string;
    elevation: number;
    description?: string;
}

class Mountain {
    private constructor(
        private id: number,
        private name: string, 
        private elevation: number,
        private range: string,
        private description?: string
    ) {}

    static new(props: MountainType): Mountain {
        return new Mountain(
            props.id,
            props.name, 
            props.elevation, 
            props.description
        );
    }

    public genMessage(): string {
        return `${this.name} の標高は ${this.elevation} mです。`;
    }
}

この場合、ベテランエンジニアの田中さんは以下のように実装することになるでしょう。

const mountain = Mountain.new({
    id: 1,
    name: 'Everest',
    elevation: 8848,
    description: 'エベレストは世界で一番高い山です。'
});

console.log(mountain.genMessage());

また、新人エンジニアの佐藤さんも以下のように実装することになるでしょう。

標高 elevation が必須であるため、標高がなければならないことをソースコードが教えてくれます。

// elevation がないとエラーになるから、追加できる!
const mountain = Mountain.new({
    id: 2,
    name: 'K2',
    elevation: 8611
});

console.log(mountain.genMessage());

実装のポイントを順に説明します。

1. constractor を private 化

インスタンス生成時に制約を設けたいので、constractorprivate 化します。

そうすることで、以下のように好き勝手にインスタンスを生成できなくすることができます。

// private なのでアクセス不可でエラーになる
const mountain = new Mountain(3, 'Kangchenjunga', 8586, '');

2. static でファクトリーメソッドを定義

constractorprivate 化したので、インスタンスを生成するためのファクトリーメソッドを定義します。

new 関数はインスタンスを生成する前に呼び出すことができるように、静的メソッドである static を付与しています。

// private なのでアクセス不可でエラーになる
const mountain = new Mountain(3, 'Kangchenjunga', 8586);

// new でなら生成可能
const mountain = Mountain.new({
    id: 1,
    name: 'Everest',
    elevation: 8848,
    description: 'エベレストは世界で一番高い山です。'
});

3. object を引数にする

new 関数の引数は MountainPropsType です。

id, name, elevation, description? ではありません。

理由は、必須のプロパティが追加された場合に引数の順序に乱れが生じる可能性があるからです。

ここでは試しに、range という山が存在する範囲を示す必須のプロパティを追加してみます。

それぞれのプロパティを引数にした場合。

class Mountain {
    private constructor(
        private id: number,
        private name: string, 
        private elevation: number,
        private range: string,
        private description?: string
    ) {}

    static new(
        id: number,
        name: string,
        elevation: number,
        range: string,
        description?: string
    ): Mountain {
        return new Mountain(
            id,
            name,
            elevation,
            range,
            description
        );
    }

    public genMessage(): string {
        return `${this.name} の標高は ${this.elevation} mです。`;
    }
}

const mountain = Mountain.new(
    1,
    'Everest',
    8848,
    'エベレストは世界で一番高い山です。'
);

console.log(mountain.genMessage());

問題点にお気づきでしょうか?

mountain.range'エベレストは世界で一番高い山です。' となっているのです。

ここで、エラーを起こしたい!!

type MountainType = {
    id: number;
    name: string;
    elevation: number;
    range: string;
    description?: string;
}

class Mountain {
    private constructor(
        private id: number,
        private name: string, 
        private elevation: number,
        private range: string,
        private description?: string
    ) {}

    static new(
        props: MountainType
    ): Mountain {
        return new Mountain(
            props.id,
            props.name,
            props.elevation,
            props.range,
            props.description
        );
    }

    public genMessage(): string {
        return `${this.name} の標高は ${this.elevation} mです。`;
    }
}

// range が不足しているため、エラーになる
const mountain = Mountain.new({
    id: 1,
    name: 'Everest',
    elevation: 8848,
    description: 'エベレストは世界で一番高い山です。'
});

console.log(mountain.genMessage());

object を引数としておけば修正箇所は増えますが、rangeHimalayasdescription'エベレストは世界で一番高い山です。' であると気づきやすくなると思います。

// range が必要だとソースコードが教えてくれる
const mountain = Mountain.new({
    id: 1,
    name: 'Everest',
    elevation: 8848,
    range: 'Himalayas',
    description: 'エベレストは世界で一番高い山です。'
});

「エラーがあるよ!」と教えてくれることが、こんなにも幸せだなんて...!!!

まとめ

調べていると、多くのエンジニアが色々な方法で、
ある意味 "縛り" のあるコードにすることで保守性を高めているのだと分かりました。
(みんな苦労しているんだろうなぁ〜笑)

private な constructor の実装では、シングルトンパターン に活用している例もあって、
そちらも参考にしていきたいなと思っているところです!!

コラボスタイル Developers

Discussion