Object Orientation Closure Approach |
|
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.
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?
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.
-------------------- -- '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.
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...
+---------------------------------------+---------------------------+-------------------------------------------------+ | 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.