Scalaでデザインパターン ~Builderパターン~


はじめに

こんにちは。
会社では「鍋島」と呼ばれていますが、この名前もようやく慣れてきました。

前回はStrategyパターンの記事を投稿しました。
今回も同様に、次のデザインパターンについての説明をしていきたいと思います。
また買い物シリーズです

Builderパターン

今回はBuilderパターンの説明をします。
Builderパターンとは、複雑なインスタンスを作る際にそのクラスを生成するためのBuilderクラスを作ります。

たとえば、Aというクラスのインスタンスを作るとしましょう。
Aというクラスは非常に多くのパラメータがあるとします。
これを毎回すべてコンストラクタに渡すと、このような問題が起こることがあります。

  • パラメータが多すぎてソースコードの見通しが悪くなってしまう
  • 必須のパラメータが一部しかないときも、コンストラクタですべて設定をする必要が出てきてしまう

そういった時にBuilderパターンを利用します。

Builderパターンを利用した場合、その問題となっている処理をBuilderクラスで行うようすることで、そのクラスを使う側は不要な処理を書く必要がなくなります。

このように、コードの見通しが良くなり、インスタンス作成が楽になります。

サンプルコード

例えば買い物に行った際に、レジで会計をしてレシートができるフローを考えてみましょう。
レジでは商品をバーコードリーダーで、一つずつ読み取っていきます。
そして読み取った商品をレシートに記録をし、すべての会計が完了するとレシートを出力します。
この場合、「レシート」インスタンスを構築するためのパラメータ(商品)の数が不定とななります。
このケースは、よりBuilderパターンが有効に作用すると考えられます。

更に今回は、会計中の一部で商品の購入をキャンセルするパターンも考慮します。
この場合、Builderに対して商品の削除を行います。
では、早速実装してみましょう。

import scala.math._

object Main extends App {
  val products = new ProductRepository().create(10)
  val user = User("nabeshi", 10000, products)
  val register = new Register(new ReceiptBuilder())
  val receipt = register
    .addProducts(user.shoppingBasket)
    .cancelProduct(user.shoppingBasket(1))
    .calculate(user.money)
  receipt.print()
}

// ReceiptBuilderを操作するクラス。ユーザーが商品を買う時はこのクラスを使用する
class Register(builder: ReceiptBuilder) {
  // 商品をbuilderに追加する
  def addProducts(products: Seq[Product]): Register= {
    new Register(products.foldLeft(builder)((builder, product) => builder.addProduct(product)))
  }
  // 商品をキャンセルする
  def cancelProduct(product: Product): Register = {
    new Register(builder.cancelProduct(product))
  }
  // 計算をしてレシートを出力する
  def calculate(money: Int): Receipt = builder.build(money)
}

// 商品を取り出すためのクラス
class ProductRepository() {
  // 商品を取り出す
  def create(num: Int): List[Product] = (1 to num).toList.map(i => Product(s"product$i", floor(random() * 1000).toInt))
}

// 商品
case class Product(name: String, price: Int)

// 購入した商品をまとめるクラス
case class Receipt(products: Seq[Product], payment: Int, price: Int) {
  // レシートを表示する
  def print(): Unit = {
    products.foreach(product => println(s"${product.name}: ${product.price} 円"))
    println(s"合計: $price 円")
    println(s"お預かり: $payment 円")
    println(s"お釣り: ${payment - price} 円")
  }
}

// builderクラス。商品を追加していきbuild関数が呼び出されたタイミングで、レシートを生成している
class ReceiptBuilder(private val productList: Seq[Product] = Seq()) {
  // レジを通した商品を追加していく
  def addProduct(product: Product): ReceiptBuilder = new ReceiptBuilder(productList :+ product)
  // 商品をキャンセルする
  def cancelProduct(product: Product): ReceiptBuilder = new ReceiptBuilder(productList.diff(Seq(product)))
  // レシートを生成する
  def build(money: Int): Receipt = Receipt(productList, money, productList.map(_.price).sum)
}

// 商品を買うユーザー
case class User(name: String, money: Int, shoppingBasket: Seq[Product])

解説

それでは、サンプルコードの解説をしていきます。
まずは、それぞれのクラス定義について解説します。

  • Register ここに購入する商品を入れることでレシートを生成します。
  • ProductRepository ここから商品を取り出します。
  • Product 商品です。
  • Receipt レシートオブジェクトです。このインスタンスを生成します。
  • ReceiptBuilder 今回のBuilderです。ここでレシートを生成します。

次にBuilderパターンをどのように利用しているかを解説します。
上でも書きましたが、ReceiptBuilderが今回のBuilderパターンの肝です。
ReceipBuilderによってReceiptを作ります。
このReceipBuilderに、バーコードリーダーで読み取った商品を、addProduct関数で追加していきます。
すべての商品を追加し終わったら、accouting関数を呼び出すことでレシートを生成しています。

このようにして、ReceipBuilderクラスは、レシート生成するために必要な処理を集約します。

最後に

最後まで読んでくださりありがとうございます。
Builderパターンのメリットが伝わりましたでしょうか?
よかったら利用してみてください

以上です。

次回は、FactoryMethodパターンを解説していきます。