程式扎記: [Scala IA] The Basics : Ch3. OOP in Scala - Mixin with Scala traits

標籤

2016年8月9日 星期二

[Scala IA] The Basics : Ch3. OOP in Scala - Mixin with Scala traits

Mixin with Scala traits (p70) 
trait is like an abstract class meant to be added to other classes as a mixin. Traits can be used in all contexts where other abstract classes could appear, but only traits can be used as mixin. In OOP languages, a mixin is a class that provides certain functionality that could be used by other classes. You can also view a trait as an interface with implemented methods. You’ll see shortly how Scala traits will help you in implementing the second user story. 
NOTE. 
Another difference between traits and abstract classes in Scala is that an abstract class can have constructor parameters, but traits can’t take any parameters. Both can take type parameters, which I discuss in the next chapter.

The second user story you need to implement in your driver is an ability to create, delete, and find documents in a MongoDB database. MongoDB stores documents in a collection, and a database could contain multiple collections. You need to create a component that will represent a MongoDB collection. The common use case is to retrieve documents from a collection; another use case would be to perform administrative functions like creating and deleting documents. The Java Mongo driver provides a DBCollection class that exposes all the methods to operate on the collection, but you’re going to take it and slice it into multiple views. In Scala, you could do that using a trait. You’ll use different traits for different types of jobs. 

In this implementation you’ll wrap the existing DBCollection and provide three kinds of interfaces: a read-only collection, an administrable collection, and an updatable collection. The following listing shows how the read-only collection interface will look. 
- Listing 3.5 ReadOnly collection trait 
  1. package ch3  
  2.   
  3. import com.mongodb.{DBCollection => MongoDBCollection }  
  4. import com.mongodb.DBObject  
  5.   
  6. trait ReadOnly {  
  7.   val underlying: MongoDBCollection  
  8.   def name = underlying getName  
  9.   def fullName = underlying getFullName  
  10.   def find(doc: DBObject) = underlying find doc  
  11.   def findOne(doc: DBObject) = underlying findOne doc  
  12.   def findOne = underlying findOne  
  13.   def getCount(doc: DBObject) = underlying getCount doc  
  14. }  
The only abstract member defined in this trait is underlying, which is an abstract value. In Scala, it’s possible to declare abstract fields like abstract methods that need to be inherited by subclasses. 
NOTE. 
The difference between def and val is that val gets evaluated when an object is created, but def is evaluated every time a method is called.

It’s not necessary to have an abstract member in a trait, but usually traits contain one or more abstract members. Note that you’re invoking the findOne or getCount method on the underlying collection without using the . operator. Scala allows you to treat any method as you would the infix operator (+, -, and so on). 

The DBObject parameter is nothing but a key-value map provided by the Mongo Java driver, and you’re going to use the class directly. In the full-blown driver implementation, you’ll probably want to wrap that class too, but for the toy driver you can live with this bit of leaky abstraction. I’ll talk about the details of these methods shortly when you test the methods. The next two traits you’re going to look at are Administrable and Updatable. In the Administrabletrait, you’ll expose methods for drop collection and indexes; and in the Updatable trait you’ll allow create and remove operations on documents—see the following listing. 
- Listing 3.6 Administrable and Updatable traits 
  1. package ch3  
  2.   
  3. import com.mongodb.DBObject  
  4.   
  5. trait Administrable extends ReadOnly {  
  6.   def drop: Unit = underlying drop  
  7.   def dropIndexes: Unit = underlying dropIndexes  
  8. }  
  9.   
  10. trait Updatable extends ReadOnly{  
  11.   def -=(doc: DBObject): Unit = underlying remove doc  
  12.   def +=(doc: DBObject): Unit = underlying save doc  
  13. }  
Both traits extend the ReadOnly trait because you also want to provide all the features of a read-only collection. If your trait extends another trait or class, then that trait can only be mixed into a class that also extends the same superclass or trait. This makes sense because you want to make sure that someone else implements the abstract members that your trait depends on. As with abstract classes, you can’t create an instance of a trait; you need to mix it with other concrete classes. Here’s a concrete implementation of the read-only collection: 
  1. package ch3  
  2.   
  3. import com.mongodb.{DBCollection => MongoDBCollection }  
  4.   
  5. class DBCollection(override val underlying: MongoDBCollection) extends ReadOnly{  
  6.     // TBD  
  7. }  
