Our project moved its middle tier to Scala recently. It had been Java, but with an able team on hand and a lot of work ahead of us, Scala was adopted instead. Not only was the transition to Scala painless, but it also brought an important and serendipitous benefit in our problem space: Scala case classes transform directly into JSON and vice versa. It’s hard to overstate just how much benefit this brought for this application.
Background: Jackson and Java
This being a web-tier project, it was already using Jackson to fetch data from a back-end REST service. Simple but verbose Java beans provided the data carriers from the REST resources.
The Scala Approach: Case Classes
As I summarised earlier, Scala makes it very easy to write immutable POJOs - information-carrying data classes that cannot be altered once constructed - called case classes. Case classes are intended to be used in data transfer, message passing and pattern matching situations. For such classes, the compiler automatically converts the constructor arguments into immutable fields (val
s). It creates a companion object that removes the need for the new
keyword when creating new instances. Also you get equals
, hashCode
, toString
and copy
methods automatically; these use the fields specified in the constructor arguments.
We might declare some case classes as follows. Firstly, suppose we have a shopping basket like this.
case class Basket(id: String,
contents: List[LineItem],
canBeFulfilled: Boolean,
operations: List[Link] = Nil) {
def totalPriceNet: Price = contents.foldLeft(Price.zero) {
(price, item) => price + item.priceNet
}
def totalVat: Price = contents.foldLeft(Price.zero) {
(price, item) => price + item.vat
}
def totalPriceGross: Price = contents.foldLeft(Price.zero) {
(price, item) => price + item.priceGross
}
}
The basket holds its ID, a list of line items, a flag and a list of operations expressed as hyperlinks, which is an optional parameter and we’ll touch on possible for this uses later. It also has three methods that compute totals using a fold operation, which is a functional way of accumulating values from a collection of things.
The line items are in a List
of LineItem
s, which is another case class shown below. This is based on a Product
case class, which is self explanatory. Line items have a description, some optional product (using Scala’s Option
), a quantity and more operations. We’ll talk more about optional fields later.
case class Product(productId: String,
description: String,
unitPrice: Price,
unitVat: Price) {
def unitGross = unitPrice + unitVat
}
case class LineItem(description: String,
product: Option[Product],
quantity: Int,
operations: List[Link] = Nil) {
def priceNet: Price = if (product.isDefined) product.get.unitPrice * quantity else Price.zero
def vat: Price = if (product.isDefined) product.get.unitVat * quantity else Price.zero
def priceGross: Price = if (product.isDefined) product.get.priceGross * quantity else Price.zero
}
The three methods on this class use the optional product. There is an alternative way of expressing the price calculations by using the Option.map
function, but that’s not the topic of discussion here.
Just to complete the data model, we need to define Price
and Link
.
case class Price(amount: BigDecimal) {
def +(other: Price) = Price(this.amount + other.amount)
def -(other: Price) = Price(this.amount - other.amount)
def *(factor: Int) = Price(amount * factor)
def /(divisor: Int) = Price(amount / factor)
}
object Price {
val zero = Price(0)
def apply(p: Int) = new Price(BigDecimal(p))
def apply(p: String) = new Price(BigDecimal(p))
}
case class Link(rel: String, href: String, method: Option[String] = None)
In these classes, we have some optional fields, for which Scala’s excellent Option
is a great relief from null pointers (Hoare’s Billion Dollar Mistake). The optional fields are given a default value, so there’s no need to add extra auxiliary constructors for this purpose.
Auxiliary constructors and other methods can be added to case classes if required; indeed they can be treated much like any other class (although it would be foolish to add anything that isn’t immutable). Case classes are easy to use: they might be put to good use here for example:
val link = Link("urn:removeItem", "http://localhost:8080/app/basket/123/item/1/remove", Some("POST"))
val price1 = Price("6.00")
val vat1 = price1 / 5
val product1 = Product("33134791", "Twinings English Breakfast Tea (100)", price1, vat1)
val product2 = Product("33134792", "Twinings Earl Grey Tea (100)", price1, vat1)
val lineItem1 = LineItem(product1.description, Some(product1), 2, List(link))
val lineItem2 = LineItem(product2.description, Some(product2), 1, List(link))
val basket = Basket("123", List(lineItem1, lineItem2), true, Nil)
{
"id" : "123",
"contents" : [ {
"description" : "Twinings English Breakfast Tea (100)",
"product" : {
"productId" : "33134791",
"description" : "Twinings English Breakfast Tea (100)",
"unitPrice" : {
"amount" : 6.00
},
"unitVat" : {
"amount" : 1.20
}
},
"quantity" : 2,
"operations" : [ {
"rel" : "urn:removeItem",
"href" : "http://localhost:8080/app/basket/123/item/1/remove",
"method" : "POST"
} ]
}, {
"description" : "Twinings Earl Grey Tea (100)",
"product" : {
"productId" : "33134792",
"description" : "Twinings Earl Grey Tea (100)",
"unitPrice" : {
"amount" : 6.00
},
"unitVat" : {
"amount" : 1.20
}
},
"quantity" : 1,
"operations" : [ {
"rel" : "urn:removeItem",
"href" : "http://localhost:8080/app/basket/123/item/1/remove",
"method" : "POST"
} ]
} ],
"canBeFulfilled" : true,
"operations" : [ ]
}
- optional fields
- possible uses for links
Mapping Technology: Jackson plus Jerkson
The Benefits
*[JSON]: JavaScript Object Notation
comments powered by Disqus