ふと意識した途端、クラスをnewしたときにプロパティへ何を初期値として入れるべきなのかが段々分からなくなってきました。
自分の中で考えられうる実装のバリエーションを分類した上で、どう使い分けるべきなのか現状の考えをメモします。
A. 初期値はnull
だよ派
null
とのユニオン型を定義し、コンストラクタ内でnull
をセットする実装。
-
メリット
- 意味のある値がセットされている状態か、そうでないかが区別できる
-
懸念
- プロパティを使用する際、
null
の場合の分岐も実装しなければならない
- プロパティを使用する際、
class MyEntity {
public name: string | null;
public num: number | null;
public date: Date | null;
public messages: string[] | null;
public subEntity: SubEntity | null;
constructor() {
this.name = null;
this.num = null;
this.date = null;
this.messages = null;
this.subEntity = null;
}
}
B. 初期値はundefined
だよ派
undefined
とのユニオン型を定義し、意図的にundefined
のままにしておく実装。
-
メリット
- 意味のある値がセットされている状態か、そうでないかが区別できる
-
懸念
- プロパティを使用する際、
undefined
の場合の分岐も実装しなければならない - 値が
undefined
のとき、本当に存在しないプロパティなのか、値がセットされていないだけなのかが分かりにくそう
- プロパティを使用する際、
class MyEntity {
public name: string | undefined;
public num: number | undefined;
public date: Date | undefined;
public messages: string[] | undefined;
public subEntity: SubEntity | undefined;
constructor() {
}
}
C. 初期値は空っぽい値だよ派
空っぽい値を明示的にコンストラクタ内でセットする実装。
-
メリット
- ユニオン型がいらないので、型定義がシンプル
-
null
やundefined
のケースを想定した分岐が不要
-
懸念
- 値がセットされていないのか、セットした値がたまたまデフォルト値と同じなのかが判別できない
(配列が初期値のままなのか、セットした値が実際にゼロ件なのかなど) - 文字列型と配列以外の型では、本当の意味で「空っぽい値」を定義できない(特に日付型)
- 値がセットされていないのか、セットした値がたまたまデフォルト値と同じなのかが判別できない
class MyEntity {
public name: string;
public num: number;
public messages: string[];
public subEntity: SubEntity;
constructor() {
this.name = '';
this.num = 0; // 0が空っぽい値というのはかなり強引……
this.messages = [];
this.subEntity = new SubEntity();
}
}
D. 意味のあるデフォルト値をセットしちゃうよ派
明示的に意味のあるデフォルト値をコンストラクタ内でセットしてしまう実装。
-
メリット
- 型定義がシンプル
-
null
やundefined
の状態を想定せずに実装できる - 前提が整っていれば、初期状態と値がセットされた状態を区別できる
-
懸念
- 定義したデフォルト値が偶然セットした値と一致していまう可能性がある
- デフォルト値のせいで無意味にメモリを消費するのでは
- 設計の難易度が余計に増すのでは
class MyEntity {
public name: string;
public num: number;
public date: Date;
constructor() {
this.name = 'not set'; // 初期値は'not set'だという仕様にしてしまう
this.num = -9999; // 初期値は-9999だという仕様にしてしまう
this.date = new Date(1889, 0); // 初期値は1889年1月1日だという仕様にしてしまう
}
}
E. 絶対にコンストラクタ引数からセットするよ派
コンストラクタの引数から値をとらせることを強制する実装。
-
メリット
- 型定義がシンプル
- 初期値に悩む必要がない
-
null
やundefined
の状態を想定せずに実装できる
-
懸念
- 「まだ具体的な値は分からないけどとりあえずインスタンスを生成したい」という場合に使いにくい
- 初期値をどうするのかという観点では、問題を一つ上のレイヤーへ先送りしただけ
class MyEntity {
constructor(
public name: string,
public num: number,
public date: Date,
public messages: string[],
public subEntity: SubEntity
) {
}
}
const entity = new MyEntity('hoge', 1, new Date(), ['message1', 'message2'], new SubEntity());
よく選ぶ実装パターンと選ばない実装パターン、その理由
よく選ぶ実装パターン
実際は、プロパティの型に応じてよく使うパターンが分かれている印象です。
筆者の経験上妥当なことが多い実装パターンを挙げます。
-
文字列型
- 空文字列
''
を初期値する場合が多い。要件によってはnull
にすることもある
- 空文字列
-
数値型
-
null
を初期値する場合が多い。要件によっては0
にすることもある
-
-
日付型
-
null
を初期値とする場合が多い
-
-
配列型
- ゼロ件の配列
[]
をセットすることが多い。要件によってはnull
にすることもある
- ゼロ件の配列
-
カスタム型
- その場でインスタンスをnewすることが多い。要件によっては
null
にすることもある
- その場でインスタンスをnewすることが多い。要件によっては
一般化はできていません。
要件に応じ、プロパティ値が初期状態かどうかを判定したい場合は初期値をnull
にする設計に寄せる。
そうでなければ空っぽい値をとる設計に寄せる、というのは言えるかもしれません。
// よく選ぶ実装パターンの例
class MyEntity {
public name: string;
public num: number | null;
public date: Date | null;
public messages: string[];
public subEntity: SubEntity;
constructor(param: Partial<MyEntity> = {}) {
this.name = '';
this.num = null;
this.date = null;
this.messages = [];
this.subEntity = new SubEntity();
Object.assign(this, param);
}
}
// インスタンス生成時にパラメータオブジェクトから値を渡す使い方が好き
const entity = new MyEntity({
name: 'Who bar',
num: 0,
date: new Date(1955, 1, 24),
messages: ['who', 'bar']
});
滅多にやらないパターン
逆に、以下の3パターンを好んで選ぶことはほとんどありません。
-
B. 初期値は
undefined
だよ派- 個人的には
undefined
を意図的に初期値とすることにあまりメリットが感じられません。null
の方が意図が感じられます
- 個人的には
-
D. 意味のあるデフォルト値をセットしちゃう派
- 「なぜそれがデフォルト値であるべきなのか」という根拠を考えるのが難しく、それなら
null
や空値の方がマシに感じます - たとえば入力フォームで最初からデフォルトの値を表示させておきたいなどのケースでは、初期化時のパラメータから拾うか、インスタンス生成後に値をセットすることで対応させる方が自然に感じます。
- 「なぜそれがデフォルト値であるべきなのか」という根拠を考えるのが難しく、それなら
-
E. 絶対にコンストラクタ引数からセットするよ派
- インスタンス生成時に常に具体的な値が必要となるので不便な印象があります