LoginSignup
3
0

【TypeScript】ジェネリクスで我流ファクトリーメソッドを作ってみた【typoを減らせ!】

Last updated at Posted at 2023-12-03

0. INDEX

  1. 背景
  2. 目的
  3. サンプルコードと解説
  4. 追記
  5. あとがき

1. 背景

おはこんばんにちは1。私です。

昔からシステム開発の実装にあたっては、システムのビジネスロジックを回す仕掛けやらユーティリティを作る事が多い気がしていて2、各ビジネスロジックそのものは他のメンバーも書いたりしていた…と思います。

そうなると、例えば不必要にリテラルな値を打たせるとtypoってしまう危険があるので、この画像の様に極力コードアシストが効くような実装にしたい訳です。

image.png

最近の私の業務では、Azure App Service3やAzure Functions4を使う機会が増えてきました。
合わせて、Node.js+TypeScript+VueとかPython+Flaskみたいな構成でWEBシステムを開発する事が増えました。
とは言えTSやPythonは使いこなせていない上に元がJava使いなので、「Javaならこういうのあるんだけどなー」と悶々とする場面がまだまだ多いです。

そんなワケで、今回はTypeScriptのジェネリクスの勉強がてら、我流のファクトリーメソッドを書いてみたのでその紹介をします。

2. 目的

今回は、サンプルコードとしてあるインターフェース5を持つ、インスタンスを生成するメソッドを実装してみます。

で、今回の肝はファクトリーメソッドとジェネリクスなので用語についても情報を載せておきます。

2.1. ファクトリーメソッド

Factory Method パターンは、他のクラスのコンストラクタをサブクラスで上書き可能な自分のメソッドに置き換えることで、 アプリケーションに特化したオブジェクトの生成をサブクラスに追い出し、クラスの再利用性を高めることを目的とする。

ほうほう。私ごとき凡愚6にはよく分かりません。
でも、元々私がやりたかった事を考えると本流のファクトリーメソッドからは離れそうなのでこの記事に関しては 我流 であるとします。

2.2. ジェネリクス

ジェネリック(総称あるいは汎用)プログラミング(英: generic programming)は、具体的なデータ型に直接依存しない、抽象的かつ汎用的なコード記述を可能にするコンピュータプログラミング手法である。

Javaだと稀によく見る謎の文字 T です。言語仕様的には大文字の英字1文字なら何でもいいらしい7、通常は「TSU」あたりにするのが無難でしょう。

2023/12/06追記分
大文字英字1文字 という部分が誤りでした。直感でいくつか検証した所、予約語以外は使えそうな感じですね。
とは言え、日本語が使えるのは少々ビックリしました。8

image.png

3. サンプルコードと解説

多いので多少カットしています。全文が見たい場合はこちらからどうぞ。

最初に掲載したコード
https://stackblitz.com/edit/typescript-cfemxe?file=index.ts

2023/12/06追記分のコード
https://stackblitz.com/edit/typescript-l8z9c9?file=index.ts

3.1. サンプルコード

こんな構成になっています。

<ROOT>
├ utils
│ ├ IDto.ts // ファクトリーメソッドにおける親代わりのインターフェース
│ ├ HogeDto.ts // ファクトリーメソッドで生成したいクラス
│ ├ FugaDto.ts // 同上
│ └ PiyoDto.ts // 罠用。インターフェースがないクラス
├ index.ts // メイン処理
└ index.html // 動作結果の確認画面
index.ts
// Library
import IDto from './utils/IDto';
import HogeDto from './utils/HogeDto';
import FugaDto from './utils/FugaDto';
import PiyoDto from './utils/PiyoDto';

/**
 * DTOを生成するファクトリー的なクラス
 */
class DtoFactory<T extends IDto> {
  static readonly HOGE = new DtoFactory(HogeDto);
  static readonly FUGA = new DtoFactory(FugaDto);
  static readonly PIYO = new DtoFactory(PiyoDto);

  private clazz: new () => T;

  private constructor(clazz: new () => T) {
    this.clazz = clazz;
  }

  get(): T {
    return new this.clazz();
  }
}

// 以降、動作確認
const hoge = DtoFactory.HOGE.get();
const hogeStr: string = JSON.stringify(hoge, null, '    ');

const fuga = DtoFactory.FUGA.get();
const fugaStr: string = JSON.stringify(fuga, null, '    ');

