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


はじめに

こんにちはインターン生の鍋島(仮)です。
10月あたりからインターンとして入り、Scalaの基礎的な部分をメインに教えてもらっています。

これから、インターンで得た知見をアウトプットするため、記事を書いていこうと思います。
まずは、Scalaを使ってデザインパターンを解説する記事を書いていこうと思います。
週1ペースで更新をする予定です。遅れたらごめんなさい

Strategyパターンとは

さて本題に入ります。

Strategyパターンとは、状況に応じて処理を変更したい時に、その処理を戦略として切り出しておくデザインパターンです。
うまく活用することで、ソースコードの見通しがよくなり、拡張性を上げられます。
Strategyパターンでは、戦略をビジネスロジックとは別のクラスに定義します。
例えば買い物をする際、事前に購入するものを決めておくこともあれば、無目的に店に向かい、その場で決定することもあります。この場合、どのように商品を選ぶかを戦略として定義します。
そして、使用する際はこの戦略を差し替えながら使用します。

以下サンプルコードです。

サンプルコード

このサンプルコードは、購入する商品を選ぶ戦略を差し替えながら、買い物をシミュレーションするものです。

今回実装する買い物戦略は、この2つです。
- 商品カテゴリごとに予算割合を事前に決めておく戦略
- 特定カテゴリの商品だけを購入判断する戦略

import scala.math._

object Main extends App {
  val user = User(10000, 10000, 0.2, 0.5, 0.2)
  val products = new ProductRepository().findAll(20)
  val shop = Shop(products)
  //  今回は別の戦略を使うため
  //  val strategy = new UnlimitedGenreStrategy() 
  val strategy = new OneGenreStrategy("toy")
  User.shopping(user)(shop, strategy)
}

class ProductRepository() {
  private val genres = Seq("beauty", "food", "toy")

  def findAll(num: Int): Seq[Product] = for {
    i <- (1 to num)
  } yield {
    val genre = genres(floor(random() * genres.length).toInt)
    Product(i, s"$genre$i", floor(random() * 5000).toInt, genre)
  }
}

case class Product(id: Int, name: String, price: Int, genre: String)
// 所持金,予算,美容用品の予算割合,食品の予算割合,おもちゃの予算割合
case class User(wallet: Int, budget: Int, maxBeautyPercentage: Double, maxFoodPercentage: Double, maxToyPercentage: Double) {
  private def notBuy(product: Product): Unit = println(s"${product.price}円の${product.name}を購入しませんでした")
}

object User {
  def shopping(user: User)(shop: Shop, strategy: ShoppingStrategy): Unit = {
    // Userをイミュータブルにするために、購入した時に作り直している
    shop.products.foldLeft(user) { (u, product) =>
      // strategyを呼び出して購入するかの判断をしている
      if (strategy.judgeBuy(u, product)) {
        buy(product, u)
      } else {
        u.notBuy(product)
        u
      }
    }
  }

  private def buy(product: Product, user: User): User = {
    val balance = user.wallet - product.price
    println(s"${product.price}円の${product.name} を購入した! 残り$balance 円")
    User(balance, user.budget, user.maxBeautyPercentage, user.maxFoodPercentage, user.maxToyPercentage)
  }
}


case class Shop(products: Seq[Product])

trait ShoppingStrategy {
  // 買い物の戦略
  def judgeBuy(user: User, product: Product): Boolean
}

// 商品カテゴリごとに予算割合を事前に決めておく戦略。
class UnlimitedGenreStrategy() extends ShoppingStrategy {
  override def judgeBuy(user: User, product: Product): Boolean = {
    //  商品の予算割合を計算
    val budgetPercentage = product.price.toDouble / user.budget
    product.genre match {
      case _ if user.wallet < product.price => false
      case "beauty" => budgetPercentage <= user.maxBeautyPercentage
      case "food" => budgetPercentage <= user.maxFoodPercentage
      case "toy" => budgetPercentage <= user.maxToyPercentage
    }
  }
}

// 特定カテゴリの商品だけを購入判断する戦略
class OneGenreStrategy(genre: String) extends ShoppingStrategy {
  override def judgeBuy(user: User, product: Product): Boolean = product.genre match {
    case _ if user.wallet < product.price => false
    case "beauty" if "beauty" == genre => true
    case "food" if "food" == genre => true
    case "toy" if "toy" == genre => true
    case _ => false
  }
}

解説

まずはそれぞれのクラスの役割を見ていきましょう

class ProductRepository ここからProductを取得します
case class Product 商品です
case class User 買い物をする人です
case class Shop 買い物をする店舗です。店舗内に商品が並んでいます
trait ShoppingStrategy 今回実装する、買い物の戦略です
class UnlimitedGenreStrategy 商品カテゴリごとに予算割合を事前に決めておく戦略です
class OneGenreStrategy 特定カテゴリの商品だけを購入判断する戦略です

次に、実装を見ていきましょう。
買い物の戦略は、ShoppingStrategyを継承して作ります。
事前に説明したように、今回は2つ戦略を定義します。

まず、OneGenreStrategyは特定カテゴリの商品だけを購入判断する戦略なので、商品のカテゴリとコンストラクタで受け取ったカテゴリが同じであればTrueを返すように実装します。
次に、UnlimitedGenreStrategyは商品カテゴリごとに予算割合を事前に決めておく戦略なので、ユーザーのそれぞれの予算割合を超えていない商品にTrueを返すように実装します。
これらをそれぞれのjudgeBuy関数に実装します。

これで戦略の定義が出来ました。

買い物を行う際は、User.shopping関数に戦略を渡します。
同じShoppingStrategyを実装しているため、どの戦略を渡しても正常に動作します。

最後に、私が社内でまさかりを投げられたポイントを紹介します。
今回の戦略は、"購入する商品を選ぶ"戦略なので、それ以上のことをやっていはいけません。

私は、ShoppingStrategyのjudgeBuy関数で購入処理まで行ってしまっていましたが、それでは正しく関心ごとの分離ができていませんでした。
Strategyパターンでは、ビジネスロジックと戦略部分を分けて実装することが重要です。Strategyには戦略以上のことを書かないようにしましょう。

最後に

いかがでしたか?このパターンのメリットが伝わりましたかね?

新しく学んだものでも、他人に説明することで理解が深まります。

今までの説明でおおよそのイメージがついたかもしれませんが、いざコードを書いてみようとするとわからなくなってしまうものです。

以上、Strategyパターンの解説でした。最後まで読んでいただきありがとうございます!