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 lose?

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 losers. 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 February 1, 2023 6:04 am GMT (diff)