Non-AOP based approach for implementing service layer in Kotlin

Usually when you’re adding a service layer in java you have to deal with so-called cross-cutting concerns in your service such as adding uniform logging for your service methods and dealing with exceptions in a uniform way.

This can be done in multiple ways. As any seasonal programmer knows, direct, brute-force approach when you’re writing this code every time for each method just doesn’t work well. If you’re doing it yourself for every method and every service in your application you’re cluttering your code and just making it harder to maintain and extend as for any significant changes in your cross-cutting code you have to rewrite service implementations for every method. Or, simply put, it requires O(n) efforts, where n is a count of methods in your service layer.

More traditional approach to address this problem is to use AOP. Comparing to the previous approach it requires just O(1) efforts. However AOP is viewed as a hack by OOP purists.

In this article I’ll try to describe another approach which doesn’t use anything besides plain old OOP and at the same time will require the same O(1) efforts for implementing cross-cutting concerns in your code.

So, let’s start. We’ll need a some sort of API for that framework that would support cross-cutting concerns. Let’s define it.

Public API for our framework

If we want to deal with input and output data in a uniform way, we can start with the generic Request and Result traits that would define input and output data for our service calls:

trait Result trait Request

These traits will merely designate (or mark) your domain objects that will carry the data that will be needed to make a particular request or will identify your service response.

As a service might return result of a remote call or any lengthy operation we can come up with a trait that will represent a result of asynchronous call:

trait Response {
  fun get(): TResult
}

As you may probably noticed this trait looks like a simplified version of Future interface. The reason why we might not want to use Future here is because usually we just don’t need to deal with its complicated interface: it also has get() method and its more complicated counterpart, but using these methods may not be convenient for application programmer as get() waits for a response indefinitely and we may not want to do that as usually we want to work with service that define certain SLA. And the other get(time, timeUnit) method is just not convenient to call as you need to know and set that time manually. In our Response trait we defined get() method out of an assumption that it will deal with setting service timeouts according to those services SLAs (as well as dealing with retries) and we won’t let the consumers of our service API deal with this operational burden.

Also, for our service implementers we’ll need to provide a convenient way of creating Response objects:

trait ResponseCreator {
  fun create(request : TRequest, calcFun: (TRequest) -> TResult) : Response
}

This trait will provide an interface to the service implementer that will abstract away dealing with cross-cutting stuff such as logging request and responses, measuring execution time etc.

Sample Usage

So, here is how it may look like from the service implementer’s point of view:

//
// Sample Service API
//

trait GetUserProfilesRequest : Request {
  val userIds : List
}

trait UserProfile {
  val id : Long?
  val name : String
  val avatarUrl : String
}

trait GetUserProfileResult : Result {
  val profiles : List
}

trait UserService {
  fun getUserProfiles(request : GetUserProfilesRequest) : Response
}

//
// Sample Service Impl
//

data class UserProfileImpl(override val id : Long?,
                           override val name : String,
                           override val avatarUrl : String) : UserProfile

data class GetUserProfileResultImpl(override val profiles : List) : GetUserProfileResult

data class GetUserProfileRequestImpl(override val userIds : List) : GetUserProfilesRequest

class UserServiceImpl(val responseCreator : ResponseCreator) : UserService {
  override fun getUserProfiles(request: GetUserProfilesRequest): Response {
    return responseCreator.create(request, { (r) -> /* hey mom, look at this amazing type inference! */
      val result = ArrayList(r.userIds.size)
      for (id in r.userIds) {
        result.add(UserProfileImpl(id = id, name = "user#${id}", avatarUrl = "http://avatarUrl_${id}"))
      }
      GetUserProfileResultImpl(profiles = result)
    })
  }
}

Sample Framework Implementation

Sample, synchronous implementation of ResponseCreator is shown below (note, that this code also contains “aspect” behavior which may be common for all the services you’re working with in your application):

/**
 * Sample response, in real applications may use Futures with the default service SLA timeouts/retries
 */
class ImmediateResponse(val result : TResult) : Response {
  override fun get(): TResult = result
}

/**
 * Sample response creator
 */
class ImmediateResponseCreator : ResponseCreator {
  override fun create(request: TRequest,
                                         calcFun: (TRequest) -> TResult): Response {
    // [1] log request
    println(" > request = ${request}")
    // [2] start measuring time
    val startTime = System.currentTimeMillis()
    try {
      val result = calcFun(request)
      // [3] stop measuring time
      val timeDelta = System.currentTimeMillis() - startTime
      // [4] log result and execution time
      println(" > result = ${result}, timeDelta = ${timeDelta}")
      return ImmediateResponse(result)
    } catch (e : Exception) {
      // [3] stop measuring time
      val timeDelta = System.currentTimeMillis() - startTime
      // [4] log exception and execution time
      println(" > error = ${e.getMessage()}, timeDelta = ${timeDelta}")
      throw e // rethrow an exception
    }
  }
}

Full listing may be found in this gist.

Links:

Leave a Reply

Your email address will not be published. Required fields are marked *