Object Orientation Closure Approach


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 12:04 am GMT (diff)

AltStyle によって変換されたページ (->オリジナル) /