問題

私は、業務では主にPHPとJavascriptを書き、ごく稀にJavaやSwiftをたまに触ります。
また、趣味で最近TypeScriptを触っています。

PHPでこんな感じでFactoryパターンを書くと、DRYで好きです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class Factory
{
private $class_map;

public function __construct()
{
$this->class_map = new SplObjectStorage();
$this->class_map[MammalianType::Dog()] = Dog::class;
$this->class_map[MammalianType::Cat()] = Cat::class;
$this->class_map[MammalianType::Capibara()] = Capibara::class;
}

public function create(MammalianType $mamalian_type, PetSource $source): Mammalian
{
assert(isset($this->class_map[$mamalian_type]));

return new $this->class_map[$mamalian_type]([
'name' => $source->name, // constructorに渡すものは全て一緒で引数からインジェクトしたものを入れる。
'blood_type' => $source->blood_type,
]);
}
}

しかし、Java・Swiftなどの静的型付言語を触る時に同じような実装をするには、どうしたら良いものかと頭を抱えてしまいます。
PHPでも$class_mapプロパティに代入されているSplObjectStorageのvalueは厳密にはstringなのです。

Javaでの実装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Factory
{
// Any!?
private Map<MammalianType, Any> classMap;

public Factory()
{
// Any!?
this.classMap = new HashMap<MammalianType, Any>();

// どう書けば良いか不明
}

public Mammalian create(MammalianType mamalianType, PetSource source)
{
// Any!?
Any constructor = this.classMap.get(mamalianType);

return new constructor(source.name, source.bloodType);
}
}

もちろんHashMapのvalueをStringにしてRefrectionを利用することで、おそらくインスタンス化はできます。
しかし、そんなことのためにRefrectionを利用するのも筋が悪そうですし、何より私はそれを美しいと感じません。

それではこんなコードはどうでしょうか?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Factory
{
public Mammalian create(MammalianType mamalianType, PetSource source) throws Error
{
Mammalian result;
switch (mamalianType)
{
case Dog:
result = this.createDog(source);
break;
case Cat:
result = this.createCat(source);
break;
case Capibara:
result = this.createCapibara(source);
break;
default:
throw new Error('未定義');
}

return result;
}

private Dog createDog(PetSource source)
{
return new Dog(source.name, source.bloodType);
}

private Cat createCat(PetSource source)
{
return new Cat(source.name, source.bloodType);
}

private Dog createCapibara(PetSource source)
{
return new Capibara(source.name, source.bloodType);
}
}

それぞれのメソッドを定義しました。
私は、これでは以下のことを不安に感じてしまうのです。

  • createXXXメソッドの中身がDRYにできないのか。
  • Mammalianクラスの子クラスが増えた場合にメソッドとswitch文両方を追加しなければならないのか。

この不安は私が静的型付言語に慣れていないので発生する不安なのかはわかりません。

この点で、TypeScriptでは途中までうまくいきました。

TypeScriptでの実装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default class Factory
{
private classMap: Map<MammalianType, typeof Mammalian>;

constructor()
{
this.classMap = new Map<MammalianType, typeof Mammalian>();
this.classMap.set(MammalianType.Dog, Dog);
this.classMap.set(MammalianType.Cat, Cat);
this.classMap.set(MammalianType.Capibara, Capibara);
}

public create(mamalianType: MammalianType, source: PetSource): Mammalian
{
if (!this.classMap.has(mammalianType)) {
throw new Error('定義されていない哺乳類種別です。');
}

const mammalianConstructor: typeof Mammalian = this.classMap.get(mamalianType);

return new mammalianConstructor({name: source.name, bloodType: source.bloodType});
}
}

このコードには重大な問題があります。
Mammalianをabstractクラスにできないのです。

ここまでの私の要求は

  1. FactoryクラスをDRYに実装したい。
  2. Mammalianクラスを抽象クラスにしたい。

この二つですが、静的型付言語ではこれらの要求に沿う実装を行うことが難しく感じました。

しかし、TypeScriptはこのような要求に応えることのできる言語でした。

解決

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface MammalianConstructor {
new (source: PetSource) : Mammalian;
}

export default class Factory
{
private classMap: Map<MammalianType, MammalianConstructor>; // ここの型が変わった。

constructor()
{
this.classMap = new Map<MammalianType, MammalianConstructor>();
this.classMap.set(MammalianType.Dog, Dog);
this.classMap.set(MammalianType.Cat, Cat);
this.classMap.set(MammalianType.Capibara, Capibara);
}

public create(mamalianType: MammalianType, source: PetSource): Mammalian
{
if (!this.classMap.has(mammalianType)) {
throw new Error('定義されていない哺乳類種別です。');
}

const mammalianConstructor: MammalianConstructor = this.classMap.get(mamalianType);

return new mammalianConstructor(source);
}
}

なんということでしょう。

TypeScriptでは、interfaceにインスタンス化するときの返り値を定義することができるため、
intefaceへ依存することで、私の要求を全て叶えることができました。

私は、この発見をしたときすごく嬉しかったです。
同じようなことで困っている人がいたら、この記事が参考になれば幸いです。

余談

TypeScript

本日参加した、Nagoya Frontend User Groupのもくもく会で以下のようにすれば、Enumと派生クラスの網羅性を保証できると教えていただきました。
ありがとうございます。🤗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface MammalianConstructor {
new (source: PetSource) : Mammalian;
}

export default class Factory
{
private toMammalianConstructor() : MammalianConstructor
{
switch (type) {
case MarkerType.First: return FirstMarker;
case MarkerType.Second: return SecondMarker;
case MarkerType.DeliveryService: return DeliveryServiceMarker;
}

// never型の変数に代入してEnumがすべて網羅されているかチェックする。網羅されていなければコンパイルエラー
const unreachableCheck: never = type;
}

public create(mamalianType: MammalianType, source: PetSource): Mammalian
{
const mammalianConstructor: MammalianConstructor = this.classMap.get(mamalianType);

return new mammalianConstructor(source);
}
}

Java

あと、この記事を書いている時に以下のようにすればいけるんじゃ?という内容を見つけたのでメモ。(未検証)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Factory
{
private Map<MammalianType, Class<? exnteds Mammalian>> classMap;

public Factory()
{
this.classMap = new HashMap<MammalianType, Class<? exnteds Mammalian>>();

this.classMap.put(MammalianType.Dog, Dog);
this.classMap.put(MammalianType.Cat, Cat);
this.classMap.put(MammalianType.Capibara, Capibara);
}

public Mammalian create(MammalianType mamalianType, PetSource source)
{
Class<? exnteds Mammalian> constructor = this.classMap.get(mamalianType);

return new constructor(source.name, source.bloodType);
}
}

参考

Stack Overflow - Class constructor type in typescript?
Stack Overflow - How do I check that a switch block is exhaustive in TypeScript?
Nagoya Frontend User Group
いまさら!? Class クラス (2) : Class オブジェクトの取得方法