Scala - Modular programming using Objects

Up to now, all the pages has discuseed programming in the small, and from this page, we will discuss program in the larger. 

Programming in the small:

 design and implementing smaller program pieces out of which you can construct a larger program 

 Programing in the large:
   organizing and assemblying the smaller pieces into larger programs, applications or systems. 


 Problem with module
 the problem with module is that it does not offer configuration a package into two different ways within the same programs.  and you cannot inherit between packages.

 possible solution 

  •    1. there should be a module construct that provides  a good separation of interface and implemenation 
  •    2. there should be one way to replace one module with another that has the same interface with out changing or recompiling the modules that depends on the replace one 
  •    3. there should be a way to wire modules together, this wiring task can be thought as configuring the system .
   

 as to point 3, there exist other technique that is called  dependency injection. 

A recipe program 

Imagine you are building an enterprise web application that will allow users to manage recipes. You want to partition the software into layers. Including a domain layer and an application layer. domain layer has domain object, which will capture business concepts and rules and ecapsulate state that will be persited to an external relational database. In the application offers to client (including the user interface layer). the appplication layer will implement service by coordinate tasks and delegating the work to objects of the domain layer. 

we first has the Food class. and the recipes. 

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
}

and we then make entitties that shall persisted in the database. 

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"
)
scala uses objects modules, so you can start modularizing your pgoram by making two singleton objects to server as the mock implementations of the database and browser modules  during test. 
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)
}

object SimpleBrowser { 
  def recipesUsing(food : Food ) = {
    SimpleDatabase.allRecipes.filter(recipe => recipe.ingredients.contains(food) )
  }
}
a simple test code is as such . 
// test code 
//
val apple = SimpleDatabase.foodNamed("Apple").get

SimpleBrowser.recipesUsing(apple)

now, we add FoodCategories, the food categories categorize foods into categories. below is the revised SimpleDatabase and SimpleBrowser . we add a food category class and a list of all categories in the database. notice that we have those private keyword, so useful for implementing classes, it also useful for implementation module, Items marked private are part of the implementation of a module, and thus are particularly easy to change without affecting module. 


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))
  // this has hard-link to the SimpleDatabase.FoodCategory, which we tries to avoid 
  //
  def displayCategory(category: SimpleDatabase.FoodCategory) {
    println(category)
  }
}

Abstraction 


One of the problem that we have with the above code is that it has hard-link from SimpleBrowder to SimpleDatabase, as in the following code. 


SimpleDatabase.allRecipes.filter(recipe => recipe.ingredients.contains(food))
we will rework the code . 



// we first make the Browser a class "if a module is an object, then a template for a module is a class. 
// we are making abstract class for Browser
//
abstract class Browser { 
  val database : Database
  
  def recipesUsing(food : Food ) =
    database.allRecipes.filter(recipe => recipe.ingredients.contains(food))
  
  def displayCategory (category : database.FoodCategory) { 
    println(category)
  }
}
and because of this, we make the Database an abstract class. 



// we will also make a Database 
// that is abstract 
//
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]
}
So, we can update the SimpleDatabase to extends the Database class. 



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
}
to check the business layer works, run the following to see that correct list returned. 



val apple = SimpleDatabase.foodNamed("Apple").get // return Food = Apple 
a specific browser module is made by instantiating the Browser class and specifying which database to use.



object SimpleBrowser extends Browser { 
  val database = SimpleDatabase
}
Now, test the specific browser simpleBrowser works. 



SimpleBrowser.recipesUsing(apple)
// List[Recipe] = List(Fruit salad)
TO show that what we created is really pluggable  we created a StudentBrowser adn a StudentDatabase.



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

object StudentBrowser extends Browser { 
  val database = StudentDatabase
}

Splitting modules into traits


Abstraction does give us a hell lots of pluggability and flexibility, however, Scala does offer more advanced constructed such as Trait. And a module is too large to fit into a single file. Suppose now we want to move categorization code out of the main Database file and into its own. you could create a trait for the code as shown below.


// move the FoodCategories 
// out of the SimpleDatabase
// 
trait FoodCategories { 
  case class FoodCategory(name : String, foods : List[Food]) 
  def allCategories : List[FoodCategory]
}
now, Database can mix in the FoodCategories, (instead of associated the FoodCategories inside the Database definition ). 



// now you can mix in FoodCategories
// instead have FoodCategories inside Datbase 
//
abstract class Database extends FoodCategories {
  
