A 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.
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
- package ch3
- import com.mongodb.{DBCollection => MongoDBCollection }
- import com.mongodb.DBObject
- trait ReadOnly {
- val underlying: MongoDBCollection
- def name = underlying getName
- def fullName = underlying getFullName
- def find(doc: DBObject) = underlying find doc
- def findOne(doc: DBObject) = underlying findOne doc
- def findOne = underlying findOne
- def getCount(doc: DBObject) = underlying getCount doc
- }
NOTE.
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
- package ch3
- import com.mongodb.DBObject
- trait Administrable extends ReadOnly {
- def drop: Unit = underlying drop
- def dropIndexes: Unit = underlying dropIndexes
- }
- trait Updatable extends ReadOnly{
- def -=(doc: DBObject): Unit = underlying remove doc
- def +=(doc: DBObject): Unit = underlying save doc
- }
- package ch3
- import com.mongodb.{DBCollection => MongoDBCollection }
- class DBCollection(override val underlying: MongoDBCollection) extends ReadOnly{
- // TBD
- }
- private def collection(name: String) = underlying.getCollection(name)
- def readOnlyCollection(name: String) = new DBCollection(collection(name))
- def administrableCollection(name: String) = new DBCollection(collection(name)) with Administrable
- def updatableCollection(name: String) = new DBCollection(collection(name)) with Updatable
- Listing 3.7 Completed DB.scala
- package ch3
- import scala.collection.convert.Wrappers._
- import com.mongodb.{ DB => MongoDB }
- class DB private(val underlying: MongoDB) {
- private def collection(name: String) = underlying.getCollection(name)
- def collectionNames = for(name <- nbsp="" span="">new JSetWrapper(underlying.getCollectionNames)) yield name ->
- def readOnlyCollection(name: String) = new DBCollection(collection(name))
- def administrableCollection(name: String) = new DBCollection(collection(name)) with Administrable
- def updatableCollection(name: String) = new DBCollection(collection(name)) with Updatable
- }
- object DB {
- def apply(underlying: MongoDB) = new DB(underlying)
- }
- package ch3
- import com.mongodb.{DBCollection => MongoDBCollection }
- import com.mongodb.DBObject
- class DBCollection(override val underlying: MongoDBCollection) extends ReadOnly
- trait ReadOnly {
- val underlying: MongoDBCollection
- def name = underlying getName
- def fullName = underlying getFullName
- def find(doc: DBObject) = underlying find doc
- def findOne(doc: DBObject) = underlying findOne doc
- def findOne = underlying findOne
- def getCount(doc: DBObject) = underlying getCount doc
- }
- trait Administrable extends ReadOnly {
- def drop: Unit = underlying drop
- def dropIndexes: Unit = underlying dropIndexes
- }
- trait Updatable extends ReadOnly{
- def -=(doc: DBObject): Unit = underlying remove doc
- def +=(doc: DBObject): Unit = underlying save doc
- }
- Listing 3.9 Test client for driver QuickTour.scala
- import ch3._
- import com.mongodb.BasicDBObject
- object QuickTour extends App {
- def client = new MongoClient
- def db = client.db("mydb")
- for(name <- db.collectionnames="" name="" nbsp="" println="" span="">->
- val col = db.readOnlyCollection("test")
- println(col.name)
- val adminCol = db.administrableCollection("test")
- adminCol.drop
- val updatableCol = db.updatableCollection("test")
- val doc = new BasicDBObject()
- doc.put("name", "MongoDB")
- doc.put("type", "database")
- doc.put("count", 1)
- val info = new BasicDBObject()
- info.put("x", 203)
- info.put("y", 102)
- doc.put("info", info)
- updatableCol += doc // (1) Add document to collection
- println(updatableCol.findOne)
- updatableCol -= doc
- println(updatableCol.findOne)
- // Add 100 documents to “test” collection
- for(i <- nbsp="" span="">1 to 100) updatableCol += new BasicDBObject("i", i) ->
- val query = new BasicDBObject
- // (2) Query for 71st document in C collection
- query.put("i", 71);
- val cursor = col.find(query)
- while(cursor.hasNext()) {
- println(cursor.next());
- }
- }
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:
- package ch3
- import com.mongodb.DBObject
- trait Memoizer extends ReadOnly {
- val history = scala.collection.mutable.Map[Int, DBObject]()
- override def findOne = {
- history.getOrElseUpdate(-1, { super.findOne })
- }
- override def findOne(doc: DBObject) = {
- history.getOrElseUpdate(doc.hashCode, { super.findOne(doc) })
- }
- }
- def readOnlyCollection(name: String) = new DBCollection(collection(name)) with Memoizer
- def administrableCollection(name: String) = new DBCollection(collection(name)) with Administrable with Memoizer
- def updatableCollection(name: String) = new DBCollection(collection(name)) with Updatable with Memoizer
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:
- 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 linearization. Linearization 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:
Similarly, Updatable and DBCollection also don’t have that issue:
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:
Now if you add the Memoizer trait into the mix, it will show up before Updatable because it’s the rightmost element:
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:
- package ch3
- import com.mongodb.DBObject
- trait LocaleAware extends ReadOnly{
- override def findOne(doc: DBObject) = {
- doc.put("locale", java.util.Locale.getDefault.getLanguage)
- super.findOne(doc)
- }
- override def find(doc: DBObject) = {
- doc.put("locale", java.util.Locale.getDefault.getLanguage)
- super.find(doc)
- }
- }
- new DBCollection(collection(name)) with Memoizer with LocaleAware
- new DBCollection(collection(name)) with LocaleAware with Memoizer
Supplement
* Scala Gossic : 了解更多 - 混入特徵 (堆疊修飾)
沒有留言:
張貼留言