jnfr
jnfr

Reputation: 931

Scala inheritance best practices using a container classes

I have architected some code like this:

A "HandlerArguments" class which will take in a bunch of factories and other helpers needed for all the handler subclasses:

class HandlerArguments(
    val a: A,
    val b: B,
    val c: C) { 
    /* nothing here */
}

A Handler superclass that will take in HandlerArguments:

class Handler(val args: HandlerArguments) {
    def subHandler1 = new SubHandler1(args)
    def subHandler2 = new SubHandler2(args)
    def subHandler3 = new SubHandler3(args)
    var message: Message = null

    /* code here that invokes subHandler1, 2, or 3 based on a matcher */
    def invokeCommand(msg: Message) = {
        message = msg
        someCommand match {
                    case command1 => subHandler1.something()
            case command2 => subHandler2.something()
            case command3 => subHandler3.something()
        }
    }
}

And the subHandlers, or subClasses:

class subHandler1(args: HandlerArguments) extends Handler(args) { 
...
    args.something.somethingElse(message.x)
...
}
class subHandler2(args: HandlerArguments) extends Handler(args) { ... }
class subHandler3(args: HandlerArguments) extends Handler(args) { ... }

And in another file, I am initializing the Handler:

/* initializing Handler */
val args = new HandlerArguments(a, b, c)
val handler = new Handler(args)
handler.invokeCommand(someMsg)

My questions are,

  1. is this the best way to do this? The main thing I want to achieve is not having to pass 'message' around a lot between the superclass Handler and the subclasses, (i.e. subHandler1.something(message)).

  2. How is my use of HandlerArguments? I thought about using traits or abstract classes but this is something that needs to be set once and then used by the Handler class.

  3. It seems weird that I'm passing args from Handler to the SubHandlers, only to pass it back to Handler in the extends clause. Is there a better way to do this?

Thoughts? Thanks!

Upvotes: 1

Views: 559

Answers (1)

Bhashit Parikh
Bhashit Parikh

Reputation: 3131

What you have here is an example of a command/lookup-table pattern.

A few things that I can offer off the top of my head are:

make HandlerArguments a case class. That way, it's a bit less verbose and you get a lot of added benefits.

case class HandlerArguments(a: A, b: B, c: C)

and you are done. The arguments to case class constructors are automatically considered vals.

Another problem that I can see is that the construction of subclass instances from the super-class would result in a stack-overflow (as subclass construction also constructs a super class instance, and the cycle goes on indefinitely).

However, according to your use case, there probably shouldn't be a superclass-subclass relationship between your Handler and the SubHandler* classes. The subHandler* classes are the actual handlers, that is, they are the ones that handle the messages. Your Handler class simply dispatches messages to them. What you can do here would be something like the following.

case class HandlerArgs(a: A, b: B, c: C)

trait MessageHandler {
  val args: HandlerArgs 
  def handle(msg: String)
}

class Handler1(val args: HandlerArgs) extends MessageHandler {
  override def handle(msg: String) = ???
}

class Handler2(val args: HandlerArgs) extends MessageHandler {
  override def handle(msg: String) = ???
}

class Handler3(val args: HandlerArgs) extends MessageHandler {
  override def handle(msg: String) = ???
}

class MessageDispatcher(args: HandlerArgs) {
  private val messageHandler1 = new Handler1(args)
  private val messageHandler2 = new Handler2(args)
  private val messageHandler3 = new Handler3(args)

  def dispatch(message: String) {
    val someCommand: Command = ???
    someCommand match {
      case Command("command1") => messageHandler1.handle(message)
      case Command("command2") => messageHandler2.handle(message)
      case Command("command3") => messageHandler3.handle(message)
    }
  }
}

val dispatcher = new MessageDispatcher(HandlerArgs(new A, new B, new C))
dispatcher.dispatch("<some command>")

Update after comment

If you want the messages to be shared by all the instances, one option is to add the template pattern to the existing code:

trait MessageHandler {
  val args: HandlerArgs 
  private var message: Option[String] = None
  // Save the message in a common implementation, and...
  private def saveMessage(msg: String) {
    message = Option(msg)
  }

  def handle(msg: String) {
    saveMessage(msg)
    doSomethingWithMessage(msg)
  }

  // let the subclasses handle the the actual message handling
  protected def doSomethingWithMessage(msg: String)

}

class Handler1(val args: HandlerArgs) extends MessageHandler {
  override def doSomethingWithMessage(msg: String) = ???
}

// rest of the message handlers

Of course, there are several ways of doing this.

Another way, as discussed in the comments, was to use a single element container. For ex.

class MessageHolder {
  var message: Option[String] = None
}

trait MessageHandler {
  val args: HandlerArgs
  val messageHolder: MessageHolder
  def handle(msg: String)
}

class Handler1(val args: HandlerArgs, 
               val messageHolder: MessageHolder) extends MessageHandler {
  override def handle(msg: String) = ???
}


class MessageDispatcher(args: HandlerArgs) {
  private val mh = new MessageHolder
  private val messageHandler1 = new Handler1(args, mh)
  private val messageHandler2 = new Handler2(args, mh)
  private val messageHandler3 = new Handler3(args, mh)

  def dispatch(message: String) {
    val someCommand: Command = ???
    mh.message = Option(message)
    // ...
  }
}

Upvotes: 1

Related Questions