Chapter case example

An important part of every program is efficiency. In many cases, we can time our methods and find bottlenecks in our applications. Let's look at an example program that we will try and time afterwards.

We will have a look at parsing. In many real-life applications, we have to read data in specific formats and parse it to the objects in our code. For this example, we will have a small database of people represented in a JSON format:

[
  {
    "firstName": "Ivan",
    "lastName": "Nikolov",
    "age": 26
  },
  {
    "firstName": "John",
    "lastName": "Smith",
    "age": 55
  },
  {
    "firstName": "Maria",
    "lastName": "Cooper",
    "age": 19
  }
]

To represent this JSON in Scala, we have to define our model. It will be simple and contain only one class: Person. Here is the code for it:

case class Person(firstName: String, lastName: String, age: Int)

Since we will be reading JSON inputs, we will have to parse them. There are many parsers out there, and everyone might have their own preferences. In the current example, we have used json4s (https://github.com/json4s/json4s). We have the following extra dependency in our pom.xml file:

<dependency>
    <groupId>org.json4s</groupId>
    <artifactId>json4s-jackson_2.11</artifactId>
    <version>3.2.11</version>
</dependency>

The preceding dependency is easily translatable to SBT, if the reader prefers to use this build system.

We have written a class with two methods that parses an input file of the given preceding format and returns a list of Person objects. These two methods do exactly the same thing, but one of them is more efficient than the other one:

trait DataReader {
  def readData(): List[Person]
  def readDataInefficiently(): List[Person]
}

class DataReaderImpl extends DataReader {
  implicit val formats = DefaultFormats
  private def readUntimed(): List[Person] =
    parse(StreamInput(getClass.getResourceAsStream("/users.json"))).extract[List[Person]]
  
  override def readData(): List[Person] = 
    readUntimed()

  override def readDataInefficiently(): List[Person] = {
    (1 to 10000).foreach {
      case num =>
        readUntimed()
    }
    readUntimed()
  }
}

The DataReader trait acts as an interface and using the implementation is quite straightforward:

object DataReaderExample {
  def main(args: Array[String]): Unit = {
    val dataReader = new DataReaderImpl
    System.out.println(s"I just read the following data efficiently: ${dataReader.readData()}")
    System.out.println(s"I just read the following data inefficiently: ${dataReader.readDataInefficiently()}")
  }
}

It will produce output as shown in the following screenshot:

Chapter case example

The preceding example is clear. However, what if we want to optimize our code and see what causes it to be slow? The previous code does not give us this possibility, so we will have to take some extra steps in order to time and see how our application performs. In the next subsections, we will show how this is done without and with AOP.

Without AOP

There is a basic way to do our timing. We could either surround the println statements in our application, or add the timing as a part of the methods in the DataReaderImpl class. Generally, adding the timing as part of the methods seems like a better choice as in some cases these methods could be called at different places and their performance would depend on the passed parameters and other factors. Considering what we said, this is how our DataReaderImpl class could be refactored in order to support timing:

class DataReaderImpl extends DataReader {
  implicit val formats = DefaultFormats
  private def readUntimed(): List[Person] =
    parse(StreamInput(getClass.getResourceAsStream("/users.json"))).extract[List[Person]]
  
  override def readData(): List[Person] = {
    val startMillis = System.currentTimeMillis()
    val result = readUntimed()
    val time = System.currentTimeMillis() - startMillis
    System.err.println(s"readData took ${time} milliseconds.")
    result
  }

  override def readDataInefficiently(): List[Person] = {
    val startMillis = System.currentTimeMillis()
    (1 to 10000).foreach {
      case num =>
        readUntimed()
    }
    val result = readUntimed()
    val time = System.currentTimeMillis() - startMillis
    System.err.println(s"readDataInefficiently took ${time} milliseconds.")
    result
  }
}

As you can see, the code becomes quite unreadable and the timing interferes with the actual functionality. In any case, if we run our program, the output will show us where the problem is:

Without AOP

We will see how to improve our code using aspect-oriented programming in the next subsection.

Note

In the previous example, we used System.err.println to log the timing. This is just for example purposes. In practice, using loggers, for example slf4j (http://www.slf4j.org/), is the recommended option, as one can have different logging levels and switch logs using configuration files. Using loggers here would have added extra dependencies and it would have pulled your attention away from the important material.

With AOP

As we saw, adding our timing code to our methods introduces code duplication and makes our code hard to follow, even for a small example. Now, imagine that we also have to do logging and other activities. Aspect-oriented programming helps in separating these concerns.

We can revert the DataReaderImpl class to its original state, where it does not do any logging. Then we create another trait called LoggingDataReader, which extends from DataReader and has the following contents:

trait LoggingDataReader extends DataReader {

  abstract override def readData(): List[Person] = {
    val startMillis = System.currentTimeMillis()
    val result = super.readData()
    val time = System.currentTimeMillis() - startMillis
    System.err.println(s"readData took ${time} milliseconds.")
    result
  }

  abstract override def readDataInefficiently(): List[Person] = {
    val startMillis = System.currentTimeMillis()
    val result = super.readDataInefficiently()
    val time = System.currentTimeMillis() - startMillis
    System.err.println(s"readDataInefficiently took ${time} milliseconds.")
    result
  }
}

Something interesting here is the abstract override modifier. It notifies the compiler that we will be doing stackable modifications. If we do not use this modifier, our compilation will fail with the following errors:

Error:(9, 24) method readData in trait DataReader is accessed from super. It may not be abstract unless it is overridden by a member declared `abstract' and `override'

    val result = super.readData()

                       ^

Error:(17, 24) method readDataInefficiently in trait DataReader is accessed from super. It may not be abstract unless it is overridden by a member declared `abstract' and `override'

    val result = super.readDataInefficiently()

                       ^

Let's now use our new trait using a mixin composition, which we already covered earlier in this book, in the following program:

object DataReaderAOPExample {
  def main(args: Array[String]): Unit = {
    val dataReader = new DataReaderImpl with LoggingDataReader
    System.out.println(s"I just read the following data efficiently: ${dataReader.readData()}")
    System.out.println(s"I just read the following data inefficiently: ${dataReader.readDataInefficiently()}")
  }
}

If we run this program, we will see that, as before, our output will contain the timings.

The advantage of using aspect-oriented programming is clear—the implementation is not contaminated by other code, which is irrelevant to it. Moreover, we can add extra modifications using the same approach—more logging, retry logic, rollbacks, and so on. Everything happens by just creating new traits that extend DataReader and mixing them in, as shown previously. Of course, we can have multiple modifications applied at the same time, which will execute in order and the order of their execution will follow the rules of linearization, which we are already familiar with.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.179.59