データベースのモック、作ったAPIのIN/OUTのダミーデータ、外部システムのモック、用途はなんでも良いけどテストデータってしょっちゅう作るじゃん。
でも保守は面倒だし正しく使うのも使わせるのも大変だし、どうにかならんもんかとちょっと考えてみた。
型型したいのでサンプルコードはJava、でも別に何の言語でも良いと思う。
お題
仮に契約というデータがあり、内容が以下の様な感じだとする。
{userName=ほげ太郎, plan=NORMAL, limit=UNLIMITED, items=[Item1, Item2], coupon=}
key | value |
---|---|
userName | 必須 |
plan | NORMAL/PREMIUM |
limit | UNLIMITED/LIMITED |
items | 0..* |
coupon | NORMAL時はなし、PREMIUM時は必須 |
このハッシュを作る方法を考えようと言う事。
方法
過去に所属したチームでは.xml
や.json
や.txt
で全パターン作って用意しておいたり、以下の様なJavaで書かれたテストデータもあった。
public static Map<String, Object> 商品複数_割引なし() {
Map<String, Object> map = new HashMap<>();
map.put("userName", "ほげ太郎");
map.put("plan", "NORMAL");
map.put("limit", "UNLIMITED");
map.put("items", Arrays.asList("Item1", "Item2"));
return map;
}
それらに共通する問題は「全パターン用意すると膨大になる点」と「couponはNORMAL時はなし、PREMIUM時は必須
の様な仕様が漏れ易い点」だ。
テキトーに数えてみても、例えばitems
は0件
、1件
、2件
の場合を用意するとして
plan2種 x limit2種 x items3種 = 12件
の用意が必要になってしまう。
数年の仕様変更を経てそれにplan2種
と支払い方法3種
が増えたりして、LIMITED
の場合はコンビニ決済
は不可能、なんてなってきたら絶対に破綻する、断言する。
Builderを作る
Builderを使って部分部分を組み立てる方法だけ用意し、使う側が欲しい様に組み立てられる様にしてみる。
また、最終的にMap
が欲しいからと言って内部での中間生成状態までMap
である必要もない。
public class ContractBuilder {
private String userName;
private Plan plan;
private Limit limit;
private List<String> items = new ArrayList<>();
private Optional<String> coupon = Optional.empty();
public ContractBuilder userName(String userName) {
this.userName = userName;
return this;
}
public ContractBuilder normalPlan() {
this.plan = Plan.NORMAL;
return this;
}
public ContractBuilder premiumPlan(String coupon) {
this.plan = Plan.PREMIUM;
this.coupon = Optional.of(coupon);
return this;
}
public ContractBuilder limited() {
this.limit = Limit.LIMITED;
return this;
}
public ContractBuilder unlimited() {
this.limit = Limit.UNLIMITED;
return this;
}
public ContractBuilder item(String item) {
this.items.add(item);
return this;
}
}
この方針だと全てのパターンをこれだけの行数で網羅することが出来る。
一気にフル要素を手に入れるのではなく、各要素をだんだんと組み立てて行くイメージで生成する。
で、最後に一度だけ欲しい形に変換して吐き出せば良い。
public Contract build() {
return new Contract(userName, plan, limit, items, coupon);
}
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("userName", userName);
map.put("plan", plan);
map.put("limit", limit);
map.put("items", items);
map.put("coupon", coupon.orElse(""));
return map;
}
保持している値を好きな形に一度だけ変換すれば良いので、Map
以外にもすぐ吐ける。
this
を返しているので、こんな感じで連結しながら使う
Map contract = new ContractBuilder()
.userName("ほげ太郎")
.normalPlan()
.limited()
.item("Item1")
.item("Item2")
.toMap();
Map contract = new ContractBuilder()
.userName("ほげ太郎")
.premiumPlan("ABC")
.unlimited()
.item("Item1")
.item("Item2")
.toMap();
メリット
先述の通り、これだけで全パターンが網羅できるし例えば支払い方法が2パターン増えても、メソッドを2つ増やすだけで対応が済む。
もう1つのポイントは、例えばpremiumPlan
はcoupon
を引数に取るので「PREMIUM
だけどcoupon
の設定を忘れる」という事がないし、
「limited()
の場合のみになんらかの条件を書き足す」という事が容易であること。
デメリット
利用する側が正しく組み立てる必要があるので、不正な状態を生成してしまう可能性がある。
Map contract = new ContractBuilder()
.toMap();
Map contract = new ContractBuilder()
.userName("ほげ太郎")
.normalPlan()
.normalPlan()
.normalPlan()
.toMap();
これはどこかでぬるぽを起こすので避けたい。
また、エディタの補完対象を見ても今一使い方がわからない点も問題だ。
例えば「normalPlan()
とpremiumPlan()
が排他なのか」、「plan
とlimit
は排他なのか両方設定するべきなのか」、「順番やメソッド間の依存はあるのか」、「何を満たせばtoMap
を実行して大丈夫なのか」と言ったことが察せない。
1要素を選択する毎に異なるクラスを返す
上記の問題を解決するには、return this;
の所をそれぞれ違う型にする方法がある。
public class ContractStrictBuilder {
private String userName;
private Plan plan;
private Limit limit;
private List<String> items = new ArrayList<>();
private Optional<String> coupon = Optional.empty();
public _Plan userName(String _userName) {
userName = _userName;
return new _Plan();
}
public class _Plan {
public _Limit normalPlan() {
plan = Plan.NORMAL;
return new _Limit();
}
public _Limit premiumPlan(String _coupon) {
plan = Plan.PREMIUM;
coupon = Optional.of(_coupon);
return new _Limit();
}
}
public class _Limit {
public _Item limited() {
limit = Limit.LIMITED;
return new _Item();
}
public _Item unlimited() {
limit = Limit.UNLIMITED;
return new _Item();
}
}
public class _Item {
public _Item item(String item) {
items.add(item);
return new _Item();
}
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("userName", userName);
map.put("plan", plan);
map.put("limit", limit);
map.put("items", items);
map.put("coupon", coupon.orElse(""));
return map;
}
}
}
この様に2択を迫る異なるクラスを次々と返す事で利用者は正しく組み立てることが出来る。
メリット
下のキャプチャが示す通り、「必ずこの2つから選ぶ」「ここまで来たらtoMap
を実行して良い」という事がエディタに教えてもらえるのが最大のメリット。
次はplan
を必ず設定する
この際にPREMIUM
の場合は必ずcoupon
を設定する
次はitems
の設定だが、toMap
も実行出来るのでitems
は任意項目である事がわかる
デメリット
当然だが実装コストはかかる。
まとめ
2つのBuilderを考えてみたが、どちらが優れているかという話ではなくて適材適所だと思う。
ただ後者の例はエディタで次々と「どっち?」「どっち?」って聞かれるのでなんか楽しくて好きw
使うだけなら圧倒的に後者が使い易いしね。
そんな訳でぜひ良いパターン管理を
おつノシ