LL脳がscalaの勉強を始めたよ その93


Scalaコップ本の27章の続きをやっていきますよ。27章はプログラムのモジュール化についてで、今回は抽象化からやっていきますよ(`・ω・´)

とりあえず前回までに構築したモジュール化サンプルのレシピ検索アプリは次のような感じです。まずは管理する食品とレシピオブジェクトを次のような感じです。

// 食品を表現する抽象クラスです
abstract class Food(val name:String){
  override def toString = name
}

// レシピを表現するクラスです
class Recipe(
  // 名前の定義です 
  val name:String,
  // 材料はFoodのリストで保持します
  val ingredients:List[Food],
  // 作り方は文字列で表現デス
  val instructions:String
){
  override def toString = name
}

サンプルデータはこんな感じでした

// テスト用の食品を定義
object Apple extends Food("Apple")
object Orange extends Food("Orange")
object Cream extends Food("Cream")
object Sugar extends Food("Sugar")

// テスト用のレシピを定義しますね
object FruitSalad extends Recipe(
  "fruit salad",
  List(Apple, Orange, Cream, Sugar),
  "全てをまぜます"
)

そしてデータを保持するDBとDBの内容を検索するDBブラウザですね。

package org.test.recipe

object SimpleDatabase{
  def allFoods = List(Apple, Orange, Cream, Sugar)
  def foodNamed(name:String):Option[Food] = 
    allFoods.find(_.name == name)
  def allRecipes:List[Recipe] = List(FruitSalad)
  // 食品分類を表現するケースクラスを定義しますよ
  case class FoodCategory(name:String, foods:List[Food])
  // privete宣言によって他のプログラムに影響を出さずに
  // 変更できるように食品分類一覧を定義します
  private var categories = List(
    FoodCategory("fruits", List(Apple, Orange)),
    FoodCategory("misc", List(Cream, Sugar)))
  // 全ての食品分類を返しますね
  def allCategories = categories
}

object SimpleBrowser {
  def recipesUsing(food:Food) =
    SimpleDatabase.allRecipes.filter(recipe => 
      recipe.ingredients.contains(food))
  // 食品分類を返します 
  def displayCategory(category:SimpleDatabase.FoodCategory){
    println(category)
  }
}

今回も上記をサンプルとして進めていきますよー

抽象化

前回までに構築したサンプルでは2つのオブジェクトの形式でDBモジュールとDBブラウザモジュールに分割しているのですが、実際のところDBブラウザモジュールからDBモジュールに下記のようなハードリンクが残っているので完全にはモジュラースタイルになっていないのデス(´・ω・`)

def recipesUsing(food:Food) =
  // DBブラウザモジュール内でDBモジュールを呼び出しております
  SimpleDatabase.allRecipes.filter(recipe => 
    recipe.ingredients.contains(food))

上記のように直接コードの中で呼び出しているので、呼び出すモジュールが変更された場合は再コンパイルする必要があるみたいです(´・ω・`)

仮にハードリンクを無くすために各モジュールをプラグイン化しようとした場合はコードの重複を避ける実装を考える必要がありますね。例えば同じコードベースで複数のレシピDBをサポートしつつ複数のDB毎のブラウザをつくるようにした場合に各ブラウザが同じインスタンスを再利用したりみたいです(´・ω・`)DB実装に応じてDBブラウザの再設定をするような動作ですかネ

上記のような再利用率の高いプラグインを実現するためにコップ本的オススメはオブジェクトであるモジュールのテンプレートと言えるクラスを利用することみたいです。例えばこんな感じでブラウザの定義をクラスで行い、使うDBを抽象メンバーとして指定してやります。

// DBブラウザを抽象クラスとして実装してやります
abstract class Browser {
  // 抽象メンバーとしてDBを指定しますね
  val database:Database
  // 抽象メンバーであるDatabaseを利用するように変更します
  def recipesUsing(food:Food) = 
    database.allRecipes.filter(recipe => 
      recipe.ingredients.contains(food))
  def displayCategory(category:database.FoodCategory){
    println(category)
  }
}

上記DBブラウザクラスで利用出来るようにDB自体もクラスになりますね(´・ω・`)共通化のために全てのDBに共通するものをできるだけ詰め込みつつ、各DB毎に異なるものは別個に再定義するような宣言を行いますデス。

// DBブラウザクラスから抽象メンバーとして使えるようにクラス化します
abstract class Database {
  // DBの中身になるデータは抽象メンバとして
  // 各DB毎に実装します
  def allFoods:List[Food]
  def allRecipes:List[Recipe]
  // 共通化の部品はこちらで定義しますね
  def foodNamed(name:String) = 
    allFoods.find(f => f.name == name)
  // カテゴリーデータも各DBごとに定義しますネ
  case class FoodCategory(name:String, foods:List[Food])
  def allCategories:List[FoodCategory]
}

上で定義したテンプレート的クラスを継承しつつSimpleDatabaseオブジェクトを実装してやりますな(`・ω・´)

// DBクラスを継承してDBオブジェクトを定義します
object SimpleDatabase extends Database {
  // DBの中身やカテゴリを定義してやります
  def allFoods = List(Apple, Orange, Cream, Sugar)
  def allRecipes:List[Recipe] = List(FruitSalad)
  private var categories = List(
    FoodCategory("fruits", List(Apple, Orange)),
    FoodCategory("misc", List(Cream, Sugar)))
  def allCategories = categories
}

