Scalafication of an Equity Quote Application Part 3
26 July 2011 Comments off
Reading time:
9 minutes
Word count:
1990
This is the final part in a series of article about porting an existing Java application to Scala. The application calls the Yahoo! Financial web service and retrieve stock quotes for the user, storing them in a local repository. The original Java project was described in the part one.
In the part two of this series, we looked at the unit test, and in particular the behaviour driven development (BDD) styles in the ScalaTest framework.
We will look at the Scala implementation of this business application. The easiest task is to simply convert the exceptions. Here are the Exceptions.scala
file:
package uk.co.xenonique.stockquoteapp_scala class ConnectionException( msg: String, root: Exception ) extends RuntimeException( msg, root ) { def this( msg: String ) = this( msg, null ) } class DataRetrievalException( msg: String, root: Exception ) extends RuntimeException( msg, root ) { def this( msg: String ) = this( msg, null ) } class UnknownStockSymbolException( msg: String, root: Exception ) extends RuntimeException( msg, root ) { def this( msg: String ) = this( msg, null ) }
We have more than one public class in a single source file. It is sort of allow Scala to look like a dynamic scripting language like Python or Ruby.
Next, we port the quote repository over to Scala. We remove the semi-colons, reverse the declarations of the variable types on the way. We take advantage of Scala support for Option
types, we retrieve the stock quote by symbol name. The Scala library has two types of collections: immutable and mutable. By default, immutable collections are those that imported into the compiler as a predefinition. The QuoteRepositoryKeyValueStore
imports a mutable Map
collection for storing symbol associated with stock quotes.
Here is the QuoteRepository.scala
file:
package uk.co.xenonique.stockquoteapp_scala import scala.collection.mutable.Map trait QuoteRepository { def isEmpty(): Boolean def size(): Int def clear(): Unit def store( quote: StockQuote ): Unit def get( symbol: String ): Option[StockQuote] def contains( symbol: String ): Boolean def getSymbols(): List[String] } class QuoteRepositoryKeyValueStore extends QuoteRepository { private var store: Map[String, StockQuote] = Map() def clear(): Unit = { store.clear() } def contains(symbol: String): Boolean = store.contains(symbol) def get(symbol: String ): Option[StockQuote] = store.get(symbol) def isEmpty(): Boolean = store.isEmpty def size(): Int = store.size def store(quote: StockQuote ): Unit = { store.put(quote.symbol, quote) } def getSymbols(): List[String] = { // We use a tuple to iterate through the map, which return mutable list // then we use "toList" to convert to an immutable list. ( for ( (key,value ) <- store ) yield (key) ).toList } }
In above implementation class, we can write, in Scala, methods such as size()
as one-liner methods.
We can convert the Java stock quotes to a case class and get the benefits of an implicit equalsTo()
, hashCode()
and toString()
methods; and even get a handy copy()
method too. Note: That we override the definition of the toString()
class specifically for better formatting and, of course, debugging, but we did not need to do this for a case class.
Here is the StockQuote.scala
file:
package uk.co.xenonique.stockquoteapp_scala import java.math.RoundingMode import java.math.BigDecimal case class StockQuote( symbol: String, askPrice: BigDecimal, bidPrice: BigDecimal ) { // Auxiliary (convenience) constructor def this( symbol: String, askPriceStr: String, bidPriceStr: String ) = this ( symbol, new BigDecimal(askPriceStr).setScale(2, RoundingMode.DOWN ), new BigDecimal(bidPriceStr).setScale(2, RoundingMode.DOWN ) ) // Auxiliary (copy) constructor def this( ref: StockQuote ) = this ( ref.symbol, ref.askPrice, ref.bidPrice ) def meanPrice: BigDecimal = bidPrice.add( askPrice.subtract(bidPrice).divide( StockQuote.TWO ).setScale(2, RoundingMode.DOWN ) ) override def toString(): String = { return "StockQuote [ symbol=" + symbol +", askPrice=" + askPrice + ", bidPrice=" + bidPrice + ", meanPrice="+meanPrice+"]"; } } object StockQuote { def TWO = new BigDecimal("2.0") }
The real hard part of the quote retriever service is get information from the Internet, by calling the web service. Luckily calling the Yahoo! Finance service is a simple REST GET request in comparison to other more complicated SOAP or REST web services.
Here is the QuoteRetriever.scala
file:
package uk.co.xenonique.stockquoteapp_scala import java.lang.NumberFormatException import java.io._ import java.net._ trait QuoteRetriever { def getStockQuote( symbol: String ): StockQuote } object QuoteRetrieverYahooImpl { val TOKEN_SYMBOL="@SYMBOL@" val DEFAULT_URL_FRAGMENT="https://finance.yahoo.com/d/quotes.csv?s="+TOKEN_SYMBOL+"&f=sb2b3" } class QuoteRetrieverYahooImpl( var urlFragment: String = QuoteRetrieverYahooImpl.DEFAULT_URL_FRAGMENT ) extends QuoteRetriever { def getStockQuote( symbol: String ): StockQuote = { val queryUrl: String = urlFragment.replace(QuoteRetrieverYahooImpl .TOKEN_SYMBOL, symbol) try { val url: URL = new URL( queryUrl ) val connection: HttpURLConnection = url.openConnection().asInstanceOf[HttpURLConnection] connection.setRequestMethod("GET") connection.connect() val is: InputStream = connection.getInputStream() val reader: BufferedReader = new BufferedReader( new InputStreamReader(is) ) try { val line = reader.readLine() if ( line != null ) { val tokens = line.split(",").toList if ( tokens.length < 3 ) { throw new DataRetrievalException( "Data service at URL=["+queryUrl+"] does not supply enough fields (3 != "+tokens.length+")") } var stockSymbol = tokens(0) if ( stockSymbol.length() > 0 && stockSymbol.charAt(0) == '"' && stockSymbol.charAt(stockSymbol.length()-1) == '"') { stockSymbol = stockSymbol.substring(1, stockSymbol.length()-1 ) } val askPrice = tokens(1) if ( "N/A".equals(askPrice)) { throw new UnknownStockSymbolException( "Unknown stock quote symbol ["+stockSymbol+"] price not available") } val bidPrice = tokens(2) if ( "N/A".equals(bidPrice)) { throw new UnknownStockSymbolException( "Unknown stock quote symbol ["+stockSymbol+"] price not available") } try { return new StockQuote(stockSymbol, askPrice, bidPrice) } catch { case e:NumberFormatException => throw new DataRetrievalException( "Unable to read numerical data from quote service URL=["+queryUrl+"]", e) } } else { throw new DataRetrievalException( "Data unavailable from quote service URL=["+queryUrl+"]") } } finally { if ( reader != null) { try { reader.close() } catch { case e: IOException => null } } if ( is != null) { try { is.close() } catch { case e: IOException => null } } } } catch { case e: MalformedURLException => throw new ConnectionException("unable to connect to the remote quote service URL=["+queryUrl+"]", e) case e: IOException => throw new ConnectionException("I/O failure reading service URL=["+queryUrl+"]", e) } } }
Effectively, we could have left the QuoteRetriever
as a simple Java interface, because we gained nothing. If however we wanted to define a default implementation in a Scala trait we could do so, where it would be not possible with the current version of Java 6 / 7.
We make use of the companion object QuoteRetrieverYahooImpl
to define Scala constants. Essentially this would be same as declaring a Java variable as public final static String
.
Inside the QuoteRetrieverYahooImpl
class, let us look at the implementation method getStockQuote()
. We chucked away Java standard library StringTokenizer
and replaced it with the nicer Scala equivalent. We split a String into Array[String]
of fragments using the split
method, and then convert the array into a list collection by calling toList
. Since Scala 2.8, these methods toList()
, toSet()
and toArray()
are ubiquitous across the Scala collections.
There is probably a more object functional way of building the temporary variables using functions and closures, however for intermediate Java developer this is easier to follow. The rest of the code is just boiler plate to tackle the exception handling of the Java I/O library. See the last part of this article for an improvement.
Last, but not least is the main application class in the StockQuoteApp.scala
file.
package uk.co.xenonique.stockquoteapp_scala import java.lang.System class StockQuoteApp( retriever: QuoteRetriever, repo: QuoteRepository ) { def this() = this( new QuoteRetrieverYahooImpl(), new QuoteRepositoryKeyValueStore() ) def doInteractive(): Unit = { println("=======================================================") println(" Welcome Peter Pilgrim's Stock Quote Application") println("=======================================================") var quit = false while ( !quit) { println("Type in a stock symbol or `:repo' to list the repository or `:quit' to quit") println("or `:clear' to clear the repository") var line = readLine("$ ") if ( line != "" ) { line = line.trim() if ( line.startsWith(":r") || line.startsWith(":p")) { val symbols = repo.getSymbols() printf("%10s %9s %9s %9s\n", "SYMBOL", "MEAN", "ASK", "BID") printf("%10s %9s %9s %9s\n", "--------", "------", "-----", "-----") for ( symbol <- symbols ) { val quote = repo.get(symbol).get printf("%10s: %9.2f %9.2f %9.2f\n", quote.symbol, quote.meanPrice, quote.askPrice, quote.bidPrice ) } } else if ( line.startsWith(":c")) { repo.clear() println("Repository cleared.") } else if ( line.startsWith(":q")) { quit = true } else { if ( line.length() > 0 ) { try { val quote = findAndUpdateStockQuote(line) printf("%s: %7.2f\n", quote.symbol, quote.meanPrice) } catch { case e:ConnectionException => System.err.println("CONNECTION ERROR : "+e.getMessage()) case e: UnknownStockSymbolException => System.err.println("STOCK SYMBOL NOT FOUND : "+e.getMessage()) } } } } } // Prints name and age to the console println("Goodbye.") } def findAndUpdateStockQuote(symbol: String): StockQuote = { val quote = retriever.getStockQuote(symbol) repo.store(quote) quote } } object StockQuoteApp { val CONNECTION_PROPERTIES_FILENAME: String = "connection.properties" def main( args: Array[String] ): Unit = { println("Hello world. This is the Scala application!") new StockQuoteApp().doInteractive } }
It should be fairly self-explanatory that this program is interactive one. It reads a symbol at the command line and invokes the quote retriever to find the stock quote for the symbol.
There is one other improvement to QuoteRetrieverYahooImpl
we could make use of. Scala supports function objects; functions calling other functions and returning a different (or the same input) function. Scala methods on class or object types can have more than one parameter list in order to support currying of parameters. Scala language has the feature of partial functions that enable this facility.
Here is a class called TryResource
, which closes an InputStream
resource.
package uk.co.xenonique.stockquoteapp_scala import java.io.InputStream import java.io.FileInputStream import java.io.InputStreamReader import java.io.BufferedReader import java.io.IOException import java.lang.System // Needs to be generic class TryResource[T <: InputStream]( inputStream: T ) { private val is: T = inputStream def execute( body: ( InputStream => Unit ) ): Unit = { try { body(is) } finally { if ( is != null ) { try { is.close() } catch { case e:IOException => System.err.println(e) } } } } } object TryResource { def tryResource[T <: InputStream]( inputStream: T )( body: ( InputStream => Unit ) ) { val construct = new TryResource( inputStream) construct.execute( body ) } def demo1() { val construct = new TryResource( new FileInputStream("pom.xml") ) construct.execute( (is: InputStream ) => { val reader = new BufferedReader( new InputStreamReader(is)) var count = 1 var line: String = null do { line = reader.readLine() if ( line != null ) { printf("demo1 %5d %s\n", count, line) count = count + 1 } } while ( line != null ) } ) println("==== Done it ====") } def demo2() { tryResource ( new FileInputStream("pom.xml") ) ( ( is: InputStream ) => { val reader = new BufferedReader( new InputStreamReader(is)) var count = 1 var line: String = null do { line = reader.readLine() if ( line != null ) { printf("demo2 %5d %s\n", count, line) count = count + 1 } } while ( line != null ) } ) println("==== Done it ====") } def main( args: Array[String]): Unit = { demo2() } }
The method tryResource
takes two parameter lists. The first parameter list is a argument list of one, the input stream resource. The second parameter list is also a argument of one, except it is a so-called called-by-name type, by which the function call is delayed, evaluated after inside the method. In other words, we call tryResource()()
by passing a block of code that operates on the InputStream
and the library function will automatically close the resource for us, regardless of whether block of code completes with normal or abnormal termination. Java 7 now has a similar acquire-release mechanism as a language feature, whereas in Scala, we implemented this a library feature. Scala is scalable language after all.
It would be straight forward now to use tryResource
in the QuoteRetrieverYahooImpl
directly. This is an exercise for you, the reader. Hint: You should have a good read about Scala’s structured types!
Good Luck!