2016年8月13日 星期六

[Scala IA] The Basics : Ch3. OOP in Scala - Case class

Case class (p78) 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:

* Scala prefixes all the parameters with val, and that will make them public value. But remember that you still never access the value directly; you always access through accessors.* Both equals and hashCode are implemented for you based on the given parameters.* The compiler implements the toString method that returns the class name and its parameters.* Every case class has a method named copy that allows you to easily create a modified copy of the class’s instance. You’ll learn about this later in this chapter.* A companion object is created with the appropriate apply method, which takes the same arguments as declared in the class.* The compiler adds a method called unapply, which allows the class name to be used as an extractor for pattern matching (more on this later).* A default implementation is provided for serialization:scala>val me = Person("Lee", "John")me: Person = Person(Lee,John)

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.

You’re allowed to prefix the parameters to the case class with var if you want both accessors and mutators. Scala defaults it to val because it encourages immutability.

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:

Scala case classes and objects make it easy to send serializable messages over the network. You’ll see a lot of them when you learn about Scala actors. NOTE.

From Scala 2.8 on, case classes without a parameter list are deprecated. If you have a need, you can declare your case class without a parameter. Use () as a parameter list or use the case object.

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

Here you’re creating four options: Sort, Skip, Limit, and NoOption. The NoOption case is used when no option is provided for the query. Each query option could have another query option because you’ll support multiple query options at the same time. The Sort option takes another DBObject in which users can specify sorting criteria. Note that all the option case classes extend an empty trait, and it’s marked as sealed. I’ll talk about modifiers in detail later in the chapter, but for now a sealed modifier stops everyone from extending the trait, with a small exception. To extend a sealed trait, all the classes need to be in the same source file.

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

Here each method creates a new instance of a query object with an appropriate query option so that, like a fluent interface (http://martinfowler.com/bliki/Fluent Interface.html), you can chain the methods together as in the following:

Here you’re searching documents for which the i > 20 condition is true. From the result set you skip 20 documents and limit your result set to 10 documents. The most extraordinary part of the code is the last parameter of the Queryclass: option: QueryOption = NoOption. Here you’re assigning a default value to the parameter so that when the second parameter isn’t specified, as in the previous snippet, the default value will be used. You’ll look at default parameters in the next section. I’m sure that, as a focused reader, you’ve already spotted the use of the companion object that Scala generates for case classes. When creating an instance of a case class, you don’t have to use new because of the companion object. To use the new query class, add the following new method to the ReadOnly trait:

Before discussing implementation of the find-by-query method, let’s see how case classes help in pattern matching. You’ll be using pattern matching to implement the method. You learned about pattern matching in chapter 2, but I haven’t discussed case classes and how they could be used with pattern matching. One of the most common reasons for creating case classes is the pattern-matching feature that comes free with case classes. Let’s take the Person case class once again, but this time you’ll extract firstName and lastName from the object using pattern matching:

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:

The apply method is simple; it returns an instance of the Person class and it is called when you create an instance of a case class. The unapply method gets called when the case instance is used for pattern matching.Typically, the unapply method is supposed to unwrap the case instance and return the elements (parameters used to create the instance) of the case class. I’ll talk about the Option type in Scala in detail in the next chapter, but for now think of it as a container that holds a value. If a case class has one element, the Option container holds that value. But because you have more than one, you have to return a tuple of two elements. NOTE.

Sometimes instead of unapply, another method called unapplySeq could get generated if the case class parameters end with a repeated parameter (variable argument). I’ll discuss that in a later chapter.

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

Here you’re using pattern matching to apply each query option to the result returned by the find method—in this case, DBCursor. The nested applyOptions function is applied recursively because each query option could wrap another query option identified by the next variable, and you bail out when it matches NoOption.

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.