新しくクラスベースで定義したSimpleDatabaseを利用するブラウザオブジェクトもテンプレートとなるクラスを継承して作りますネ(´・ω・`)

// ブラウザクラスを継承します
object SimpleBrowser extends Browser {
  // 必要な処理は全てクラスで定義したので
  // 利用するDBのみを指定してやります
  val database = SimpleDatabase
}

さて、実際に動かしてみますよー、前回定義したものと同じように動作しますね(`・ω・´)

scala> val apple = SimpleDatabase.foodNamed("Apple").get
apple: Food = Apple

scala> SimpleBrowser.recipesUsing(apple)
res1: List[Recipe] = List(fruit salad)

プラグイン化したことで各モジュールの実装が楽になった上に、クラス定義を使いまわせるので次のようにちょろっと改造した複数のDBとDBブラウザのセットを作成することが出来ますねー

// 新しいモックDBを定義しますデス
object StudentDatabase extends Database {
  // Foodを拡張して冷凍食品オブジェクトを定義します
  object FrozenFood extends Food("FrozenFood")
  // レンジでチンするレシピを定義です
  object HeatItUp extends Recipe(
    "heat it up",
    List(FrozenFood),
    "Microwave the 'food' for 10 minutes.")
  // レシピと食品とカテゴリーを定義子ますね
  def allFoods = List(FrozenFood)
  def allRecipes = List(HeatItUp)
  def allCategories = List(
    FoodCategory("edible", List(FrozenFood)))
}

//  StudentDatabase 向けブラウザを定義します
object StudentBrowser extends Browser {
  val database = StudentDatabase
}

んじゃ実行してみますよー

scala> val frozen = StudentDatabase.foodNamed("FrozenFood").get
frozen: Food = FrozenFood

scala> StudentBrowser.recipesUsing(frozen)
res2: List[Recipe] = List(heat it up)

おお、できましたー。それにしても学生のレシピが冷凍食品ってのはちょっと物悲しいですな(´・ω・`)

モジュールのトレイトへの分割

トレイトを利用することで大きなモジュールを複数のファイルに分割することが出来るみたいです。これによって一つのファイルに巨大なモジュールを無理やり詰め込むようなコトを回避できるみたいですね。例としてサンプルのDatabaseのメインファイルからカテゴリー関連のコードを切り離してみますよ

まずはカテゴリー関連の処理をトレイトとしてまとめます

trait FoodCategories {
  case class FoodCategory(name:String, foods:List[Food])
  def allCategories:List[FoodCategory]
}

上記のトレイトを使ってDatabaseクラスを再構築してみますね

// ミックスインしてカテゴリー機能を付加しまうネ
abstract class Database extends FoodCategories {
  def allFoods:List[Food]
  def allRecipes:List[Recipe]
  def foodNames(name:String) = 
    allFoods.find(f => f.name == name)
}

DimpleDatabaeのほうも同じような形で食品とレシピの2つのトレイトに分割してみますよ。まずは食品のみを定義してみますデス(`・ω・´)

trait SimpleFoods {
  object Pear extends Food("Pear")
  def allFoods = List(Apple, Pear)
  def allCategories = Nil
}

次にレシピをトレイトで定義してみますが…この形式だと残念ながらコンパイルが通らないです。

trait SimpleRecipes {
  object FruitSalad extends Recipe(
    "fruit salad",
    List(Apple, Pear),
    "まぜまぜします"
  )
  def allRecipes = List(FruitSalad)
}

//// コンパイルエラーです
// スコープ的にPearが見つけられないです(´・ω・`)
<console>:14: error: not found: value Pear
           List(Apple, Pear),
                       ^

上記の問題はPearを定義したトレイトと利用しているトレイトが違うので、スコープからPearが外れてしまったために起こっているみたいですね。なのでコンパイラにSimpleRecipesとSimpleFoodsが一緒にミックスインされるのでちゃんと処理できるYO!と教えなければならないです(´・ω・`)そんなときには自分型というしくみを上手く使ってやるといいみたいです。

自分型はクラス内でthisが使われるときにthisの型として想定される。。。という票右舷らしいのですが、とりあえずイメージを掴むために上記失敗トレイトを書きなおしたやりましょー

trait SimpleRecipes {
  // ここで自分型で定義することで
  // コンパイラに無問題だとアピールします
  this:SimpleFoods => 
  object FruitSalad extends Recipe(
    "fruit salad",
    List(Apple, Pear),
    "まぜまぜします"
  )
  def allRecipes = List(FruitSalad)
}

自分型で定義された部分は他のトレイトと一緒にミックスインするので知らない型があったもそのトレイトで定義された型だから大丈夫、というアピールするのです…という解釈でいいですかね?上記SimpleFoodsだと中にある変数その他はthis.〇〇になってPearもthis.Pearとして解釈されるみたいです(´・ω・`)

最後に、各トレイトをミックスインして実装したSimpleDatabaseはこんな感じになりますね

object SimpleDatabase extends Database with SimpleFoods with SimpleRecipes

んじゃ実行してみますよー

scala> val pear = SimpleDatabase.foodNamed("Pear").get 
pear: Food = Pear

scala> SimpleBrowser.recipesUsing(pear)                
res8: List[Recipe] = List(fruit salad)

おおー、できましたな(`・ω・´)トレイトは大きいクラス・オブジェクトの分割にも使えるのね、便利だなc⌒っ゚д゚)っφ メモメモ...

いじょうー

時間切れなのでココマデです(´・ω・`)次回は実行時リンクからやりますねー