  def allFoods : List[Food]
  def allRecipes : List[Recipe]
  def foodNamed (name : String) = allFoods.find(f => f.name == name)
  
}
Continue this way, let divide SimpleDatabase into two traits, one for foods and another for recipes. 



object SimpleDatabase extends Database with SimpleFoods with SimpleRecipes
and the SimpleFoods trait 



trait SimpleFoods {
  object Pear extends Food("Pear")
  def allFoods = List(Apple, Pear)
  def allCategories = Nil // 
}
and the SimpleRecipes as follow.(well, I lied, the following code does not compile )



// the following code does not compile, because 
// Pear is defined in simpleFoods, 
// 
// SimpleRecipes class is suppose to work with SimpleFood, and we should use "self type" or precisely specify this requirements on the the concrete classes. 
// 
trait SimpleRecipes { // Does not compile 
  object FruitSalad extends Recipe (
    "Fruit salad",
    List(Apple, Pear), // Uh oh
    "Mix it all together."
  )
  def allRecipes = List(FruitSalad)
}
because Apple, Pear is defined inside the Database, we know that the SimpleRecipes will mix together with the SimpleFods into the SimpleDatabase. 


the key here is the self-typing, code as follow. 

Technically, a self type is an assumed type for "this" whenever "this" is mentioned within class. Pragmatically, a self-type specifies the requirement on any concrete class the trait is mixed into


// self-type
// 
// self-type is way you can tell this to the compiler, however, Scala provides the self-type for precisely this situation 
//Technically, a self type is an assumed type for "this" whenever "this" is mentioned  within class. Pragmatically, a self-type specifies
// the requirement on any concrete class the trait is mixed into. 
trait SimpleRecipes { 
  this : SimpleFoods => //
    
    object FruitSalad extends Recipe (
    	"Fruit salad",
    	List(Apple, Pear), // now pear is in scope
    	"Mix it all together."
    )
    def allRecipes = List(FruitSalad)
}
// This is safe, because any concrete class that mixes in SimpleRecipes must also be a subtype of SimpleFoods. 
//

Runtime linking


Scala module can be linked togther at runtime, and you can decide which modules will link to which depending on runtime computations. 


object GotApples {
  
  def main(args : Array[String]) {
    val db : Database = 
      if (args(0) == "student") 
        StudentDatabase
      else 
        SimpleDatabase
      
       object brower extends Browser {
        val database = db
      }
    
    val apple = SimpleDatabase.foodNamed("Apple").get
    
    for (recipe <- brower.recipesUsing(apple))
      println(recipe)
    
  }
}


to run the code, either from the command line or from within the interpreter (with simuated call to Main method). 


// simulate the calls. 
GotApples.main(Array[String]("simple"))
// or... 
GotApples.main(Array[String]("studen"))

// run the following commands 
//

$ scala GotApples simple
$ scala GotApples student

Tracking module instances


though same code, the difference browser and database modules created in the previous really are separately modules. That means that each module has it own contents, including any nested classes. FoodCategory in SimpleDatabase or example, in a different class from FoodCategory in StudentDatbase. 


val category = StudentDatabase.allCategories.head

SimpleBrowser.displayCategory(category) // you will get an error ,SimpleBrowser.database.FoodCategory 
//<console>:21: error: type mismatch;
// found   : StudentDatabase.FoodCategory
// required: SimpleBrowser.database.FoodCategory
Sometimes, you may encounter a case where wo types are the same but the compiler can't verify it . (how the compiler does not know verify it ?). 


you can fix it the problem using "singleton types". For examples, in GotApples program, the type checker does not know that db and browser.database are the same .

the key is "db.type ".. db.type. A singletong type is extremely specific and holds only one object, in this case, whichever object is referred to by db.


object GotApples {
  def main(args : Array[String]) {
    val db : Database = 
      if (args(0) == "student") 
        StudentDatabase
      else 
        SimpleDatabase
      
        // the key is as follow. 
        // : db.type 
        // a singletong type is extremely specific and holds only one object, in this case, whichever object is referred to by db. 
        //
       object brower extends Browser {
        val database : db.type = db
      }
    
    val apple = SimpleDatabase.foodNamed("Apple").get
    
    for (recipe <- brower.recipesUsing(apple))
      println(recipe)
      
    // now it works. 
    for (category <- db.allCategories)
      brower.displayCategory(category)

  }
}









你可能感兴趣的:(scala)