You’re overriding the underlying abstract value with whatever value will be passed to the primary constructor when creating the instance of DBCollection. Note that the override modifier is mandatory when overriding members of a superclass. The following adds three methods that return different flavors of the collection: 
  1. private def collection(name: String) = underlying.getCollection(name)  
  2. def readOnlyCollection(name: String) = new DBCollection(collection(name))  
  3. def administrableCollection(name: String) = new DBCollection(collection(name)) with Administrable  
  4. def updatableCollection(name: String) = new DBCollection(collection(name)) with Updatable  
You’re getting the underlying collection by name and wrapping it into a DBCollection instance. When building the administrable and updatable collection, you’re mixing in the corresponding traits using a with clause. Using the withkeyword, you can mix one or more traits into an existing concrete class. Another way of thinking about Scala mixins is as decorators. Like decorators, mixins add more functionality to existing classes. That allows you to widen a thin interface with additional traits when needed, as you did with the ReadOnlyAdministrable, and Updatable traits. Below shows what the DB class (listing 3.7) and DBCollection class (listing 3.8) look like so far. 
- Listing 3.7 Completed DB.scala 
  1. package ch3  
  2.   
  3. import scala.collection.convert.Wrappers._  
  4.   
  5. import com.mongodb.{ DB => MongoDB }  
  6.   
  7. class DB private(val underlying: MongoDB) {  
  8.   private def collection(name: String) = underlying.getCollection(name)  
  9.   def collectionNames = for(name <- nbsp="" span="">new JSetWrapper(underlying.getCollectionNames)) yield name  
  10.      
  11.   def readOnlyCollection(name: String) = new DBCollection(collection(name))  
  12.   def administrableCollection(name: String) = new DBCollection(collection(name)) with Administrable  
  13.   def updatableCollection(name: String) = new DBCollection(collection(name)) with Updatable  
  14. }  
  15.   
  16. object DB {  
  17.   def apply(underlying: MongoDB) = new DB(underlying)  
  18. }  
- Listing 3.8 DBCollection.scala 
  1. package ch3  
  2.   
  3. import com.mongodb.{DBCollection => MongoDBCollection }  
  4. import com.mongodb.DBObject  
  5.   
  6. class DBCollection(override val underlying: MongoDBCollection) extends ReadOnly  
  7.   
  8. trait ReadOnly {  
  9.   val underlying: MongoDBCollection  
  10.   def name = underlying getName  
  11.   def fullName = underlying getFullName  
  12.   def find(doc: DBObject) = underlying find doc  
  13.   def findOne(doc: DBObject) = underlying findOne doc  
  14.   def findOne = underlying findOne  
  15.   def getCount(doc: DBObject) = underlying getCount doc  
  16. }  
  17.     
  18. trait Administrable extends ReadOnly {  
  19.   def drop: Unit = underlying drop  
  20.   def dropIndexes: Unit = underlying dropIndexes  
  21. }  
  22.     
  23. trait Updatable extends ReadOnly{  
  24.   def -=(doc: DBObject): Unit = underlying remove doc  
  25.   def +=(doc: DBObject): Unit = underlying save doc  
  26. }  
