Last Updated: February 25, 2016
·
664
· abe33

Generic Objects Pool with CoffeeScript

Here's another usage of my previous mixins helper in order to easily create objects pools.

Objects pools allow to reuse instances of a class to avoid garbage collection overhead when releasing these instances. Instances are created only if no other instance are available and have to be explicitely released through the pool.

Let's start with a quick reminder, the Function's extensions:

Function::include = (mixins...) ->
  excluded = ['constructor']
  for mixin in mixins
    excl = excluded.concat()
    excl = excl.concat mixin::excluded if mixin::excluded?
    @::[k] = v for k,v of mixin.prototype when k not in excl
    mixin.included? this

  this

Function::extend = (mixins...) ->
  excluded = ['extended', 'included']
  for mixin in mixins
    excl = excluded.concat()
    excl = excl.concat mixin.excluded if mixin.excluded?
    @[k] = v for k,v of mixin when k not in excl
    mixin.extended? this

  this

Function::concern = (mixins...) ->
  @include.apply(this, mixins)
  @extend.apply(this, mixins)

This is a bit more complex than in my previous tip. Below a quick explanation of these extensions.

  • The Function::include method decorates the class prototype with the passed-in mixin prototype.
  • The Function::extend method decorates the class with the passed-in object properties.
  • The Function::concern decorates both.

Properties can be excluded using an excluded property defined on either the prototype or the class according to the type of injection used.

Hooks on inclusion/extension can be created using either included or extended properties on the mixin class.

The Instances Pool Mixin

Now let's create the InstancesPool mixin.

The InstancesPool will provide two class methods to retrieve/release instances and will stores theses instances in two arrays. One for used instances, the other for released instances.

One constraint on poolable classes is that they should avoid having constructor arguments. As instance will be reused, the constructor will only be called once. To workaround that, instead of the constructor, poolable classes should make their initialization in the init method. This method will be called each time the instance move from the unsused pool to the used pool.

class InstancesPool
  # The two objects stores are created in the extended hook to avoid
  # that all the class extending `InstancesPool` shares the same instances.
  @extended: (klass) ->
    klass.usedInstances = []
    klass.unusedInstances = []

  # The `get` method returns an instance of the class.
  @get: (options={}) ->
    # Either retrieve or create the instance.
    if @unusedInstances.length > 0
      instance = @unusedInstances.shift()
    else
      instance = new this

    # Stores the instance in the used pool.
    @usedInstances.push instance

    # Init the instance and return it.
    instance.init(options)
    instance

  # The `release` method takes an instance and move it from the
  # the used pool to the unused pool.
  @release: (instance) ->
    # We can't release unused instances of instances created using
    # the `new` operator without using `get`.
    unless instance in @usedInstances
      throw new Error "Can't release an unused instance"

    # The instance is removed from the used instances pool.
    index = @usedInstances.indexOf(instance)
    @usedInstances.splice(index, 1)

    # And then moved to the unused instances one.
    @unusedInstances.push instance

  # Default `init` implementation, just copy all the options
  # in the instance.
  init: (options={}) -> @[k] = v for k,v of options

  # Default `dispose` implementation, call the `release` method
  # on the instance constructor. A proper implementation should
  # take care of removing/cleaning all the instance properties.
  dispose: -> @constructor.release(this)

Now let's create a poolable class and try creating/releasing some instances:

class PoolableClass
  @concern InstancesPool

instance1 = PoolableClass.get(x: 10, y: 20)
instance2 = PoolableClass.get(x: 20, y: 10)

console.log 'used:', PoolableClass.usedInstances.length
console.log 'unused:', PoolableClass.unusedInstances.length

instance2.dispose()

console.log '----'
console.log 'used:', PoolableClass.usedInstances.length
console.log 'unused:', PoolableClass.unusedInstances.length

instance3 = PoolableClass.get()

console.log '----'
console.log 'used:', PoolableClass.usedInstances.length
console.log 'unused:', PoolableClass.unusedInstances.length

instance1.dispose()
instance3.dispose()

console.log '----'
console.log 'used:', PoolableClass.usedInstances.length
console.log 'unused:', PoolableClass.unusedInstances.length