Scala で Builder Pattern

ScalaでJava like なBuilder Patternを書く機会は自分の場合あまり無い(Static Factoryで事足りることが多い)のですが、少し古いJavaのプロダクトのリファクタをした際にBuilder Patternで生成処理を書き直したので、Scalaの場合はどうなるのか見ていこうと思います。

Generalized Typed Constraint の勉強にも一部なるところもあったりします。

JavaにおけるBuilder Pattern

JavaにおけるBuilder Pattern は GoF と Effective Java が有名かなと思います。Builder Patternの説明としては、Effective Javaの方が使い所とかが個人的には分かりやすい気がします。コードとしても、Effective Javaの書き方の方が、コードが散らばらないので好みですね。※Effective Javaはクラス内に static class として、Builder クラスを定義するが、GoFの方は、Person クラスと別に、PersonBuilder のようなクラスを作る。

Effective Java 第3版
GoF本のJava版

JavaにおけるBuilder Pattern を使いたくなるときと、BuilderPattern以前の書き方

Builder Patternが使いたくなるのは、必須では無いけれど場合に応じて外から値を渡してオブジェクトを生成したい時、オプショナルパラメータが多い時に使いたくなります。オプショナルパラメータが多い時に選択される伝統的なパターンとして、Effective Javaではテレスコーピング・コンストラクタ(Telescoping Constructor)とJavaBeansパターンが紹介されていました。

テレスコーピング・コンストラクタは、単純にコンストラクタを複数用意するパターンです。

class NutritionFacts {
private final int servingSize; // 必須
private final int servings; // 必須
private final int calories; // オプション
private final int fat; // オプション
private final int sodium; // オプション
private final int carbohydrate; // オプション
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}

6個のパラメータでこれなので、コンストラクタがいくつあっても足りないですし、上から順番にしか対応できてないので、例えば、carbohydrate は設定したいけど、他のパラメータはデフォルトパラメータで良いとかには対応できません。

私がこの業界に入った時にはすでにJavaBeansパターンは廃れてましたが、古いプロダクトだとやはりよく使われています。JavaBeans パターンは setter を使って行うパターンです。そもそも不変でないので、バグが入り込みやすいですし、Scalaではまず出会わないですね。でもやっぱり古いJavaのプロダクトだとよく見ます。setterを排除したい時に、Builder Patternに置き換えるのはリファクタとしてはやりやすいなと思います。

class NutritionFacts {
private int servingSize = –1; // 必須
private int servings = –1; // 必須
private int calories = 0; // オプション
private int fat = 0; // オプション
private int sodium = 0; // オプション
private int carbohydrate = 0; // オプション
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public void setServings(int servings) {
this.servings = servings;
}
public void setCalories(int calories) {
this.calories = calories;
}
public void setFat(int fat) {
this.fat = fat;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}

Scalaを使っていると、ほぼ直面しない、値がミュータブルな状態なってしまうのがJavaBeansパターンの最悪の欠点ですね。最新のJavaは知らないですが、基本的に値がミュータブルになってるJavaといえど、イミュータブルの方が扱いやすいことには変わりありません。しかし、JavaBeansパターンはテレスコーピングパターンよりも柔軟です。値変えたいもののsetterのみを呼び出せば良いわけなので。

Builder Pattern (Java)

テレスコーピングパターンも、JavaBeansパターンもどちらも使いづらい。そこで使われるのがBuilder Patternです。(Effective Java ver.)

class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// 必須パラメータ
private final int servingSize;
private final int servings;
// オプショナルパラメータ。デフォルト値を入れておく。
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingsSize, int servings) {
this.servingSize = servingsSize;
this.servings = servings;
}
public Builder calories(int calories) {
this.calories = calories;
return this;
}
public Builder fat(int fat) {
this.fat = fat;
return this;
}
public Builder sodium(int sodium) {
this.sodium = sodium;
return this;
}
public Builder carbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium =builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
class Main {
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.build();
}
}

呼び出しのところを見ればわかるように、Builderからメソッドチェーンのように生成処理が書けます。

ScalaにおけるBuilder Pattern

Javaの方を見てわかるように、基本的にはオプショナルパラメータはデフォルト値で良くて、時々オプショナルパラメータの値を外部から渡したい。そしてそのようなオプショナルパラメータが多い時にBuilder Patternは活躍します。

case class のデフォルトパラメータを使う

Scalaでデフォルトパラメータを使う場合は、すごく簡単にかけます。

case class NutritionFacts(
servingSize: Int,
servings: Int,
calories: Int = 0,
fat: Int = 0,
sodium: Int = 0,
carbohydrate: Int = 0
)
val nf = NutritionFacts(1, 1, fat = 10)