const piyo = DtoFactory.PIYO.get();
const piyoStr: string = JSON.stringify(piyo, null, '    ');

const appDiv: HTMLElement = document.getElementById('app');
appDiv.innerHTML = `
<pre>
HOGE: ${hoge.constructor.name}
${hogeStr}
---
FUGA: ${fuga.constructor.name}
${fugaStr}
---
PIYO: ${piyo.constructor.name}
${piyoStr}
</pre>
`;
utils/IDto.ts
export default interface IDto {}
utils/HogeDto.ts
import IDto from './IDto';

export default class HogeDto implements IDto {
  public name: string;

  constructor(name: string = 'hoge') {
    this.name = name;
  }
}
utils/FugaDto.ts
import IDto from './IDto';

export default class FugaDto implements IDto {
  public name: string;
  public nickname: string;

  constructor(name: string = 'fuga', nickname: string = null) {
    this.name = name;
    this.nickname = nickname;
  }
}
utils/PiyoDto.ts
export default class PiyoDto {
  public name: string;

  constructor(name: string = 'piyo') {
    this.name = name;
  }
}

3.2. 解説

utils/*Dto.ts

ファクトリーメソッドで生成したいクラスを定義しています。
スーパークラス代わりのIDto.tsと、スーパークラス的な物を使わないPiyoDto.tsという罠クラスを置いている以外は特に解説も不要でしょう。

index.ts

ファクトリーなクラスの実装と、その動作確認処理を書いています。

使わせ方はこういう感じを想定しています。9

const hoge = DtoFactory.HOGE.get();

任意のクラスインスタンスを生成する方法については、検証の結果、以下のようにすると出来ると分かりました。

function get<T>(clazz: new () => T): T {
  return new clazz();
}

// 別途 `Test`という名前のクラスを定義済みだとして
const test = get(Test);

という事でクラスはこういう構成になります。

class DtoFactory<T extends IDto> {
  // コンストラクタは隠蔽しているので、内部で作っておく必要がある。
  // Dtoが増えたらここに追加していけばいい
  static readonly HOGE = new DtoFactory(HogeDto);

  private clazz: new () => T;

  private constructor(clazz: new () => T) {
    // get()を呼んだ時に必要なので確保!
    this.clazz = clazz;
  }

  get(): T {
    // 確保しておいたクラスをインスタンス化する
    return new this.clazz();
  }
}

こんな順番で呼んでインスタンスを作るわけですね!

  1. プライベートなコンストラクタで、生成したいクラスを指定する
  2. get()を呼んだ時、1で確保したクラスを使い、インスタンスの生成処理を定義する
  3. 静的フィールドで我流ファクトリメソッドの種(つまり12)の処理を仕込んで、外から呼べるようにする

3.3. 実行結果

最終的にクラスはこのようになりました。実行してみましょう。

/**
 * DTOを生成するファクトリー的なクラス
 */
class DtoFactory<T extends IDto> {
  static readonly HOGE = new DtoFactory(HogeDto);
  static readonly FUGA = new DtoFactory(FugaDto);
  static readonly PIYO = new DtoFactory(PiyoDto);

  private clazz: new () => T;

  private constructor(clazz: new () => T) {
    this.clazz = clazz;
  }

  get(): T {
    return new this.clazz();
  }
}

image.png

お、ちゃんとインスタンス生成出来ているな。よすよす…

image.png

アルェ?! Piyoデテル?

image.png

アイエエエ?! ピヨ? ピヨナンデ!?

出力処理は以下の様に書いています。

const hoge = DtoFactory.HOGE.get();
const hogeStr: string = JSON.stringify(hoge, null, '    ');

const fuga = DtoFactory.FUGA.get();
const fugaStr: string = JSON.stringify(fuga, null, '    ');

const piyo = DtoFactory.PIYO.get();
const piyoStr: string = JSON.stringify(piyo, null, '    ');

// Write TypeScript code!
const appDiv: HTMLElement = document.getElementById('app');
appDiv.innerHTML = `
<pre>
HOGE: ${hoge.constructor.name}
${hogeStr}
---
FUGA: ${fuga.constructor.name}
${fugaStr}
---
PIYO: ${piyo.constructor.name}
${piyoStr}
</pre>
`;

Piyoの部分はこうですね。

const piyo = DtoFactory.PIYO.get();
const piyoStr: string = JSON.stringify(piyo, null, '    ');

