`
fujohnwang
  • 浏览: 156287 次
社区版块
存档分类
最新评论

ScalaPattern之Stackable Modification Trait Pattern

阅读更多

原文出处:http://www.artima.com/scalazine/articles/stackable_trait_pattern.html



One way to use Scala's traits is as stackable modifications. In this pattern, a trait (or class) can play one of three roles: the base , a core , or a stackable . The base trait (or abstract class) defines an abstract interface that all the cores and stackables extend, as shown in Figure 1. The core traits (or classes) implement the abstract methods defined in the base trait, and provide basic, core functionality. Each stackable overrides one or more of the abstract methods defined in the base trait, using Scala's abstract override modifiers, and provides some behavior and at some point invokes the super implementation of the same method. In this manner, the stackables modify the behavior of whatever core they are mixed into.

Figure 1. Roles in the stackable trait pattern.

This pattern is similar in structure to the decorator pattern, except it involves decoration for the purpose of class composition instead of object composition. Stackable traits decorate the core traits at compile time, similar to the way decorator objects modify core objects at run time in the decorator pattern.

As an example, consider stacking modifications to a queue of integers. (This example is adapted from chapter 12 or Programming in Scala , by Martin Odersky, Lex Spoon, and Bill Venners.) The queue will have two operations: put , which places integers in the queue, and get , which takes them back out. Queues are first-in, first-out, so get should return the integers in the same order they were put in the queue.

Given a class that implements such a queue, you could define traits to perform modifications such as these:

  • Doubling : double all integers that are put in the queue
  • Incrementing : increment all integers that are put in the queue
  • Filtering : filter out negative integers from a queue

These three traits represent modifications , because they modify the behavior of an underlying "core" queue class rather than defining a full queue class themselves. The three are also stackable . You can select any of the three you like, mix them into a class, and obtain a new class that has all of the modifications you chose.

An abstract IntQueue class (the "base") is shown in Listing 1. IntQueue has a put method that adds new integers to the queue and a get method that removes and returns them. A basic implementation of IntQueue (a "core" class), which uses an ArrayBuffer , is shown in Listing 2.

  abstract class IntQueue {
    def get(): Int
    def put(x: Int)
  }

Listing 1: Abstract class IntQueue .

  import scala.collection.mutable.ArrayBuffer

  class BasicIntQueue extends IntQueue {
    private val buf = new ArrayBuffer[Int]
    def get() = buf.remove(0)
    def put(x: Int) { buf += x }
  }

Listing 2: A BasicIntQueue implemented with an ArrayBuffer .

Class BasicIntQueue has a private field holding an array buffer. The get method removes an entry from one end of the buffer, while the put method adds elements to the other end. Here's how this implementation looks when you use it:

  scala> val queue = new BasicIntQueue
  queue: BasicIntQueue = BasicIntQueue@24655f

  scala> queue.put(10)

  scala> queue.put(20)

  scala> queue.get()
  res9: Int = 10

  scala> queue.get()
  res10: Int = 20

So far so good. Now take a look at using traits to modify this behavior. Listing 3 shows a trait that doubles integers as they are put in the queue. The Doubling trait has two funny things going on. The first is that it declares a superclass, IntQueue . This declaration means that the trait can only be mixed into a class that also extends IntQueue .

  trait Doubling extends IntQueue {
    abstract override def put(x: Int) { super.put(2 * x) }
  }

Listing 3: The Doubling stackable modification trait.

The second funny thing is that the trait has a super call on a method declared abstract. Such calls are illegal for normal classes, because they will certainly fail at run time. For a trait, however, such a call can actually succeed. Since super calls in a trait are dynamically bound, the super call in trait Doubling will work so long as the trait is mixed in after another trait or class that gives a concrete definition to the method.

This arrangement is frequently needed with traits that implement stackable modifications. To tell the compiler you are doing this on purpose, you must mark such methods as abstract override . This combination of modifiers is only allowed for members of traits, not classes, and it means that the trait must be mixed into some class that has a concrete definition of the method in question.

Here's how it looks to use the trait:

  scala> class MyQueue extends BasicIntQueue with Doubling
  defined class MyQueue

  scala> val queue = new MyQueue
  queue: MyQueue = MyQueue@91f017

  scala> queue.put(10)

  scala> queue.get()
  res12: Int = 20

In the first line in this interpreter session, we define class MyQueue , which extends BasicIntQueue and mixes in Doubling . We then put a 10 in the queue, but because Doubling has been mixed in, the 10 is doubled. When we get an integer from the queue, it is a 20.

Note that MyQueue defines no new code. It simply identifies a class and mixes in a trait. In this situation, you could supply "BasicIntQueue with Doubling " directly to new instead of defining a named class. It would look as shown in Listing 4:

  scala> val queue = new BasicIntQueue with Doubling
  queue: BasicIntQueue with Doubling = \$anon\$1@5fa12d

  scala> queue.put(10)

  scala> queue.get()
  res14: Int = 20

Listing 4: Mixing in a trait when instantiating with new .

To see how to stack modifications, we need to define the other two modification traits, Incrementing and Filtering . Implementations of these traits are shown in Listing 5:

  trait Incrementing extends IntQueue {
    abstract override def put(x: Int) { super.put(x + 1) }
  }

  trait Filtering extends IntQueue {
    abstract override def put(x: Int) {
      if (x >= 0) super.put(x)
    }
  }

Listing 5: Stackable modification traits Incrementing and Filtering .

Given these modifications, you can now pick and choose which ones you want for a particular queue. For example, here is a queue that both filters negative numbers and adds one to all numbers that it keeps:

  scala> val queue = (new BasicIntQueue
       |     with Incrementing with Filtering)
  queue: BasicIntQueue with Incrementing with Filtering...

  scala> queue.put(-1); queue.put(0); queue.put(1)

  scala> queue.get()
  res15: Int = 1

  scala> queue.get()
  res16: Int = 2

The order of mixins is significant. (Once a trait is mixed into a class, you can alternatively call it a mixin .) Roughly speaking, traits further to the right take effect first. When you call a method on a class with mixins, the method in the trait furthest to the right is called first. If that method calls super , it invokes the method in the next trait to its left, and so on. In the previous example, Filtering 's put is invoked first, so it removes integers that were negative to begin with. Incrementing 's put is invoked second, so it adds one to those integers that remain.

If you reverse the order, first integers will be incremented, and then the integers that are still negative will be discarded:

  scala> val queue = (new BasicIntQueue
       |     with Filtering with Incrementing)
  queue: BasicIntQueue with Filtering with Incrementing...

  scala> queue.put(-1); queue.put(0); queue.put(1)

  scala> queue.get()
  res17: Int = 0

  scala> queue.get()
  res18: Int = 1

  scala> queue.get()
  res19: Int = 2

Overall, code written in this style gives you a great deal of flexibility. You can define sixteen different classes by mixing in these three traits in different combinations and orders. That's a lot of flexibility for a small amount of code, so you should keep your eyes open for opportunities to arrange code as stackable modifications.

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics