Object Orientation Closure Approach

lua-users home
wiki

Caveat

This is comparing a closures for objects approach with an extremely naive table based object approach. In most cases it would be considered idiomatic to make the methods for mariner objects part of a metatable so that each mariner instance would not require a hash for all the functions. This key design point means the memory comparisons below are not useful at all. The memory overhead for the closure approach will clearly be much less favorable compared to the sane method for implementing objects with tables.

Intro

This page describes alternative to ObjectOrientationTutorial

Please read the page mentioned above first to understand the differences of alternative method.

The most common OOP way in Lua would look like this:

mariner = {}



function mariner.new ()

   local self = {}



   self.maxhp = 200

   self.hp = self.maxhp



   function self:heal (deltahp)

      self.hp = math.min (self.maxhp, self.hp + deltahp)

   end

   function self:sethp (newhp)

      self.hp = math.min (self.maxhp, newhp)

   end



   return self

end



-- Application:                                                           

local m1 = mariner.new ()

local m2 = mariner.new ()

m1:sethp (100)

m1:heal (13)

m2:sethp (90)

m2:heal (5)

print ("Mariner 1 has got "..m1.hp.." hit points")

print ("Mariner 2 has got "..m2.hp.." hit points")

And the output:


Mariner 1 has got 113 hit points

Mariner 2 has got 95 hit points

We actually use the colon here to pass the object ('self' table) to function. But do we have to?

Simple case

We can get quite the same functionality in different manner:

mariner = {}



function mariner.new ()

   local self = {}



   local maxhp = 200

   local hp = maxhp



   function self.heal (deltahp)

      hp = math.min (maxhp, hp + deltahp)

   end

   function self.sethp (newhp)

      hp = math.min (maxhp, newhp)

   end

   function self.gethp ()

      return hp

   end



   return self

end



-- Application:                                                           

local m1 = mariner.new ()

local m2 = mariner.new ()

m1.sethp (100)

m1.heal (13)

m2.sethp (90)

m2.heal (5)

print ("Mariner 1 has got "..m1.gethp ().." hit points")

print ("Mariner 2 has got "..m2.gethp ().." hit points")

Here we've got not only variables `maxhp` and `hp` encapsulated, but also reference to `self` (note `function self.heal` instead of `function self:heal` - no more `self` sugar). This forks because each time `mariner.new ()` is invoked new independent closure is constructed. It is hard not to notice the performance improvement in all methods except access to private variables `hp` (`self.hp` in first case is faster then `self.gethp ()` in second). But lets see the next example.

Complex case

--------------------

-- 'mariner module':

--------------------

mariner = {}



-- Global private variables:

local idcounter = 0

local defaultmaxhp = 200

local defaultshield = 10



-- Global private methods

local function printhi ()

   print ("HI")

end



-- Access to global private variables

function mariner.setdefaultmaxhp (value)

   defaultmaxhp = value

end



-- Global public variables:

mariner.defaultarmorclass = 0



function mariner.new ()

   local self = {}



   -- Private variables:

   local maxhp = defaultmaxhp

   local hp = maxhp

   local armor

   local armorclass = mariner.defaultarmorclass

   local shield = defaultshield



   -- Public variables:

   self.id = idcounter

   idcounter = idcounter + 1



   -- Private methods:

   local function updatearmor ()

      armor = armorclass*5 + shield*13

   end



   -- Public methods:

   function self.heal (deltahp)

      hp = math.min (maxhp, hp + deltahp)

   end

   function self.sethp (newhp)

      hp = math.min (maxhp, newhp)

   end

   function self.gethp ()

      return hp

   end

   function self.setarmorclass (value)

      armorclass = value

      updatearmor ()

   end

   function self.setshield (value)

      shield = value

      updatearmor ()

   end

   function self.dumpstate ()

      return string.format ("maxhp = %d\nhp = %d\narmor = %d\narmorclass = %d\nshield = %d\n",

			    maxhp, hp, armor, armorclass, shield)

   end



   -- Apply some private methods

   updatearmor ()



   return self

end



-----------------------------

-- 'infested_mariner' module:

-----------------------------



-- Polymorphism sample



infested_mariner = {}



function infested_mariner.bless (self)

   -- No need for 'local self = self' stuff :)



   -- New private variables:

   local explosion_damage = 700



   -- New methods:

   function self.set_explosion_damage (value)

      explosion_damage = value

   end

   function self.explode ()

      print ("EXPLODE for "..explosion_damage.." damage!!\n")

   end



   -- Some inheritance:

   local mariner_dumpstate = self.dumpstate -- Save parent function (not polluting global 'self' space)

   function self.dumpstate ()

      return mariner_dumpstate ()..string.format ("explosion_damage = %d\n", explosion_damage)

   end



   return self

end



function infested_mariner.new ()

   return infested_mariner.bless (mariner.new ())

end



---------------

-- Application:

---------------

local function printstate (m)

   print ("Mariner [ID: '"..m.id.."']:")

   print (m.dumpstate ())

end



local m1 = mariner.new ()

local m2 = mariner.new ()

m1.sethp (100)

m1.heal (13)

m2.sethp (90)

m2.heal (5)

printstate (m1)

printstate (m2)

print ("UPGRADES!!\n")

mariner.setdefaultmaxhp (400) -- We've got some upgrades here

local m3 = mariner.new ()

printstate (m3)



local im1 = infested_mariner.new ()

local im2 = infested_mariner.bless (m1)



printstate (im1)

printstate (im2)



im2.explode ()

The output:


Mariner [ID: '0']:

maxhp = 200

hp = 113

armor = 130

armorclass = 0

shield = 10



Mariner [ID: '1']:

maxhp = 200

hp = 95

armor = 130

armorclass = 0

shield = 10



UPGRADES!!



Mariner [ID: '2']:

maxhp = 400

hp = 400

armor = 130

armorclass = 0

shield = 10



Mariner [ID: '3']:

maxhp = 400

hp = 400

armor = 130

armorclass = 0

shield = 10

explosion_damage = 700



Mariner [ID: '0']:

maxhp = 200

hp = 113

armor = 130

armorclass = 0

shield = 10

explosion_damage = 700



EXPLODE for 700 damage!!

It's all quite self-explained. We've got all the common OOP tricks in pretty clean and fast manner.

Gain or loose?

Time for battle. The arena is 'Intel(R) Core(TM)2 Duo CPU T5550 @ 1.83GHz'. The competitors are:

-- Table approach



--------------------

-- 'mariner module':

--------------------

mariner = {}



-- Global private variables:

local idcounter = 0

local defaultmaxhp = 200

local defaultshield = 10



-- Global private methods

local function printhi ()

   print ("HI")

end



-- Access to global private variables

function mariner.setdefaultmaxhp (value)

   defaultmaxhp = value

end



-- Global public variables:

mariner.defaultarmorclass = 0



local function mariner_updatearmor (self)

   self.armor = self.armorclass*5 + self.shield*13

end

local function mariner_heal (self, deltahp)

   self.hp = math.min (self.maxhp, self.hp + deltahp)

end

local function mariner_sethp (self, newhp)

   self.hp = math.min (self.maxhp, newhp)

end

local function mariner_setarmorclass (self, value)

   self.armorclanss = value

   self:updatearmor ()

end

local function mariner_setshield (self, value)

   self.shield = value

   self:updatearmor ()

end

local function mariner_dumpstate (self)

   return string.format ("maxhp = %d\nhp = %d\narmor = %d\narmorclass = %d\nshield = %d\n",

			 self.maxhp, self.hp, self.armor, self.armorclass, self.shield)

end





function mariner.new ()

   local self = {

      id = idcounter,

      maxhp = defaultmaxhp,

      armorclass = mariner.defaultarmorclass,

      shield = defaultshield,

      updatearmor = mariner_updatearmor,

      heal = mariner_heal,

      sethp = mariner_sethp,

      setarmorclass = mariner_setarmorclass,

      setshield = mariner_setshield,

      dumpstate = mariner_dumpstate,

   }

   self.hp = self.maxhp



   idcounter = idcounter + 1



   self:updatearmor ()



   return self

end



-----------------------------

-- 'infested_mariner' module:

-----------------------------



-- Polymorphism sample



infested_mariner = {}



local function infested_mariner_set_explosion_damage (self, value)

   self.explosion_damage = value

end

local function infested_mariner_explode (self)

   print ("EXPLODE for "..self.explosion_damage.." damage!!\n")

end

local function infested_mariner_dumpstate (self)

   return self:mariner_dumpstate ()..string.format ("explosion_damage = %d\n", self.explosion_damage)

end



function infested_mariner.bless (self)

   self.explosion_damage = 700

   self.set_explosion_damage = infested_mariner_set_explosion_damage

   self.explode = infested_mariner_explode



   -- Uggly stuff:

   self.mariner_dumpstate = self.dumpstate

   self.dumpstate = infested_mariner_dumpstate



   return self

end



function infested_mariner.new ()

   return infested_mariner.bless (mariner.new ())

end

and

-- Closure approach



--------------------

-- 'mariner module':

--------------------

mariner = {}



-- Global private variables:

local idcounter = 0

local defaultmaxhp = 200

local defaultshield = 10



-- Global private methods

local function printhi ()

   print ("HI")

end



-- Access to global private variables

function mariner.setdefaultmaxhp (value)

   defaultmaxhp = value

end



-- Global public variables:

mariner.defaultarmorclass = 0



function mariner.new ()

   local self = {}



   -- Private variables:

   local maxhp = defaultmaxhp

   local hp = maxhp

   local armor

   local armorclass = mariner.defaultarmorclass

   local shield = defaultshield



   -- Public variables:

   self.id = idcounter

   idcounter = idcounter + 1



   -- Private methods:

   local function updatearmor ()

      armor = armorclass*5 + shield*13

   end



   -- Public methods:

   function self.heal (deltahp)

      hp = math.min (maxhp, hp + deltahp)

   end

   function self.sethp (newhp)

      hp = math.min (maxhp, newhp)

   end

   function self.gethp ()

      return hp

   end

   function self.setarmorclass (value)

      armorclass = value

      updatearmor ()

   end

   function self.setshield (value)

      shield = value

      updatearmor ()

   end

   function self.dumpstate ()

      return string.format ("maxhp = %d\nhp = %d\narmor = %d\narmorclass = %d\nshield = %d\n",

			    maxhp, hp, armor, armorclass, shield)

   end



   -- Apply some private methods

   updatearmor ()



   return self

end



-----------------------------

-- 'infested_mariner' module:

-----------------------------



-- Polymorphism sample



infested_mariner = {}



function infested_mariner.bless (self)

   -- No need for 'local self = self' stuff :)



   -- New private variables:

   local explosion_damage = 700



   -- New methods:

   function self.set_explosion_damage (value)

      explosion_damage = value

   end

   function self.explode ()

      print ("EXPLODE for "..explosion_damage.." damage!!\n")

   end



   -- Some inheritance:

   local mariner_dumpstate = self.dumpstate -- Save parent function (not polluting global 'self' space)

   function self.dumpstate ()

      return mariner_dumpstate ()..string.format ("explosion_damage = %d\n", explosion_damage)

   end



   return self

end



function infested_mariner.new ()

   return infested_mariner.bless (mariner.new ())

end

Speed test code for table approach:

assert (loadfile ("tables.lua")) ()



local mariners = {}



local m = mariner.new ()



for i = 1, 1000000 do

   for j = 1, 50 do

      -- Poor mariner...

      m:sethp (100)

      m:heal (13)

   end

end

Speed test code for closures approach:

assert (loadfile ("closures.lua")) ()



local mariners = {}



local m = mariner.new ()



for i = 1, 1000000 do

   for j = 1, 50 do

      -- Poor mariner...

      m.sethp (100)

      m.heal (13)

   end

end

The result:


tables:

real    0m47.164s

user    0m46.944s

sys     0m0.006s



closures:

real    0m38.163s

user    0m38.132s

sys     0m0.007s

Memory usage test code for table approach:

assert (loadfile ("tables.lua")) ()



local mariners = {}



for i = 1, 100000 do

   mariners[i] = mariner.new ()

end



print ("Memory in use: "..collectgarbage ("count").." Kbytes")

Memory usage test code for closures approach:

assert (loadfile ("closures.lua")) ()



local mariners = {}



for i = 1, 100000 do

   mariners[i] = mariner.new ()

end



print ("Memory in use: "..collectgarbage ("count").." Kbytes")

The result:


tables:

Memory in use: 48433.325195312 Kbytes



closures:

Memory in use: 60932.615234375 Kbytes

No winners, no loosers. Let's see what we've got here after all...

Outro


+---------------------------------------+---------------------------+-------------------------------------------------+

| Subject                               | Tables approach           | Closured approach                               |

+---------------------------------------+---------------------------+-------------------------------------------------+

| Speed test results                    | 47 sec.                   | 38 sec.                                         |

| Memory usage test results             | 48433 Kbytes              | 60932 Kbytes                                    |

| Methods declaration form              | Messy                     | More clean                                      |

| Private methods                       | Fine                      | Fine                                            |

| Public methods                        | Fine                      | Fine                                            |

| Private variables                     | Not available             | Fine                                            |

| Public variables                      | Fine                      | Fine                                            |

| Polymorphism                          | Fine                      | Fine                                            |

| Function overriding                   | Ugly (namespace flooding) | Fine (if we grant access only to direct parent) |

| Whole class definition (at my taste)  | Pretty messy              | Kinda clean                                     |

+---------------------------------------+---------------------------+-------------------------------------------------+

As you can see, both approaches are pretty similar by functionality but have significant differences by representation. The choice of approach mostly depends on aesthetic preferences of programmer. I personally would prefer to use closure approach for big objects and tables with data only (no function references) for small things. BTW, don't forget about metatables.

See Also


RecentChanges · preferences
edit · history
Last edited October 25, 2011 6:43 pm GMT (diff)