atline atline - 1 month ago 12
Scala Question

What's the difference for object, string, symbol when sent with akka?

See following code,

Op1
,
Op2
,
Op3
all can be acted as message when communicate with other actors.

My question is what is the difference, any suggested way to use? Something maybe like use Op1 is more efficiently or less bytes will be used to use one of them? I want to find a best practice.

package local

import akka.actor._

object Constant {
case object Op1
val Op2 = "msg"
val Op3 = 'msg
}

object Local extends App {
implicit val system = ActorSystem("LocalSystem")
val localActor = system.actorOf(Props[LocalActor], name = "LocalActor")
localActor ! Constant.Op1
localActor ! Constant.Op2
localActor ! Constant.Op3
}

class LocalActor extends Actor {
def receive = {
case Constant.Op1 =>
println("1")
case Constant.Op2 =>
println("2")
case Constant.Op3 =>
println("3")
}
}

Answer

Well... the term "best practices" is very subjective. And the context of your problem statement will generally have an impact on which is the best practice for you. And for this reason you should focus more on why is this a best practice than what is the best practice.

Now lets discuss this in a generic context.

As you understand that Akka actors use messages to communicate with each other. And you must understand that whenever there is communication between two parties there exist a need of a protocol between them which enables them to communicate in a un-ambigous manner.

Also, we know that both the parties can as well decide to communicate in just english without any protocol, but that introduces the possibilities of misunderstanding each other.

Now this problem of having a possibility of ambiguity is kind of unwanted when we want to build reliable systems. Thats why we start to setup protocols between our actors.

object LaughProtocol {
  sealed trait LaughMessage

  case object Lol extends LaughMessage
  case object Rofl extends LaughMessage
  case object Lmao extend LaughMessage
}

class LaughActor extends Actor with ActorLogging{

  import LaughProtocol._

  override def receive = {
    case msg @ LaughMessage => handleLaugh(msg)
    case msg @ _ => log.info("unexpected message :: {}", msg)
  }

  def handleLaugh(msg: LaughMessage) = {
    case Lol => println("L O L")
    case Rofl => println("R O F L")
    case Lmao => println("L M A O")
  }

}

This brings us the simplicity of being able to be sure about the nature of the messages that we are going to handle. Also... for any other actor communicating with LaughActor, it clearly spells out the rules for communicating with LaughActor.

Now... you will say what was wrong with just defining these as Strings.

class LaughActor extends Actor with ActorLogging{

  override def receive = {
    case "Lol" => println("L O L")
    case "Rofl" => println("R O F L")
    case "Lmao" => println("L M A O")
    case msg @ _ => log.info("unexpected message :: {}", msg)
  }

}

You can argue that even in this case, any developer can look at the code of this actor and figure out the protocol. And while it is true that they can but when looking at real world code spanning dozens of files... it becomes very very difficult.

And not only that... you loose one of your most important helper in writing correct code which is called compiler. Just consider following lines taken from a potential user of LaughActor.

// case 1 - With sealed hierarchy of messages
laughActorRef ! LuaghProtocol.Lol

// case 2 - with strings,
laughActorRef ! "lol"

What if the developer committed a mistake and wrote following instead of above

    // case 1 - With sealed hierarchy of messages
laughActorRef ! LuaghProtocol.Loll

// case 2 - with strings,
laughActorRef ! "loll"

In the first case the compiler will immediately point out the error... but in the second case this error will pass un-noticed and may cause a lot of headache to debug when hidden in a code base of tens of thousands of lines.

But again... you can even avoid this issue by using only pre-defined strings, like following

object LaughProtocol {
  val Lol = "Lol"
  val Rofl = "Rofl"
  val Lmao = "Lmao"
}

class LaughActor extends Actor with ActorLogging{

  import LaughProtocol._

  override def receive = {
    case msg if msg.equals(LaughProtocol.Lol) => println("L O L")
    case msg if msg.equals(LaughProtocol.Rofl) => println("R O F L")
    case msg if msg.equals(LaughProtocol.Lmao) => println("L M A O")
    case msg @ _ => log.info("unexpected message :: {}", msg)
  }
}

But... consider a bigger application with dozens of actors written by a team of 5-6 developers.

Notice that... as of now developers are solely relying on the defined members LaughProtocol.Lmao etc... and they actually do not look at what values LaughProtocol.Lmao etc have.

Now... lets say another actor has a protocol like following,

object LoveProtocol {
  val LotsOfLove = "Lol"
}

Now... you should be able to notice the problem. Consider the case of any actor which is supposed to handle messages of both of these protocols. All it will ever receive is a String Lol and it will have no way of knowing whether it is a LaughProtocol.Lol or LoveProtocol.LotsOfLove.

So... now whenever a developer is adding any new message to any of the protocols he need to make sure that no other protocol is using the same String. And this is simply not an option for teams working on large code bases.

And these are just few of the reasons why people prefer things like sealed protocols in their code bases.

Comments