sc_ray
sc_ray

Reputation: 8043

Calling external services in scala code with dependencies

I am facing a major issue with my design at this juncture. My method is trying to accomplish the follows:

  1. Insert the passed in object into the database.
  2. Get the autoincremented id from the insert and use it to call webservice1 along with the object.
  3. Get the result from webservice1 and call webservice2 with the original object and some response from webservice1.
  4. Combine the results from webservice1 and 2 and write it into the database.
  5. Get the resulting autoincremented id from the last insert and call webservice3 with the original object that would eventually result into the success or failure of the operation.

I want to design this in a flexible manner since the requirements are in a flux and I do not want to keep on modifying my logic based on any changing. I do realize some amount of change is inevitable but I would like to minimize the damage and respect the open-closed principle.

My initial take was as follows:

def complexOperation(someObject:T) = 
   dbService.insertIntoDb(someObject) match {
     case Left(e:Exception) => Left(e)
     case Right(id:Int) => webService.callWebService1(id,someObject) match {
        case Left(e:Exception) => Left(e)
        case Right(r:SomeResponse1) => webService.callWebservice2(r,someObject) match {
          case Left(e:Exception) => webService.rollbackService1();Left(e)
          case Right(context:ResponseContext) => dbService.insertContextIntoDb(context) match {
            case Left(e:Exception) => Left(e)
            case Right(id:Int) => webService.callWebservice3(id,someObject) match {
               case Left(e:Exception) => webService.rollbackService3();Left(e)
               case Right(r:Response) => Right(r)
            }
          }
        } 
     }

As you can see, this is a tangled mess. I can neither unit test it, nor extend it nor very easily debug it if things spiral out of control. This code serves its purpose but it will be great to get some ideas on how I should refactor it to make the lives of the people who inherit my code a little more easier.

Thanks

Upvotes: 2

Views: 853

Answers (3)

ms-tg
ms-tg

Reputation: 2698

@Dylan had the right idea above. Let me see if I can help translate what you want to do into idiomatic Scala 2.9.1 code.

This version doesn't attempt any rollbacks:

// 1: No rollbacks, just returns the first exception in Left
def complexOperation1(someObject:T): Either[Exception, Response] = {      
  for {
    id       <- dbService.insertIntoDb(someObject).right
    r        <- webService.callWebService1(id, someObject).right
    context  <- webService.callWebservice2(idResp, someObject).right
    id2      <- dbService.insertContextIntoDb(context).right
    response <- webService.callWebservice3(id,someObject).right
  } yield response 
}

Now, let's try to do the rollbacks exactly as you had them above:

// 2: Rolls back all web services and returns first exception in Left       
def complexOperation1(someObject:T): Either[Exception, Response] = {      
  for {
    id       <- dbService.insertIntoDb(someObject).right
    r        <- webService.callWebService1(id, someObject).right
    context  <- webService.callWebservice2(idResp, someObject).left.map { e =>
                  webService.rollbackService1()
                  e
                }.right
    id2      <- dbService.insertContextIntoDb(context).right
    response <- webService.callWebservice3(id,someObject).left.map { e =>
                  webService.rollbackService3()
                  e
                }.right
  } yield response 
}

If you define a function which does the effect (the rollback) on the left, it get's a little cleaner and easier to test, for example:

// 3: Factor out the side-effect of doing the follbacks on Left
def rollbackIfLeft[T](f: => Either[Exception, T], r: => Unit): Either[Exception, T] = {
  val result = f
  result.left.foreach(_ => r) // do the rollback if any exception occured
  result
}

def complexOperation1(someObject:T): Either[Exception, Response] = {      
  for {
    id       <- dbService.insertIntoDb(someObject).right
    r        <- webService.callWebService1(id, someObject).right
    context  <- rollbackIfLeft(webService.callWebservice2(idResp, someObject),
                               webService.rollbackService1()).right
    id2      <- dbService.insertContextIntoDb(context).right
    response <- rollbackIfLeft(webService.callWebservice3(id,someObject),
                               webService.rollbackService3()).right
  } yield response 
}

You can try out rollbackIfLeft in the scala REPL to get a sense of it:

scala> rollbackIfLeft(Right(42), println("hey"))
res28: Either[Exception,Int] = Right(42)

scala> rollbackIfLeft(Left(new RuntimeException), println("ERROR!"))
ERROR!
res29: Either[Exception,Nothing] = Left(java.lang.RuntimeException)

Hope this helps!

Upvotes: 2

Dylan
Dylan

Reputation: 13922

Have a look at scala.util.Try. It's available in Scala 2.10, which may or may not be available to you as an option, but the idea of it is perfect for your scenario.

What you have in your code example is what I like calling the "pyramid" of nesting. The best solution to this is to use flat-mapping wherever you can. But obviously that's an issue when you have stuff like Either[Exception, Result] at every step. That's where Try comes in. Try[T] is essentially a replacement for Either[Exception, T], and it comes with all of the flatMap-ing goodness that you need.

Assuming you can either change the return type of those webService calls, or provide some implicit conversion from Either[Exception, Result] to Try[Result], your code block would become something more like...

for {
    id <- dbService.insertIntoDb(someObject)
    r <- webService.callWebService1(id,someObject)
    context <- webService.callWebservice2(r,someObject)
    id2 <- dbService.insertContextIntoDb(context)
    response <- webService.callWebservice3(id,someObject).recoverWith {
        case e: Exception => webService.rollbackService3(); Failure(e)
    }
} yield response

Lift has a similar mechanism in net.liftweb.common.Box. It's like Option, but with a container for Exceptions too.

edit: It looks like you can use the left or right method of an Either, and it will let you use flatMap-ing almost exactly the way I described with Try. The only difference is that the end result is an Either[Exception, Result] instead of a Try[Result]. Check out LeftProjection for details/examples.

Upvotes: 2

Ramesh Mandaleeka
Ramesh Mandaleeka

Reputation: 29

You can use for comprehension to reduce the noise in the code.

Upvotes: 2

Related Questions