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.
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
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
- Listing 3.7 Completed DB.scala
- Listing 3.9 Test client for driver QuickTour.scala
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:
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:
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.
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:
* Scala Gossic : 了解更多 - 混入特徵 (堆疊修飾)