Webサービスを開発しているとき、ほぼ必ず書くことになる処理がオブジェクトの変換メソッドだ。
外部のAPIから受け取ったデータをクラスAにマッピングし、
その後クラスBに変換してアプリケーション内部でロジックを組み立てるということはよくある。
本記事では、この変換メソッドをどこに書くべきか?ということについて
筆者の意見を解説する。
結論
「クラスA→クラスB」への変換メソッドは、一般的には以下のどちらかで実装するのが良い。
- クラスBのファクトリメソッドとして書く
- 変換専用のファクトリクラスCを用意して、そこに書く
まずは①で実装し、処理が複雑化してしまう場合は②の選択肢を取る。
なぜか?
- オブジェクト指向の観点では、クラスBの仕様はクラスB内部に全て書くべきである
- 「クラスBの作り方」もクラスBの仕様であるため、「クラスAからこのようにしてクラスBを作ることができます」という処理はクラスBに書くのが良い
- 「作り方はそのクラス内部に書く」と決めることで、作り方のロジックが分散せず、勝手に変な方法でオブジェクトが生成されるのを防ぐことができる
具体例
外部のAPIから受け取ったデータUserApiResponse
(クラスA)から
アプリケーション内で使うためのデータUser
(クラスB)へ変換する
というシナリオを想定する。
※Getterやコンストラクタはアノテーションにより省略している
/**
* 外部APIから受け取ったデータ(クラスA相当)
*/
@Getter
@RequiredArgsConstructor
public class UserApiResponse {
private final String userName;
private final Integer userAge;
}
/**
* 内部ロジックで使うためのデータ(クラスB相当)
*/
@Getter
@RequiredArgsConstructor
public class User {
private final String name;
private final Integer age;
}
方法①:クラスBのファクトリメソッドとして書く
■実装例
/**
* 内部ロジックで使うためのデータ(クラスB相当)
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class User {
private final String name;
private final Integer age;
// ファクトリメソッド
public static User from(final UserApiResponse response) {
return new User(response.getUserName(), response.getUserAge());
}
}
■使い方
final UserApiResponse userApiResponse = new UserApiResponse("名前", 20);
// オブジェクトの変換
final User user = User.from(userApiResponse);
このように実装することで、以下のメリットが生まれる。
- Userクラスの実装を見るだけで、Userクラスのレシピが分かる
- ファクトリメソッド
User.from()
経由でしか作成できないため、UserApiResponse
以外の情報からUser
クラスが作られることはない(変なところで勝手に作られることがない)
コンストラクタで書かないのか?
コンストラクタで書けばよいのではないか?という疑問が生まれると思うが
専用のファクトリメソッドを用意することをおすすめする。
例外も投げやすいし、ユニットテストも書きやすい。
その代わり、コンストラクタ経由で勝手にオブジェクトが生成されないように@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
を付与して
コンストラクタの使用を制限しておくと良い。
「作り方が集約される」ことのメリット
Userに関する仕様で、「20歳以上を前提とする」ような仕様になった場合を想像すると分かりやすい。
その場合、「20歳以上」というバリデーションをUserクラスに実装すると仕様がそのクラスに集約されてわかりやすくなる。
■「20歳以上」を仕様に入れたAdultUserクラス
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class AdultUser {
private final String name;
private final Integer age;
// ファクトリメソッド
public static AdultUser from(final UserApiResponse response) {
// 20歳未満の場合は、そもそもオブジェクトを作らせない
if (response.getUserAge() < 20) {
throw new RuntimeException("未成年です");
}
return new AdultUser(response.getUserName(), response.getUserAge());
}
}
このように実装することで、アプリケーション内に記載されている全てのAdultUserクラスは
「絶対にage>=20である」と断言することができる。
誤って未成年のオブジェクトが生成されることはありえない上に、
年齢制限が30歳になった場合でも、AdultUserクラスを書き換えるだけでOKとなる。
方法②:変換専用のファクトリクラスCを用意して、そこに書く
基本的な方針として、「クラスBの作り方」はクラスBに書きたいところではあるが、
そうするのが難しいケースがある。
それが「クラスBの作り方が複雑」である場合だ。
複雑なオブジェクトを作成する場合、その生成過程も複雑になることが多い。
- クラスBを作る前に、DBに存在チェックをしたい
- クラスAのデータ構造が複雑すぎて、クラスBのファクトリメソッドに書くのは重い
特に、ファクトリクラスはstaticメソッドで記述するのでテストが書きにくい。
テストが書きにくいメソッドを複雑化するのは避けたいところである。
■Userクラス生成専用のUserFactoryクラス
@Component
@RequiredArgsConstructor
public class UserFactory {
private final UserRepository userRepository;
public User create(final UserApiResponse response) {
// 外部に存在確認をして、存在しなかった場合はエラー
if (!userRepository.exists(response.getUserName())) {
throw new RuntimeException("ユーザーが存在しません");
}
return new User(response.getUserName(), response.getUserAge());
}
}
このようにすることで、UserRepository経由でDBなどの存在確認を行った上で
オブジェクトを生成することができる。
このとき、Userクラスのコンストラクタをパッケージプライベートにしておくことで、
変なオブジェクト作成も(絶対ではないが)防ぐことができる。
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class User {
private final String name;
private final Integer age;
}
この方法はしっかりメリデメあるので
「そもそもクラスBを解体してシンプルにできないか?」をまずは考えてほしい。
メリット | staticメソッド(ファクトリメソッド)が複雑化しないので、テストしやすい |
デメリット | クラスBに全ての仕様を書ききることができず、仕様を知るためにクラスCを参照する必要がある |
まとめ
- まずはクラスBのファクトリメソッドとして書く
- 処理が複雑化するようであれば、新たにクラスBの生成に専念するクラスCを作成する