これを書いている時に、警告も何も出なかったので最後の望みを掛けて、実行時エラーを期待したのですが、何事もなかったように動いちゃいました。
これに関しては、この場では解決できなかったけど、それほど問題にも思わなかったので一旦スルーしていましたが、コメント指摘で解決出来たので後述します。10

2023/12/06追記分
コメントでのご指摘により、構造的部分型 (structural subtyping) が一致する為、暗黙の型一致となり警告もなく PiyoDto が出力されたらしい事が分かりました。
私の理解した範疇ですがフィールドやメソッドが一致していれば同じと判断されるらしいとの事で、検証の為に次の実装に変更してみました。(全文はコチラ

utils/PiyoDto.ts
export default class PiyoDto {
  public nickname: string;

  constructor(nickname: string = 'piyo') {
    this.nickname = nickname;
  }
}

そうすると、このスクショの様にエディタ上で警告が出る事を確認できました。
image.png

まあ、プログラム自体は動いてはしまうのですが、警告が出るなら実務でそのまま実装する事は無いと思うので、これはこれで良いかな…となりました。

4. 追記

2023/12/06追記分

  • 型パラメータについて。大文字英字1文字に取り消し線を入れ、追加検証した内容を追記しました
  • 構造的部分型 (structural subtyping) について。既存コードをforkして検証コードを追加しました。forkしたコードはこちら

htsignさん、ご指摘ありがとうございます!

5. あとがき

今回はTypeScriptで型パラメータが使える事を知ったので色々試してみた結果を書いてみました。
特にフレームワークは使っていない(と思う)ので実装できる状況は多いかも知れませんが、「採用するか?」と考えるとちょっと微妙かもしれませんねω

本当はこう書きたかったし。

const hoge: IDto<HogeDto> = FactoryDto.get();

もうちょっと現場で揉まれていくと、この記事は「俺は一体何を書いていたんだ…」と黒歴史後で見返してギャーってなるやつであると気づく日が来るかもしれません。まあ、その時はその時で。

それにしても初めてアドベントカレンダーに参加したわけなんですが、
社内の投稿数を上げようという試みにほんのり義務感が働いて来ますね。
個人単位ではカレンダーを使う事は無いと思いますが、所属グループがまた立ち上げたら参加しようと思います。

  1. この挨拶。 Dr.スランプ にもあったなーと思ったり思わなかったり。

  2. 適材適所くらいにと思ってくれてるなら良いけど「アイツに業務系を書かせるのはヤヴァイ」とか思われてたらストレスで夜しか眠れないかもしれない。

  3. コードやDockerイメージ何かをデプロイWEBサービスを展開しやすくなるサービス。かつてはオンプレサーバやVMを準備し、ミドルウェアをインスコして、アプリをデプロイして…みたいな事をやってたけど、これのおかげでポータル上でポチポチすればアプリ以外は完了する。開発者はよりアプリ開発に注力できるようになると言う触れ込み…だと思う。

  4. こちらもコードやDockerイメージをデプロイできるのでappserviceと似た特性を持っている。ただこちらはサーバーレス形態もあるので、画面を持つようなサービスよりもAPIやバッチ的な使い方が向いている…と思う。本格的なバッチならAzure Batchの方が良いけど。

  5. 今回はinterfaceを使っているが、型を抽象化したいだけなので、スーパークラスや抽象クラスを使っても良い。

  6. (すごい人から見て)平凡な人で愚かな人。いわゆるモブ。多分あの人のせいで俺の中で有名になった。画像を貼ると怒られると思うので、画像検索結果だけ貼っておこう。うん。

  7. 神「別に大文字でなくともいいし1文字でなくともいいです。なんなら日本語でもいいです。」俺「アイエエエ?!」

  8. 日本語がいけるなら、もしや古の「ジャンガリアン記法」が使えるのでは?!…ダメでした。

  9. ヒロシデス…。ファクトリーメソッド的にクラスを指定するのはご法度だと知っていたとです。ヒロシデス…。それでもクラスやインスタントなしに、ファクトリークラス側でTが何かを知る術が分からなかったとです。ヒロシデス…ヒロシデス…。

  10. ファクトリーメソッド的には、DtoFactory.HOGEと書いている時点で、具体的なクラスを指定しないというルールを破っているのでニンともカンとも。

3
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0