If you have done any UI programming then you are undoubtedly familiar with the Observer Pattern, even if you may not know it by it's formal name. It is certainly not limited to UI programming, it's just ubiquitous there. In summary it is a pattern where there is some (observable) object that notifies other objects (observers) about changes in its state. In UI programming, these notifications about state changes are known as events. For example a button on a screen will notify observers when it is tapped.
The pattern is so ubiquitous that it is built into many languages. For example, Java provides the java.util.Observer interface and java.util.Observable class to make it easier for developers to implement the Observer Pattern. Similarly, C# has the IObserver
and IObservable
interfaces. Our example doesn't use any of these convenience APIs, but creates its own to demonstrate the full pattern APIs, but creates its own to demonstrate the full pattern.
trait Observer[E] { def receiveUpdate(event: E); } trait Observable[E] { private var observers = ListBuffer.empty[Observer[E]] def addObserver(observer: Observer[E]) { observers += observer } def notifyObservers(event:E) { observers.foreach{ observer => observer.receiveUpdate(event) } } }
In our example we create our own generalization of the Observer Pattern. Chances are we will have many different objects implementing pattern. This might be less desirable in a programming language that does not support multiple inheritance since your observable objects might have to subclass other objects. That's not a problem in Scala fortunately. So we start by defining a parameterized Observer interface. It is parameterized on the type of events that it will observe.
Next we define an Observable
base class. It is also parameterized on the types of events that it will emit. It contains an internal list of Observers
and an addObserver
method for adding to this list. Finally, it contains a notifyObservers
method for emitting an event to all of the Observers
. Now let's create a concrete example using this API.
class User(var name:String, var id:Long) { override def toString = name + "(" + id + ")" } class UserService extends Observable[User] { def getUser(id:Long) { var user = new User("foo",id) notifyObservers(user) } } object FancyApplication extends Observer[User] { override def receiveUpdate(user:User) { println(user) } def main(args:Array[String]) { var userService = new UserService userService.addObserver(this) args.foreach(s => userService.getUser(s.toLong)) } }
In our example we will imitate a remote service for retrieving information about a user. When the service gets the user information, it notifies any listeners (observers) about it. So we first define a User
class that is essentially the event data for our observer. Then we define a UserService
that is an Observable
. When its getUser
is called it imitates a real service by creating a mock User and then sending it to all of the observers by calling the notifyObservers
method on the base Observable
trait.
Finally we have an application called FancyApplication
. It implements the Observer
interface by defining its own receiveUpdate
method. In its main method it creates a UserService
and adds itself as an Observer
. Then it calls getUser
for each of the IDs passed in as command line arguments. When the UserService
generates a User
, it notifies the FancyApplication
, which causes the receiveUpdate method to be invoked.
One of the most notorious behavioral design patterns has always been the Visitor Pattern. The central concept behind the pattern is even dubious. The pattern allows you to keep separate a data structure and an algorithm that operates on that data structure. This runs seemingly right in the face of object-oriented programming, as you are purposefully keeping data and the operations on that data apart.
However, the Visitor Pattern is a pragmatic pattern. Sometimes there are many algorithms that need to work with a given data structure, and many of these algorithms are quite complex. Putting everything into a single object would lead to a huge, complex object and one that was under constant change. It's easy to imagine a large software organization all needing to work with a particular structure. In such situations, it is hard to argue against breaking things apart, and the Visitor Pattern is a common way of doing exactly this.
Even side-stepping the non-OO leanings of the Visitor Pattern, i'?s still a pattern that is hard to love. It involves a proliferation of interfaces and boilerplate code. A concrete example demonstrates this easily. For our example we will consider an SMS (Short Message Service) / MMS (Multimedia Messaging Service) application. Its central data structure is a collection of text and multimedia messages. Anyone who uses SMS/MMS frequently can imagine many different operations that can be done to this collection of messages: displaying them, grouping them by sender/conversation, writing them to a database, etc. We will take a simple of example of calculating the size of all messages and the size of all sent messages. Here is a classical representation of the pattern.
trait Message{ var sender:String = null var recipient:String = null var timestamp:Long = 0L def accept(visitor:MessageVisitor) } trait MessageVisitor{ def visit(textMessage:TextMessage) def visit(multimediaMessage:MultimediaMessage) }
This listing shows the heart of the Visitor Pattern. We define two interfaces. The first is for our different types of Messages
. We define some common data here (sender, recipient, timestamp), but also notice the accept method. This takes a MessageVisitor
object, whose interface is defined next. This interface defines a visit method for each implementation of Message. As you can see, we have two implementations which we now show.
class TextMessage extends Message{ var text:String = "" override def accept(visitor:MessageVisitor){ visitor.visit(this) } } class MultimediaMessage extends Message{ var attachment:Array[Byte] = Array() var mime:String = "" override def accept(visitor:MessageVisitor){ visitor.visit(this) } }
You can see that each of our implementations of Message
have an identical implementation of the accept method. They simply call the visit method on the MessageVisitor
that is provided to them. This allows for the visitor's code to be executed. To write an operation against some collection of Messages, we simply implement the MessageVisitor interface.
class SizeVisitor extends MessageVisitor{ var size:Int = 0 override def visit(textMessage:TextMessage){ size += textMessage.text.length } override def visit(multimediaMessage:MultimediaMessage){ size += multimediaMessage.attachment.length } }
This algorithm adds up the sizes of all the different Messages
that it encounters. For TextMessages
, it uses the length of the text. For MultimediaMessages
, it uses the size of the attachment. Now we can use it to calculate the total size of a collection of Messages:
class MessageHistory{ var messages:ListBuffer[Message] = ListBuffer.empty[Message] var owner:String = "" def size = { var sizeVisitor = new SizeVisitor var i = 0 while (i < messages.size){ messages(i).accept(sizeVisitor) i += 1 } sizeVisitor.size } }
Next we can write a new algorithm to calculate the total size of all messages sent by a user. All we have to do is write another MessageVisitor
:
class SentSizeVisitor(var owner:String) extends MessageVisitor{ var size:Int = 0 override def visit(textMessage:TextMessage){ if (owner == textMessage.sender){ size += textMessage.text.length } } override def visit(multimediaMessage:MultimediaMessage){ if (owner == textMessage.sender){ size += multimediaMessage.attachment.length } } }
This class is very similar to the SizeVisitor
. Its implementations of visit include an extra check if the sender of a Message
is the same as the owner of the owner passed in to it. It can be used similarly by calling the accept method on every Message in a collection.
This simple example demonstrates all of the characteristics of the Visitor Pattern. The pattern is often used on tree structures, in which case simple recursion can be added. This example also demonstrates some of the downsides of the pattern. It is possible to add more algorithms on the same data structure by simply adding more classes that implement the MessageVisitor
interface.
However, if we add any more implementations of Message
, then the MessageVisitor
interface has to be changed to visit this new type. Of course, if that interface is changed, then all existing implementations of the interface must also be changed. All of these changes must be coordinated or we will break existing code so that it will not even compile. This is why this pattern is often undesirable. Fortunately, functional programming allows us to implement this pattern in a more flexible way.
case class TextMessage(text:String, sender:String, recipient:String, timestamp:Long) case class MultimediaMessage(attachment:Array[Byte], mime:String, sender:String, recipient:String, timestamp:Long)
The functional approach embraces the separation of the data and algorithm even further. We define the data structures with no operations using Scala?s algebraic data types, case classes. Now we can easily define our algorithms separately from these structures by using pattern matching.
class MessageHistory(val messages:List[Any], val owner:String) object Messages{ def size(history:MessageHistory) = history.messages.foldLeft(0){ (sum,message) => message match { case tm:TextMessage => sum + tm.text.length case mm:MultimediaMessage => sum + mm.attachment.length case _ => sum }} def sentSize(history:MessageHistory) = history.messages.foldLeft(0){ (sum,message) => message match { case tm:TextMessage => if (history.owner == tm.sender) sum + tm.text.length else sum case mm:MultimediaMessage => if (history.owner == mm.sender) sum + mm.attachment.length else sum case _ => sum }} }
The size function is the conceptual equivalent to the SizeVisitor
class from above. It uses a fold to sum up the sizes of the messages in the list. For each value in the list, we see if it matches the types that we know about (TextMessage
, MultimediaMessage
). If it does, then it adds the sizes of these objects to the running sum. Notice that we allowed any type of object in our list, so if we encounter something we don't know about, we simply ignore it and leave the sum unchanged. The sentSize
method works similarly, essentially moving the logic in the various visit methods to the case clauses in the match statement.
The code in this listing is obviously much more concise and succinct than the traditional OOP implementation. It provides the same type of flexibility in that we can easily add new algorithms without modifying the data structures. All we would do is add another function and once again use pattern matching. Further it provides extra flexibility. We can add a new data type without breaking existing code. Of course, we would probably want to modify the existing code to consider the new types, but we will not break any builds if we cannot get to this immediately. Of course, this could be considered a weakness, since we could forget to modify one of the existing algorithms. With the traditional Visitor Pattern the compiler forces us to update all of the existing algorithms.
There has been error in communication with Booktype server. Not sure right now where is the problem.
You should refresh this page.