JavaのBuilder Patternとこれで同じことができています。コード量、可読性、段違いですね。

呼び出しも、オプショナルのところは名前付きで渡してあげれば自由に書けます。

Java likeに書いてみる

上のように簡単に書けるのはわかりましたが、Javaっぽく書くとどうなるのかも簡単に見ておきます。

class NutrionFacts(builder: NutrionFacts.Builder) {
val servingSize: Int = builder.servingSize
val sergings: Int = builder.sergings
val calories: Int = builder.calories
val fat: Int = builder.fat
val sodium: Int = builder.sodium
val carbohydrate: Int = builder.carbohydrate
}
object NutrionFacts {
class Builder {
var servingSize: Int
var sergings: Int
var calories = 0
var fat = 0
var sodium = 0
var carbohydrate = 0
def setServingSize(servingSize: Int): Builder = {
this.sergingSize = sergingSize
this
}
def setServings(servings: Int): Builder = {
this.sergings = sergings
this
}
def setCalories(calories: Int): Builder = {
this.calories = calories
this
}
def setFat(fat: Int): Builder = {
this.fat = fat
this
}
def setSodium(sodium: Int): Builder = {
this.sodium = sodium
this
}
def setCarbohydrate(carbohydratre: Int) = {
this.carbohydrate = carbohydrate
this
}
def build = new NutrionFacts(this)
}
}

static class だった Builder がScalaの場合はコンパニオンオブジェクトに移動するくらいですね。Javaと違って面倒なだけで、特に恩恵がないです。

Generalized type constrains

なんならここからが本題。Generalized type constrains を利用してさらにレベルアップさせます。

Scala 系のブログを漁ってたら少し古い下記の記事を見つけて知りました。コップ本にも、多分載って無い内容ですね。なので使い道とか調べるの難しい。。

上記の記事に記載がありますが

Scala2.8から、Predefに<:<とか=:=とかが定義されていて、これなんだろ?とずーっと疑問だった訳ですよ。で、ついったーで質問投げてたらやっと理解できました。

“generalized type constraints”というヤツで、型パラメータに与えられた型が、特定の条件を満たす場合にのみ呼び出せるメソッドを定義できるというものです。しかもコンパイル時に静的にチェックされる!! これはスゴい!!

https://yuroyoro.hatenablog.com/entry/20100914/1284471301

type-safe builder

すこし話は戻って、オブジェクトの生成時に、例えば特定の順序で初期化する必要があったりすることがあります(あるそうです。自分はそういうシーンにはまだ遭遇したことがない)。これまでの Builder Pattern の実装では、実行時にしかそのことを検証できませんでしたが、type-safe builder と呼ばれる実装方法を使うと、コンパイル時に検証できるようになります。

class Person(
val firstName: String,
val lastName: String,
val age: Int
) {
protected def this() = this("", "", 0)
protected def this(pb: Person.Builder[_]) = this(pb.firstName, pb.lastName, pb.age)
}
object Person {
sealed trait BuildStep
sealed trait HasFirstName extends BuildStep
sealed trait HasLastName extends BuildStep
class Builder[PassedStep <: BuildStep] private (
var firstName: String,
var lastName: String,
var age: Int
) {
protected def this() = this("", "", 0)
protected def this(pb: Builder[_]) = this(
pb.firstName,
pb.lastName,
pb.age
)
def setFirstName(firstName: String): Builder[HasFirstName] = {
this.firstName = firstName
new Builder[HasFirstName](this)
}
def setLastName(lastName: String)(implicit ev: PassedStep =:= HasFirstName): Builder[HasLastName] = {
this.lastName = lastName
new Builder[HasLastName](this)
}
def setAge(age: Int): Builder[PassedStep] = {
this.age = age
this
}
def build(implicit ev: PassedStep =:= HasLastName): Person = {
new Person(firstName, lastName, age)
}
}
object Builder {
def apply() = new Builder[BuildStep]()
}
}
object Main {
def main(args: Array[String]): Unit = {
val person = Person
.Builder()
.setFirstName("太郎")
.setLastName("田中")
.setAge(33)
.build
println(s"Person: ${person.lastName} ${person.firstName}, ${person.age}")
}
}

これで、例えば setFirstName を抜いて build しようとしてもIntelliJ なら静的解析の段階でエラーを出してくれますし、当然コンパイルエラーにもなります。順序を保証したい時とかに便利ですね。

最後に

Java like なBuilder PatternをScalaで使うことは基本的にはそんななく、多くはstatic factoryや、case class のデフォルトパラメータで十分だろうなと思います。type-safe builder に関してなかなかこれが必要な制約下になることはあんまり想像つきませんが、有用かなと思います。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。