WoW:Object-oriented programming: Difference between revisions
mNo edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
[[Category:Interface Customization]] | |||
[[Category:Glossary]] | |||
[[Category:Guides]] | |||
[[Category:Lua functions| Object Oriented Programming]] | |||
{{tocright}} | {{tocright}} | ||
Object Oriented Programming (OOP) is a | Object Oriented Programming (OOP) is a modern programming paradigm centered on the behavior of objects. While there are many ways to slice a problem, the OOP way involves defining one or more objects. Each object has a type (its class). It is the job of the programmer to define the classes of objects needed to solve their problem. For a more complete description of OOP, check out the [http://en.wikipedia.org/wiki/Object_oriented_programming wikipedia article]. | ||
== What about Lua? == | |||
Lua is not inherently object oriented. However, because Lua is so flexible, its easy to achieve object oriented behavior. We can accomplish encapsulation in Lua by making our variables members of a table. This almost guarantees that other addon writers will not accidentally redefine our variables to mean other things. | |||
Any member of a table can be accessed using dot notation. In the following code snippets, we define a table that represents a Wow character. The following are equivalent: | |||
// 1 | |||
Character = {} | |||
Character.name = "Murp" | |||
Character.faction = "Horde" | |||
function Character.cheerFaction() | |||
SendChatMessage("For the "..Character.faction) | |||
end | |||
=== | // 2 | ||
Character = { | |||
["name"] = "Murp", | |||
["faction"] = "Horde", | |||
["cheerFaction"] = function() | |||
SendChatMessage("For the "..Character.faction) | |||
end, | |||
} | |||
At this point, Character contains 3 variables: name, faction and cheerFaction. The Wow client runs all addon code in a shared execution environment. That environment will contain one object called Character. Thats great and all, but what we really wanted was to be able to create lots of objects that are Characters (maybe one for each toon in your guild or each toon in your raid). And they can't all be named Murp. | |||
== Constructors == | |||
In OOP, all objects know how to ''instantiate'' (or create an instance of) themselves. Whenever an instance of a class is needed, that class's constructor is called. In Lua, we'll do the same. | |||
You can name your constructor whatever you want, but for sanity's sake, most people call it ''new''. | |||
We can now create multiple instances of the class | Character = {}; | ||
function Character:new() | |||
local self = {}; -- Create a blank table | |||
self.name = "Unknown"; -- Make a name variable in the class | |||
self.faction = "Unknown";-- Make a faction variable in the class | |||
return self; -- Return the instance | |||
end | |||
We can now create multiple instances of the class: | |||
local player1 = Character:new(); -- Create a Character | local player1 = Character:new(); -- Create a Character | ||
| Line 106: | Line 51: | ||
player2.name = UnitName("target"); | player2.name = UnitName("target"); | ||
All member variables should be initialized in the constructor. We want each instance to have its own ''name'' and other attributes, so we create a ''name'' and give it a starting value every time we construct a new Character. | |||
== Member functions == | |||
Unlike member variables, we do not need more than one copy of each member function. For this reason, we don't need to initialize member functions in the constructor. | |||
If we define the functions outside the constructor, only one copy is ever made (in some cases, this can make your addon '''much''' more efficient). | |||
Character = {}; | |||
function Character:new() | |||
local self = {}; -- Create a blank table | |||
self.name = "Unknown"; -- Make a name variable in the class | |||
self.faction = "Unknown";-- Make a faction variable in the class | |||
return self; -- Return the instance | |||
end | |||
function Character:cheerFaction() | |||
SendChatMessage("For the "..self.faction) -- Using the : operator instead of . is shorthand for passing self as the first parameter | |||
end | |||
At this point, we can now create new Character objects, but when we try to call the member function, it doesn't work as we'd hope. | |||
local player1 = Character:new(); -- Create a Character | |||
player1.name = UnitName("player"); | |||
player1.faction = UnitFactionGroup("player"); | |||
player1:cheerFaction() -- player1.cheerFaction was never defined. we would like lua to find Character.cheerFaction() in its place | |||
== Metatables == | |||
We'll accomplish this with metatables. According to the Lua documentation, "Every value in Lua may have a metatable. This metatable is an ordinary Lua table that defines the behavior of the original value under certain special operations. You can change several aspects of the behavior of operations over a value by setting specific fields in its metatable." [http://www.lua.org/manual/5.1/manual.html]. | |||
In particular, we want to control how the table accesses variables that do not exist. We specify that the instance should do a lookup in the table "Character" whenever the variable (or function) can't be found in ''self''. This way, when it can't find player1.cheerFaction(), it searches for Character.cheerFaction() instead, which does exist. | |||
All metatables have a field called "__index" (note that is two underscores, not one). The __index field specifies an alternate search location--a fallback for when normal lookups fail. Here's the constructor code with this change: | |||
Character = {}; | |||
Character.__index = Character; -- Set the __index parameter to reference Character | |||
function Character:new() | |||
local self = {}; -- Create a blank table | |||
setmetatable(self, Character); -- Set the metatable so we used Character's __index | |||
self.name = "Unknown"; -- Make a name variable in the class | |||
self.faction = "Unknown"; -- Make a faction variable in the class | |||
return self; -- Return the instance | |||
end | |||
function Character:cheerFaction() | |||
SendChatMessage("For the "..self.faction) | |||
end | |||
Now, every instance of Character can access cheerFaction() and we can get away with only one copy of the function. Even more usefully, if another addon later hooks Character.cheerFaction(), all instances will use the new hooked function. | |||
== Full Example == | |||
This is the fully written class we have created through the above methods. It has a constructor, creates the required fields, and gives each instance two functions: isAlliance and isHorde, which returns true if the character's race is set to an Alliance or Horde race, respectively. It also has a function load(), which loads data given a Unit ID via the Blizzard API. | This is the fully written class we have created through the above methods. It has a constructor, creates the required fields, and gives each instance two functions: isAlliance and isHorde, which returns true if the character's race is set to an Alliance or Horde race, respectively. It also has a function load(), which loads data given a Unit ID via the Blizzard API. | ||
| Line 205: | Line 111: | ||
self.race = "Unknown"; | self.race = "Unknown"; | ||
self.class = "Unknown"; | self.class = "Unknown"; | ||
self.faction = "Unknown"; | |||
self.level = 0; | self.level = 0; | ||
return self; | return self; | ||
| Line 216: | Line 123: | ||
self.class = UnitClass(uid); | self.class = UnitClass(uid); | ||
self.level = UnitLevel(uid); | self.level = UnitLevel(uid); | ||
self.faction = UnitFactionGroup("player"); | |||
return true; | return true; | ||
end | end | ||
| Line 235: | Line 143: | ||
return ~self:isAlliance(); | return ~self:isAlliance(); | ||
end | end | ||
end | |||
function Character:cheerFaction() | |||
SendChatMessage("For the "..self.faction) | |||
end | end | ||
| Line 245: | Line 156: | ||
end | end | ||
== See Also == | |||
* [[Lua Scope]] | |||
[[ | |||
Revision as of 17:38, 13 November 2007
Object Oriented Programming (OOP) is a modern programming paradigm centered on the behavior of objects. While there are many ways to slice a problem, the OOP way involves defining one or more objects. Each object has a type (its class). It is the job of the programmer to define the classes of objects needed to solve their problem. For a more complete description of OOP, check out the wikipedia article.
What about Lua?
Lua is not inherently object oriented. However, because Lua is so flexible, its easy to achieve object oriented behavior. We can accomplish encapsulation in Lua by making our variables members of a table. This almost guarantees that other addon writers will not accidentally redefine our variables to mean other things. Any member of a table can be accessed using dot notation. In the following code snippets, we define a table that represents a Wow character. The following are equivalent:
// 1
Character = {}
Character.name = "Murp"
Character.faction = "Horde"
function Character.cheerFaction()
SendChatMessage("For the "..Character.faction)
end
// 2
Character = {
["name"] = "Murp",
["faction"] = "Horde",
["cheerFaction"] = function()
SendChatMessage("For the "..Character.faction)
end,
}
At this point, Character contains 3 variables: name, faction and cheerFaction. The Wow client runs all addon code in a shared execution environment. That environment will contain one object called Character. Thats great and all, but what we really wanted was to be able to create lots of objects that are Characters (maybe one for each toon in your guild or each toon in your raid). And they can't all be named Murp.
Constructors
In OOP, all objects know how to instantiate (or create an instance of) themselves. Whenever an instance of a class is needed, that class's constructor is called. In Lua, we'll do the same.
You can name your constructor whatever you want, but for sanity's sake, most people call it new.
Character = {};
function Character:new()
local self = {}; -- Create a blank table
self.name = "Unknown"; -- Make a name variable in the class
self.faction = "Unknown";-- Make a faction variable in the class
return self; -- Return the instance
end
We can now create multiple instances of the class:
local player1 = Character:new(); -- Create a Character
local player2 = Character:new(); -- Create another
player1.name = UnitName("player");
player2.name = UnitName("target");
All member variables should be initialized in the constructor. We want each instance to have its own name and other attributes, so we create a name and give it a starting value every time we construct a new Character.
Member functions
Unlike member variables, we do not need more than one copy of each member function. For this reason, we don't need to initialize member functions in the constructor.
If we define the functions outside the constructor, only one copy is ever made (in some cases, this can make your addon much more efficient).
Character = {};
function Character:new()
local self = {}; -- Create a blank table
self.name = "Unknown"; -- Make a name variable in the class
self.faction = "Unknown";-- Make a faction variable in the class
return self; -- Return the instance
end
function Character:cheerFaction()
SendChatMessage("For the "..self.faction) -- Using the : operator instead of . is shorthand for passing self as the first parameter
end
At this point, we can now create new Character objects, but when we try to call the member function, it doesn't work as we'd hope.
local player1 = Character:new(); -- Create a Character
player1.name = UnitName("player");
player1.faction = UnitFactionGroup("player");
player1:cheerFaction() -- player1.cheerFaction was never defined. we would like lua to find Character.cheerFaction() in its place
Metatables
We'll accomplish this with metatables. According to the Lua documentation, "Every value in Lua may have a metatable. This metatable is an ordinary Lua table that defines the behavior of the original value under certain special operations. You can change several aspects of the behavior of operations over a value by setting specific fields in its metatable." [1].
In particular, we want to control how the table accesses variables that do not exist. We specify that the instance should do a lookup in the table "Character" whenever the variable (or function) can't be found in self. This way, when it can't find player1.cheerFaction(), it searches for Character.cheerFaction() instead, which does exist.
All metatables have a field called "__index" (note that is two underscores, not one). The __index field specifies an alternate search location--a fallback for when normal lookups fail. Here's the constructor code with this change:
Character = {};
Character.__index = Character; -- Set the __index parameter to reference Character
function Character:new()
local self = {}; -- Create a blank table
setmetatable(self, Character); -- Set the metatable so we used Character's __index
self.name = "Unknown"; -- Make a name variable in the class
self.faction = "Unknown"; -- Make a faction variable in the class
return self; -- Return the instance
end
function Character:cheerFaction()
SendChatMessage("For the "..self.faction)
end
Now, every instance of Character can access cheerFaction() and we can get away with only one copy of the function. Even more usefully, if another addon later hooks Character.cheerFaction(), all instances will use the new hooked function.
Full Example
This is the fully written class we have created through the above methods. It has a constructor, creates the required fields, and gives each instance two functions: isAlliance and isHorde, which returns true if the character's race is set to an Alliance or Horde race, respectively. It also has a function load(), which loads data given a Unit ID via the Blizzard API.
Character = {};
Character.__index = Character;
function Character:new()
local self = {};
setmetatable(self, Character);
self.name = "Unknown";
self.race = "Unknown";
self.class = "Unknown";
self.faction = "Unknown";
self.level = 0;
return self;
end
function Character:load(uid)
if ~UnitExists(uid) then
return false;
end
self.name = UnitName(uid);
self.race = UnitRace(uid);
self.class = UnitClass(uid);
self.level = UnitLevel(uid);
self.faction = UnitFactionGroup("player");
return true;
end
function Character:isAlliance()
if ( self.race == "Human" or
self.race == "Night Elf" or
self.race == "Dwarf" or
self.race == "Gnome" or
self.race == "Draenei"
)
then return true;
else return false;
end
end
function Character:isHorde()
if (self.race == "Unknown") then
return false;
else
return ~self:isAlliance();
end
end
function Character:cheerFaction()
SendChatMessage("For the "..self.faction)
end
And we can quickly use this class to get information about our 40 raid members, and store them in a table:
local units = {};
for x = 1, 40 do
units[x] = Character:new();
units[x]:load("raid"..x);
end