In software development there are no silver bullets, but I am always looking out for the next bronze one.

Thursday, October 25, 2007

Extending the properties.scala Example

    I was tinkering with the properties.scala example (http://www.scala-lang.org/docu/examples/files/properties.html) from the main Scala site. One thing that occurred to me was that the functionality to automatically fire PropertyChange events could be added such a way that implementers would only need to the to use a trait and specify member properties.
    I'd originally called this concept "BeanProperty", but to prevent confusion with the Scala annotation of the same name, it's named "BindableProperty". Such as class could form the basis of some kind of property binding arrangement.


Here is a example of a class with BindableProperties:

class BindableUser extends hasBindableProperties {
val firstName = BindableProperty("firstName","")
.onGet { v => v.toUpperCase() }
.onSet { v => if (v == null)
throw new IllegalArgumentException("firstName can not be set to null");
}
v
}
}
val lastName = BindableProperty("lastName", "")

val employeeNbr = BindableProperty[Long]("employeeNbr")

var isMarried = BindableProperty[Option[Boolean]]("isMarried", None)

var isMale = BindableProperty[Boolean]("isMale")
.onSet { v => updateSalutation(v, isMarried()); v }

var salutation = BindableProperty[String]("saluation");

// Some some reason, this needed to be after isMale declaration
isMarried.onSet { v => updateSalutation(isMale(), v); v }

private def updateSalutation(male:Boolean, married:Option[Boolean]):Unit = {
Console.println("update salutation " + (male, married))
salutation := ((male, married) match {
case (true,_) => "Mr."
case (false,Some(true)) => "Mrs."
case (false,Some(false)) => "Miss."
case (false,None) => "Ms."
case _ => ""
})
}

override def toString() = salutation() + " " + firstName() + " " + lastName() + ":" + employeeNbr()
}


    The hasBindableProperties trait provide methods for creation of BindableProperties and a map to store PropertyChangeListeners much like the PropertyChangeSupport class in Java.
    The onGet and onSet methods are used to specify a block of code that can transform or validate data. Above, the onGet method method converts the stored property to uppercase upon retrieval. The isMale and isMarried properties have onSet handlers that try to infer the salutation based on gender and marital status (i.e. "Mr.", "Mrs.", "Miss.", or "Ms."). The onSet handler for firstName throws an Exception if value passed is null.
    The second parameter of the BindableProperty method specifies an initial value. If an initial value is not specified, the type for the property can not be inferred and the type parameter must be specified like how it is done for the employeeNbr property in the code above.


    There are multiple ways to set a property. This is done because tastes may vary on what is readable and some people may dislike anything resembling operator overloading. Examples of setting a property:

val zaphod = new BindableUser()
// Different ways to set
zaphod.firstName("Zafod")
zaphod.lastName.set("Beeblebrox")
zaphod.isMale := true
zaphod.employeeNbr() = 0


The ":=" is the result of a flashback to Pascal that I learned in college.


There are two ways to get a property:

Console.println("user first name: " + user.firstName())
Console.println("user last name: " + user.lastName.get())


Here's an example of a PropertyChangeListener that simply reports the change to the console:

class TestBindablePropertyChangeListener extends PropertyChangeListener {
override def propertyChange(evt: PropertyChangeEvent) =
Console.println(String.format("Property '%s' for object '%s' changed from '%s' to '%s'", List(
evt.getPropertyName()
,evt.getSource().toString()
,evt.getOldValue()
,evt.getNewValue()
).toArray)) // Scala varargs are different from Java
// Had to explicitly pass an array for the Java varargs
}


To add the listener to a given object::

zaphod.addPropertyChangeListener(new TestBindablePropertyChangeListener())


Now when a property changes for a user, it is detected and reported on the console:


Example:
    To see this in action, download the BindableProperties source here and demo source here.
Compile the BindableProperties.scala first and it will create classes in directory, ./tfd/scala/bindableproperties. Secondly, compile the BindablePropertiesDemo.scala and it will put classes in ./tfd/scala/bindableproperties/demo directory. The above BindableUser and TestBindablePropertyChangeListener are included in the demo source. From the base directory, the scala interpreter can be invoked:

C:\stuff\mycode\trunk\scala\src>scala
Welcome to Scala version 2.6.0-final.
Type in expressions to have them evaluated.
Type :help for more information.

scala>import tfd.scala.bindableproperties.demo._
import tfd.scala.bindableproperties.demo._

scala>val zaphod = new BindableUser()

scala>zaphod.firstName("Zafod")

scala>zaphod.lastName.set("Beeblebrox")

scala>zaphod.isMale := true
update salutation (true,None)

scala>zaphod.employeeNbr() = 0

scala>Console.println(zaphod.toString)
Mr. ZAFOD Beeblebrox:0

scala>zaphod.addPropertyChangeListener(new TestBindablePropertyChangeListener())

scala>zaphod.firstName := "Zaphod"
Property 'firstName' for object 'Mr. ZAPHOD Beeblebrox:0' changed from 'Zafod' to 'Zaphod'

scala>zaphod.salutation := "Part-time Galactic President"
Property 'saluation' for object 'Part-time Galactic President ZAPHOD Beeblebrox:0' changed from 'Mr.' to 'Part-time Galactic President'

scala>Console.println(zaphod.toString)
Part-time Galactic President ZAPHOD Beeblebrox:0

scala>val trillian = new BindableUser()

scala>trillian.firstName := "Trisha"

scala>trillian.lastName := "McMillan"

scala>trillian.isMale := false
update salutation (false,None)

scala>trillian.employeeNbr := 22079460347L

scala>Console.println(trillian.toString)
Ms. TRISHA McMillan:22079460347

scala>trillian.addPropertyChangeListener(new TestBindablePropertyChangeListener())

scala>trillian.firstName := "Trillian"
Property 'firstName' for object 'Ms. TRILLIAN McMillan:22079460347' changed from 'Trisha' to 'Trillian'

scala>trillian.lastName := ""
Property 'lastName' for object 'Ms. TRILLIAN :22079460347' changed from 'McMillan' to ''

scala>trillian.employeeNbr := 42
Property 'employeeNbr' for object 'Ms. TRILLIAN :42' changed from '22079460347' to '42'

scala>trillian.isMarried := Some(false);
Property 'isMarried' for object 'Miss. TRILLIAN :42' changed from 'None' to 'Some(false)'

scala>Console.println(trillian.toString)
Miss. TRILLIAN :42

scala>trillian.lastName := "Dent"
Property 'lastName' for object 'Miss. TRILLIAN Dent:42' changed from '' to 'Dent'

scala>trillian.isMarried := Some(true)
update salutation (false,Some(true))
Property 'salutation' for object 'Mrs. TRILLIAN Dent:42' changed from 'Miss.' to 'Mrs.'
Property 'isMarried' for object 'Mrs. TRILLIAN Dent:42' changed from 'Some(false)' to 'Some(true)'

scala>Console.println(trillian.toString)
Mrs. TRILLIAN Dent:42


    I've also included a demo application in the BindablePropertiesDemo.scala file, BindablePropertiesDemo that can invoked via "scala tfd.scala.bindableproperties.demo.BindablePropertiesDemo". This demo runs the above commands and has a try/catch block where a firstName property is set to null.
    Scala's flexible syntax provides a good basis for creating new idioms that can make code more expressive and DSL-like without losing static typing and be a compiled language.

No comments: