May 25, 2012

Fun with interfaces and higher-order functions


Imagine you're developing a runtime environment which can be used by other developers as a container for components. I've done it in my cells package for event-driven applications. Here the user has to implement an interface called Behavior. It defines the four methods

  • Init(env *Environment, id string) error
  • ProcessEvent(e Event, emitter Eventemitter)
  • Recover(r interface{}, e Event)
  • Stop() error

Those behavior implementations can now be deployed to an Environment with user-defined id for later subscription/unsubscription or removing. But an id must not be used more than once. This has to be checked before the behavior instance will be created. Otherwise it could be that this type allocates memory or even opens files or network connections.

But how to do that? I simply created another type called BehaviorFactory which is simply a func() Behavior. So if the behavior implementation has a constructor function like

func myBehaviorFactory() Behavior {
    return &myBehavior{}
}

the deployment can be done with

myEnv.AddCell("myId", myBehaviorFactory)

Now AddCell() first checks if the id is free. Only in this case the instance is created by calling the factory function and then call Init() on the new created behavior.

But sometimes you may need to pass some configuration data to a behavior. How can we realize that? Here higher-order functions are helping. Instead of writing a simple factory function just write a factory returning function.

func newMyBehaviorFactory(fileName string) BehaviorFactory {
    return func() Behavior { return &myBehavior{fileName} }
}

Now the deployment can be done with

myEnv.AddCell("myId", newMyBehaviorFactory(fileName))

The call of newMyBehaviorFactory() now creates the factory function which then is used inside of AddCell().

But there is more fun with interfaces. The behaviors shall be able to control some runtime aspects. Those are the length of the event queue (aka channel) and if the instances shall be pooled, how large the pool shall be and if the instances of this pool are stateful or not. Many behaviors don't care, they simply implement the Behavior interface. But those who are interested implement the AsynchronousBehavior interface with the method

  • QueueLength() int

 and/or the PoolableBehavior interface with the method

  • PoolConfig() (poolSize int, stateful bool).

 Inside of AddCell() the type assertion is now used to check if the behavior implements those interfaces like

queueLength := 1
if ab, ok := behavior.(AsynchronousBehavior); ok {
    queueLength = ab.QueueLength()
}

This way the behaviors only have to implement those methods if needed, pretty simple. Go really is a wonderful pragmatic and flexible language.

No comments:

Post a Comment