29장 Modular Programming Using Objects - codeport/scala GitHub Wiki

29.1 The problem

  • 프로그램이 커지면 잘 나누자.
  • 개발자 데스크톱, testing, staging, deployment 환경에서 각기 다른 설정을 적용할 수 있도록 잘 나누자.

29.2 A recipe application

이 앱의 모델은 아래와 같다:

abstract class Food(val name: String) {
  override def toString = name
}

class Recipe(
  val name: String,
  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),
  "Stir it all together.")

Database, Browser은 Mock 버전을 만들자:

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])

  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)
  }
}

이로써 Recipe 앱 완성!

29.3 Abstraction

추상화하면:

  • BrowserDatabase가 하드코딩되는 것도 분리하고
  • Mock을 여러 개 만들 때 재사용 할 수 있다.

그래서 만든 코드는 아래와 같다:

abstract class Database {
  def allFoods: List[Food]
  def allRecipes: List[Recipe]
  def foodNamed(name: String) = allFoods.find(f => f.name == name)
  case class FoodCategory(name: String, foods: List[Food])
  def allCategories: List[FoodCategory]
}

abstract class Browser {
  // Database와 Browser를 분리시킴.
  val database: Database
  def recipesUsing(food: Food) =
    database.allRecipes.filter(recipe => recipe.ingredients.contains(food))
  def displayCategory(category: database.FoodCategory) = println(category)
}

object SimpleDatabase extends Database {
  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
}

object SimpleBrowser extends Browser {
  val database = SimpleDatabase
}

29.4 Splitting modules into traits

Scala다운 모듈화는 Trait과 self-type annotation으로:

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

  trait SimpleRecipes {
    // self-type
    this: SimpleFoods =>
    object FruitSalad extends Recipe(
      "fruit salad",
      ist(Apple, Pear), // Now Pear is in scope
      "Mix it all together.")
    def allRecipes = List(FruitSalad)
  }

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

  abstract class Database extends FoodCategories {
    def allFoods: List[Food]
    def allRecipes: List[Recipe]
    def foodNamed(name: String) = allFoods.find(f => f.name == name)
  }

  object SimpleDatabase extends Database
    with SimpleFoods with SimpleRecipes

29.5 Runtime linking

SimpleDatabase, StudentDatabase을 Trait으로 다 만들었다고 치고 하나로 만들면 휘리릭:

object GotApples {
  def main(args: Array[String]) {
    val db: Database =
      if (args(0) == "student")
        StudentDatabase
      else
        SimpleDatabase

    object browser extends Browser {
      val database = db
    }

    val apple = SimpleDatabase.foodNamed("Apple").get

    for (recipe <- browser.recipesUsing(apple))
      println(recipe)
  }
}

29.6 Tracking module instances

BrowserDatabase랑 분리할 수 있게 하였으니까 StudentDatabase의 FoodCategory를 SimpleBrowser에서 출력할 수 있어야 하는데 에러가 쫙~:

scala> val category = StudentDatabase.allCategories.head
category: StudentDatabase.FoodCategory =
  FoodCategory(edible,List(FrozenFood))

scala> SimpleBrowser.displayCategory(category)
<console>:12: error: type mismatch;
  found : StudentDatabase.FoodCategory
  required: SimpleBrowser.database.FoodCategory
    SimpleBrowser.displayCategory(category)

.type을 사용해서 해결한다.:

object GotApples {
  def main(args: Array[String]) {
    ...

    object browser extends Browser {
      val database: db.type = db
    }

    ...

    for (category <- db.allCategories) browser.displayCategory(category)
  }
}

이 부분은 잘 이해가 안 되된다. .type은 책에서 봤던 것도 같은데, 잘 기억이:)

FoodCategory는 동일한 클래스에서 상속했지만, 써브클래스에서는 같은 타입이 아니다. 그래서 .type을 넣어줘서 해결 할 수 있다는 건데, 이 메커니즘은 모르겠다.

⚠️ **GitHub.com Fallback** ⚠️