Case classes are a special kind of class created using the keyword case. When the Scala compiler sees a case class, it automatically generates boilerplate code so you don’t have to do it. Here’s an example of a Person class:
In this code example, you’re creating a Person case class with firstName and lastName parameters. But when you prefix a class with case, the following things will happen automatically:
Now think about how many times you’ve created a data transfer object (DTO) with only accessors for the purpose of wrapping some data. Scala’s case classes will make that easier for you the next time. Both equals and hashCodeimplementations also make it safer to use with collections.
NOTE.
Like any other class, a case class can extend other classes, including trait and case classes. When you declare an abstract case class, Scala won’t generate the apply method in the companion object. That makes sense because you can’t create an instance of an abstract class. You can also create case objects that are singleton and serializable:
- trait Boolean
- case object Yes extends Boolean
- case object No extends Boolean
NOTE.
Let’s put your recently gained knowledge of case classes to use in the MongoDB driver. So far, you’ve implemented basic find methods in your driver. It’s great, but you could do one more thing to the driver to make it more useful. MongoDB supports multiple query options like Sort, Skip, and Limit that you don’t support in your driver. Using case classes and a little pattern matching, you could do this easily. You’ll add a new finder method to the collection to find by query and query options. But first, let’s define the query options you’re going to support:
- QueryOption.scala
- package ch3
- import com.mongodb.DBObject
- sealed trait QueryOption
- case object NoOption extends QueryOption
- case class Sort(sorting: DBObject, anotherOption: QueryOption) extends QueryOption
- case class Skip(number: Int, anotherOption: QueryOption) extends QueryOption
- case class Limit(limit: Int, anotherOption: QueryOption) extends QueryOption
For the Query class, you’ll wrap your good old friend DBObject and expose methods like sort, skip, and limit so that users can specify query options:
- Query.scala
- package ch3
- import com.mongodb.DBObject
- case class Query(q: DBObject, option: QueryOption = NoOption) {
- def sort(sorting: DBObject) = Query(q, Sort(sorting, option))
- def skip(skip: Int) = Query(q, Skip(skip, option))
- def limit(limit: Int) = Query(q, Limit(limit, option))
- }
- var rangeQuery = new BasicDBObject("i", new BasicDBObject("$gt", 20))
- var richQuery = Query(rangeQuery).skip(20).limit(10)
- def find (query: Query) = { "..." }
Look how you extracted the first and last names from the object using pattern matching. The case clause should be familiar to you; here you’re using a variable pattern in which the matching values get assigned to the first and lastvariables. Under the hood, Scala handles this pattern matching using a method called unapply. If you have to handcode the companion object that gets generated for Person, it will look like following:
- object Person {
- def apply(firstName:String, lastName:String) = {
- new Person(firstName, lastName)
- }
- def unapply(p:Person): Option[(String, String)] =
- Some((p.firstName, p.lastName))
- }
- }
NOTE.
In the discussion of for-comprehensions in chapter 2, I didn’t mention that the generator part of for-comprehensions uses pattern matching. I can best describe this with an example. Here you’re creating a list of persons and looping through them using pattern matching:
You’ll see more examples of extractors and pattern matching throughout the book. Before we leave this section, I still owe you the implementation of the find-by-query method, so here you go (see the following listing).
- Listing 3.10 ReadOnly trait
- trait ReadOnly {
- val underlying: MongoDBCollection
- def name = underlying getName
- def fullName = underlying getFullName
- def find(doc: DBObject): DBCursor = underlying find doc // Explicitly specify return type
- def findOne(doc: DBObject) = underlying findOne doc
- def findOne = underlying findOne
- def getCount(doc: DBObject) = underlying getCount doc
- // Find method takes query object
- def find(query: Query): DBCursor = {
- def applyOptions(cursor:DBCursor, option: QueryOption): DBCursor = {
- option match {
- case Skip(skip, next) => applyOptions(cursor.skip(skip), next)
- case Sort(sorting, next)=> applyOptions(cursor.sort(sorting), next)
- case Limit(limit, next) => applyOptions(cursor.limit(limit), next)
- case NoOption => cursor
- }
- }
- applyOptions(find(query.q), query.option)
- }
- }
When it comes to overload methods (methods with the same name), you have to specify the return type; otherwise, the code won’t compile. You have a similar limitation for recursive method calls. Scala type inference can’t infer the type of recursive methods or functions. In case of type errors, it’s always helpful to add type information. Using the test client in the following listing, you could test your new finder method.
- Listing 3.11 TestFindByQuery.scala
- import com.scalainaction.mongo._
- import com.mongodb.BasicDBObject
- def client = new MongoClient
- def db = client.db("mydb")
- val col = db.readOnlyCollection("test")
- val updatableCol = db.updatableCollection("test")
- for(i <- nbsp="" span="">1 to 100) updatableCol += new BasicDBObject("i", i) ->
- val rangeQuery = new BasicDBObject("i", new BasicDBObject("$gt", 20))
- val richQuery = Query(rangeQuery).skip(20).limit(10)
- val cursor = col.find(richQuery)
- while(cursor.hasNext()) {
- println(cursor.next());
- }
Supplement
* Gossip@DesignPattern : Behavioral - Visitor 模式
* Scala Gossic : 繼續深入 - 模式比對 (案例類別)
沒有留言:
張貼留言