はじめに

こんにちは、Rerurate_514と申します。
https://adventar.org/calendars/12078 13日目の記事です。
元々は、PDFのオブジェクトとストリームを解説する記事を投稿する予定だったんですが、思ったより大変そうだったので辞めました。
最近はずっとPDFの内部構造について勉強していたので、いけるかなーって思ってたんですけど、だめでした;;

今回はプリミティブを使用しないプログラミングと題しまして、反プリミティブの私が説いていこうと思います。

Info

プリミティブ型とは、stringやintなどのデフォルトで提供されている値のこと

よく、アプリなどを作成する際には文字列、数値、Booleanなどたくさんのプリミティブが存在するかと思います。

プリミティブの問題点

プリミティブ型を使用してしまうと変更容易性がとても失われてしまいます。

例を見てみましょう。

void setGameMode(String gameMode) {
	switch(gameMode) {
		case "easy": gameManager.setGameModeEasy();
		case "normal": gameManager.setGameModeNormal();
		case "hard": gameManager.setGameModeHard();
		else: throw Exception();
	}
}
 
void main() {
	final String defaultGameMode = "easy";
	setGameMode(defaultGameMode);
}

簡単にゲームモードを設定する関数を作ってみました。
この状態では、問題なくゲームモードを設定できるかと思います。

ただし、何かの間違いでeasyeasyyとタイプミスしたらどうでしょう。

void setGameMode(String gameMode) {
		switch(gameMode) {
		case "easy": gameManager.setGameModeEasy();
		case "normal": gameManager.setGameModeNormal();
		case "hard": gameManager.setGameModeHard();
		else: throw Exception();
	}
}
 
void main() {
	final String defaultGameMode = "easyy";
	setGameMode(defaultGameMode);
}

この状態だとelseに入り、例外が出てしまいます。

この問題は文字列、つまりプリミティブな型で制御しているのが問題になります。
これを列挙型を使用して、書き換えてみたらどうでしょう。

enum GameMode {
	easy,
	normal,
	hard
}
 
void setGameMode(GameMode gameMode) {
		switch(gameMode) {
		case GameMode.easy: gameManager.setGameModeEasy();
		case GameMode.normal: gameManager.setGameModeNormal();
		case GameMode.hard: gameManager.setGameModeHard();
	}
}
 
void main() {
	final String defaultGameMode = GameMode.easy;
	setGameMode(defaultGameMode);
}

このように書くとタイプミスによる関数呼び出しミスがなくなります。
この問題点はどこにでも存在しています。

そして基本的にレイヤードアーキテクチャにおいては、Entityはプリミティブでない型で構成したほうが良いです。
ドメイン固有の名前を冠することでプログラムの可読性がぐっと高まります。
例えば、このような例。(freezedを使用して。)

@freezed
sealed class PlayList with _$PlayList {
  const factory PlayList({
    required PlayListId id,
    required String name,
    required List<Music> list,
    required PictureImage picture,
  }) = _PlayList;
}

そのエンティティの名前などを表す際にはプリミティブを使用したほうが効率が良い場合もありますが、基本的にはこのようにクラスで構成したほうが良いです。
このエンティティを作成する際にタイプミスなどすることがぐっと減るからです(体感)

さらに、各プロパティに明示的なファクトリを設定することもできます。

@freezed
sealed class PlayListId with _$PlayListId {
  const factory PlayListId({
    required String value
  }) = _PlayListId;
 
    factory PlayListId.fromInputName(String inputName) {
    const uuid = Uuid();
    final uniqueId = uuid.v5(Namespace.url.value, inputName);
    return PlayListId(value: uniqueId);
  }
 
  factory PlayListId.createMainListId() {
    return PlayListId(value: "in-main-list");
  }
}

この場合、Stringだとただの文字列という意味になりますが、PlayListIdと表現することで一意なIDなんだなと一目でわかるかと思います。

Dartの話になりますが、freezedを使用していると、初期値の設定などができるというメリットもあります。

さらに別の具体例も見てみましょう。

void drawRect(int height, int width); 
drawRect(100, 200); // OK
drawRect(200, 100); // 間違いだがコンパイルエラーにならない

この場合、人間がwidthheightを間違えたとしてもどちらも正しくintなのでエラーになりません。

これを解決するなら型を使用するのが最も手っ取り早いです。

class Height { final int value; ... }
class Width { final int value; ... }
void drawRect(Height height, Width width);

関数にそれぞれプリミティブを使用しないようにすると、どちらがheightwidthなのかが明確になります。
Stringのままであると、コメント文などでどちらがどっちかを説明することになるかもしれません。
ただし、クラスを用いることで、プログラムが自身を説明してくれるようになるのです。

おわり

プリミティブを使用しないだけで様々なメリットを享受することができます。堅牢性、可読性、保守性などなど。。。
皆さんも、クラスを使用して単純な関数から値オブジェクトまで安全性を高めていきましょう。