Our Fathers’ Faults – Mixing Actors and OOP 2 – Acting is not an inherited trait

This is the second part of the post on why Actors and OOP are really a bad match. In the last post we have seen how adding types and methods to actors could turn into a bad idea, now we look at another aspect of OOP – actors and inheritance.

Once you have wrapped an actor inside an object as our fathers did, you can hardly resist the temptation of composing by inheritance. On paper this is also a good idea, think for example to some sort of service that has some housekeeping to do (registering/unregistering clients, notify clients), what’s wrong in having a base class Service from which LedService can be inherited?

Since we have wrapped the actor inside something like this

class ActorRefTyped[+T: ClassTag, BaseMsg](
  val untypedActorRef: ActorRef
) {
  def tell(msg: BaseMsg, sender: ActorRef): Unit = {
    untypedActorRef.tell(msg, sender)
  }
}

We now will have a double hierarchy – one branch has the typed actor inheritance and the other branch has the actor inheritance. Wrappers’ inheritance must map exactly the actor inheritance. Having two parallel hierarchies is usually a pain to maintain. Any change you do one branch you have to do it on the other as well.

Suppose we accept the troubles coming from a double hierarchy and move on. You may recall that actors receive messages via overriding the receive partial function. You can actually replace one receive partial function with another one by a call to context.become( newReceive ). This switch is convenient to implement state machine, or at least for acknowledging specific states of actors where message processing needs to be performed in a different way.

Encapsulating actors in objects and following an OO approach, you would hide the internal implementation, states included, from the external world, heirs and ancestors included. Now you have two more problems in parent/children relationship:

  • how the receive of the child and the receive of the parent interact,
  • how the context.become deals with parent/children states.

Troubles are not over yet – actors have a bunch of functions that may be overridden to hook in some custom behavior. The most used is the preStart function which is called when the actor is ready, but not yet started. In a hierarchy, you have to manually chain preStart of current inheritance level and the ancestors. But you cannot control when the preStart of heirs will be called. Consider a base actor class for managing some service. The ancestor may want to perform some initialization (e.g. querying a lower level service handler) before handing the control to the heir.

This is a real case, and has been managed by creating a custom receive for handing the control and letting the door open for some troubles if the heir wants to use a preStart.

Lesson learned composing actor behavior via class inheritance is looking for trouble, you end up with a code that is rigid and fragile. I haven’t found (yet?) a universal recipe to compose actor behavior. My impression is that Akka actor model does not cope well with composition. From case to case I found that moving common code into a distinct actor or into a distinct class may help factorization of behavior. (Yes, I know it is not a real answer to the problem and I am open to suggestions)

And this is the last post for 2019, I hope you had a good year and wish you for an even better 2020.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.