If you’ve done any Ruby programming, you’ll find lots of similarity with Ruby modules. One advantage of traits compared to module systems available in other languages is that the trait mixin is checked at compile time.If you make mistakes while stacking traits, the compiler will complain. Now you’ll build a client to demonstrate that the driver works. Ideally, you should always write unit tests to make sure your code works. Chapter 8 explores testing in Scala land. For now, the following listing shows the small client that validates your driver. 
- Listing 3.9 Test client for driver QuickTour.scala 
  1. import ch3._  
  2. import com.mongodb.BasicDBObject  
  3.   
  4. object QuickTour extends App {  
  5.     
  6. def client = new MongoClient  
  7. def db = client.db("mydb")  
  8. for(name <- db.collectionnames="" name="" nbsp="" println="" span="">
  9. val col = db.readOnlyCollection("test")  
  10. println(col.name)  
  11. val adminCol = db.administrableCollection("test")  
  12. adminCol.drop  
  13. val updatableCol = db.updatableCollection("test")  
  14. val doc = new BasicDBObject()  
  15. doc.put("name""MongoDB")  
  16. doc.put("type""database")  
  17. doc.put("count"1)  
  18. val info = new BasicDBObject()  
  19. info.put("x"203)  
  20. info.put("y"102)  
  21. doc.put("info", info)  
  22. updatableCol += doc  // (1) Add document to collection  
  23. println(updatableCol.findOne)  
  24. updatableCol -= doc  
  25. println(updatableCol.findOne)  
  26. // Add 100 documents to “test” collection  
  27. for(i <- nbsp="" span="">1 to 100) updatableCol += new BasicDBObject("i", i)  
  28. val query = new BasicDBObject  
  29. // (2) Query for 71st document in C collection  
  30. query.put("i"71);  
  31. val cursor = col.find(query)  
  32. while(cursor.hasNext()) {  
  33.   println(cursor.next());  
  34. }  
  35. }  
In the test client you’re creating collections using the methods exposed by the DB class. You’re using BasicDBObject provided by the underlying MongoDB driver to test the find method. BasicDBObject is nothing but a wrapper around a Java map. MongoDB being a schema-free database, you can put any key-value pair on it and save it to the database (1). At the end of the test, you’re using the same BasicDBObject to query the database (2). To run the test client, make sure you have the Mongo Java driver .jar file in the classpath. To specify the classpath to the Scala interpreter, use the –cp option. 

After the release of your driver, all the users are happy. But it turns out that the driver is slow in fetching documents, and users are asking whether there’s any way we could improve the performance. One way to solve this problem immediately is by memoization. To speed things up, you’ll remember the calls made to the find method and avoid making the same call to the underlying collection again. The easiest way to implement the solution is to create another trait and mix it in with the other traits. By nature Scala traits are stackable, meaning one trait can modify or decorate the behavior of another trait down the stack. Here’s how to implement the Memoizer trait: 
  1. package ch3  
  2.   
  3. import com.mongodb.DBObject  
  4.   
  5. trait Memoizer extends ReadOnly {  
  6.   val history = scala.collection.mutable.Map[Int, DBObject]()  
  7.   override def findOne = {  
  8.     history.getOrElseUpdate(-1, { super.findOne })  
  9.   }  
  10.   override def findOne(doc: DBObject) = {  
  11.     history.getOrElseUpdate(doc.hashCode, { super.findOne(doc) })  
  12.   }  
  13. }  
You’re keeping track of all the resulting DBObjects, and when the same request is made a second time, you’re not going to make a call to MongoDB—instead, you’ll return from the map. The getOrElseUpdate method is interesting; it allows you to get the value for the given key, and if it doesn’t exist, it invokes the function provided in the second parameter. Then it stores the value with the key in the map and returns the result. You saved a complete if and else block with a single method. In the case of the parameterless findOne method, you’re using -1 as the key because the method doesn’t take a parameter. To use this memoizer trait, you have to modify the existing DB class as follows: 
  1. def readOnlyCollection(name: String) = new DBCollection(collection(name)) with Memoizer  
  2. def administrableCollection(name: String) = new DBCollection(collection(name)) with Administrable with Memoizer  
  3. def updatableCollection(name: String) = new DBCollection(collection(name)) with Updatable with Memoizer  
Now whenever the findOne method is invoked, the overridden version will be called, and the result will be cached. 

There’s a little problem with this Memoizer approach, though. If you remove documents from a collection, the Memoizer will still have them and return them. You could solve this by extending the UpdatableCollection trait and overriding the remove method. The next section discusses how stackable traits are implemented in Scala. 

Class linearization (p75) 
If you’ve worked with C++ or Common Lisp, then the mixin of traits will look like multiple inheritance. The next question is how Scala handles the infamous diamond problem (http://en.wikipedia.org/wiki/Diamond_problem). See figure 3.1. Before I answer that question, let’s see how your hierarchy will look if you have a diamond problem for the following UpdatableCollection
  1. Class UpdatableCollection extends DBCollection(collection(name)) with Updatable  


Figure 3.1 Class hierarchy of UpdatableCollection before class linearization 

The problem with this hierarchy is that trying to invoke one of the find methods on UpdatableCollection will result in an ambiguous call because you could reach the ReadOnly trait from two different paths. Scala solves this problem using a something called class linearizationLinearization specifies a single linear path for all the ancestors of a class, including both the regular superclass chain and the traits. This is a two-step process in which it resolves method invocation by first using right-first, depth-first search and then removing all but the last occurrence of each class in the hierarchy. Let’s look at this in more detail. First, all classes in Scala extendscala.AnyRef, which in turn inherits from the scala.Any class. (I explain Scala class hierarchy later in this chapter.) The linearization of the ReadOnly trait is simple because it doesn’t involve multiple inheritance: 

ReadOnly –> AnyRef –> Any

Similarly, Updatable and DBCollection also don’t have that issue: 
Updatable –> ReadOnly –> AnyRef –> Any
DBCollection –> ReadOnly –> AnyRef –> Any

When class linearization is applied to your UpdatableCollection, it puts the trait first after the class because it’s the rightmost element and then removes duplication. After linearization, your UpdatableCollection looks like the following: 
UpdatableCollection –> Updatable –> DBCollection –> ReadOnly –> AnyRef –> Any

Now if you add the Memoizer trait into the mix, it will show up before Updatable because it’s the rightmost element: 
UpdatableCollection –> Memoizer –> Updatable –> DBCollection –> ReadOnly –> AnyRef –> Any

Figure 3.2 illustrates how classes and traits are laid out for the UpdatableCollection class. The figure shows traits in a separate place because I want you to think differently about them. When traits have methods implemented, they work as a façade. Check the sidebar “Trait class files on JVM” for more details. The dotted lines show the hierarchy, and the solid lines with arrowheads show how methods will be resolved after linearization. 




Stackable traits 
You’ve seen multiple uses for Scala traits. To recap, you’ve used a Scala trait as an interface using ReadOnly. You’ve used it as a decorator to expand the functionality of DBCollection using the Updatable and Administrable traits. And you’ve used traits as a stack where you’ve overridden the functionality of a ReadOnly trait with Memoizer. The stackable feature of a trait is useful when it comes to modifying the behavior of existing components or building reusable components. Chapter 7 explores abstractions provided by Scala in building reusable components. For now, let’s look at another example and explore stackable traits in more detail. 

You have another requirement for your driver; this time it’s related to locale. The Scala Mongo driver is so successful that it’s now used internationally. But the documents that you’re returning aren’t locale-aware. The requirement is to make your read-only interface locale-aware. Luckily, all the non-English documents have a field called locale. Now if only you could change your find to use that, you could address this problem. 

You could change your find method in the ReadOnly trait to find by locale, but that would break all your users looking for English documents. If you build another trait and mix it with ReadOnly, you could create a new kind of Collection that will find documents using locale
  1. package ch3  
  2.   
  3. import com.mongodb.DBObject  
  4.   
  5. trait LocaleAware extends ReadOnly{  
  6.   override def findOne(doc: DBObject) = {  
  7.     doc.put("locale", java.util.Locale.getDefault.getLanguage)  
  8.     super.findOne(doc)  
  9.   }  
  10.   override def find(doc: DBObject) = {  
  11.     doc.put("locale", java.util.Locale.getDefault.getLanguage)  
  12.     super.find(doc)  
  13.   }   
  14. }  
Now when creating a new Collection, you could mix in this trait: 
  1. new DBCollection(collection(name)) with Memoizer with LocaleAware  
The traits could be reordered as follows, with the same result: 
  1. new DBCollection(collection(name)) with LocaleAware with Memoizer  
As you can see, it’s easy to use traits in a stack to add or modify the behavior of existing classes or traits. This kind of use is common in Scala code bases, and you’ll see more on them throughout the second part of the book. Before we leave traits, there’s one more thing I’d like to mention: the use of super. As you can see, when creating a trait you can’t tell how your trait will get used and who will be above you. All you know is that it has to be of a type that your trait extends. In the previous code, you could mix in the LocaleAware trait before or after Memoizer, and in each case super would mean something different. The interpretation of super in traits is dynamically resolved in Scala. 


Supplement 
Scala Gossic : 了解更多 - 混入特徵 (堆疊修飾)

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!