WoW:Object-oriented programming

From AddOn Studio
Jump to navigation Jump to search

WoW Lua


Object Oriented Programming (OOP) is a relatively new concept (compared to procedural programing) which allows code to become "objects." These objects can be duplicated, extended, and in some languages, be converted between relatives. Object Oriented Programming also allows for programmers to maintain organization in their programs. This is especially vital in WoW addons, as all addons share the same execution environment and thus can conflict if poorly organized.

Namespaces

A namespace is an object-oriented concept which allows for organization of variables, functions, and other elements of a program. A namespace encapsulates code such that to access that code, you must first reference the namespace to which you are referring. The global namespace is one such default namespace -- all global variables are considered to be in the "global namespace." Programmers can create other namespaces, thus organizing code and reducing pollution of the global namespace. The following example in C++ shows a good example of how namespaces work, and why they are useful:

  namespace foo
  {
     int a = 1;  // Make an integer here
  };
  namespace bar
  {
     int a = 2;  // Make another integer here
  };
  cout << foo::a << "\n";      // Prints 1
  cout << bar::a << "\n";      // Prints 2
  cout << a << "\n";           // Illegal - "a" is undefined.

Namespaces maintain program organization and contain code which might other conflict. It is essentially another form of limiting variable scope, but indirectly.

Lua Namespaces

In Lua, namespaces don't directly exist. However, Lua programmers typically take advantage of several key, and unique, features which are available in the language to provide an indirect mechanism which can be considered setting up a namespace. Some other programmers, unfamiliar with languages that don't allow for the creation of namespaces, may confuse these with classes, but the major difference is that only one instance of a namespace ever exists, while there can be many separate instances of a class.

In Lua, a function is a variable. As such, it should be noted that a reference to a function can be used wherever a variable is used. The implications of this is that functions can be stored inside tables. Furthermore, in WoW, all addons share the same execution environment, therefore it is vital that some form of scope limitation is used so common variable names do not cause addon conflicts. Namespaces are one such method to organize code, thus limiting conflicts. fuck your self

Creation of a Namespace

The concept of a Lua namespace is simple. A single global variable, a table, is created, which then can be used to reference all other variables, which are not global. This allows for minimal global namespace pollution, thus minimizing (but not preventing) addon conflicts due to identically named variables. The following code creates the same variable namespaces as the above C code:

  foo = {};        -- Create a foo namespace
  bar = {};        -- Create a bar namespace
  -- Remember that foo.a is equivalent to foo["a"]
  foo.a = 1;       -- Make an integer in foo
  bar.a = 2;       -- Make an integer in bar
  DEFAULT_CHAT_FRAME:AddMessage(foo.a); -- Prints 1
  DEFAULT_CHAT_FRAME:AddMessage(bar.a); -- Prints 2

From this example it can be easily seen that creating namespaces in Lua is extremely simple; more simple than is available in many other languages.

Namespace Use

Many Lua programmers consider the use of Lua namespaces as an indicator of a good understanding of the language. This is because it is very organized and requires a good understanding of several language features. Encapsulating your addon within one or a few namespaces can result in allowing your addon to use a minimal number of global variables, thus minimizing its global namespace pollution.

Many addons authors create a single namespace which encapsulates all its code. This limits the addon's exposure without limiting functionality. Instead of creating 10 global functions, for example, the addon instead has a single namespace and 10 functions which are accessible within that namespace. The following code segment provides a bad example which unnecessarily pollutes the global namespace, and then provides a functionally equivalent example, which is far more organized. It creates two functions and two variables in the namespace:

  -- Pollutes the global namespace, making 2
  -- global variables and 2 global functions
  MyAddon_A = 1;
  MyAddon_B = 2;
  function MyAddon_PrintA()
     DEFAULT_CHAT_FRAME:AddMessage(MyAddon_A);
  end
  function MyAddon_PrintB()
     DEFAULT_CHAT_FRAME:AddMessage(MyAddon_B);
  end
  -- Creates a "MyAddon" namespace and then creates
  -- 2 variables, 2 functions inside it
  MyAddon = {};
  MyAddon.a = 1;
  MyAddon.b = 2;
  function MyAddon.Printa()
     DEFAULT_CHAT_FRAME:AddMessage(MyAddon.a);
  end
  function MyAddon.Printb()
     DEFAULT_CHAT_FRAME:AddMessage(MyAddon.b);
  end

As is directly evident, the appropriate use of namespaces can cause code to be far more organized and limit conflicts. This is necessary in an environment such as WoW, where several authors will write several addons, each of whom has no idea what the other is writing, and conflicts must be avoided as much as possible.

Classes

Classes are very similar to namespaces, and are implemented almost identically in Lua. Many programmers consider classes to be the fundamental concept of Object Oriented Programming, and the building block upon which other concepts can be created. They, like namespaces, are containers, but containers that can be duplicated. You can have many instances of a class, even though it's only declared once. Take the following C++ example:

  class Foo
  {
  public:    
     int bar; -- Creates a variable "bar"
  };
  -- The class "Foo" can now effectively be considered a variable type
  -- and thus we can make more "Foos."
  Foo baz, quux; -- Makes two Foos
  -- We can now access the int inside our Foos:
  baz.bar = 1;      -- Note that these are
  quux.bar = 2;     -- two different variables

Classes allow reuse of code and organization of blocks which could be considered one element. For example, we could create a class "character," which stores the character's name, level, race, and class. Obviously all four of these are related, so putting the variables in a single class is better than creating four variables out in the open. Furthermore, we could have many characters, so instead of creating 4 variables every time, we could just create 1 (a new character) which then handles making all of the appropriate variables that are stored for that character. In the examples that follow in this section, we will use this example of storing characters in a class "Character," so that we can consistently provide examples to establish a better understanding.

Lua Classes

Lua classes are handled similarly to Lua namespaces. Like many concepts in Lua, their implementation relies on the power of Lua tables to perform their task. One thing Lua namespaces don't have to do, however, that Lua classes do, is create themselves. As stated earlier, classes need to be reproducable atomically -- that is, multiple instances of classes need to be able to be instantiated. This is done through a Lua concept called "metatables."

Metatables are a complex concept. For information regarding them, see the [Lua documentation]. It is not, however, necessary to understand their entire purpose to create a class; it is only necessary to understand how they can be employed to accomplish our goal.

Instantiation and Construction

A class is constructed typically by a function. This provides a single point of creation which is very modular. Typically this function is called "new," though it is not required. First, the function must create a blank table. Then, it must imbue the table with the properties of the class. This may be functions, variables, or other contents which the class must store. the following example shows a simplistic class which creates a Character instance, which stores only the name of the character.

  Character = {};
  function Character:new()
     local self = {};         -- Create a blank table
     self.name = "Unknown";   -- Make a name variable in the class
     return self;             -- Return the instance
  end

We can now create multiple instances of the class very quickly:

  local player1 = Character:new(); -- Create a Character
  local player2 = Character:new(); -- Create another
  player1.name = UnitName("player");
  player2.name = UnitName("target");

We will discuss later why the name "self" is logical for this table we create, but it can actually be any name of your choosing. It is, however, typically seen as "self" because it has logical connections to the syntax used in functions, as will be discussed later.

Member Variables

A "member variable" is a term used to identify a variable that exists in a class. Member variables are not explicitly declared in Lua, but rather they are typically created during construction (when the class is instantiated).

  function Character:new()
     local self = {};
     self.name = "Unknown";
     self.level = 0;
     self.class = "Unknown";
     self.race = "Unknown";
     return self;
  end

As you can see, the variables are created upon construction, much in the manner which you would find in a namespace. A table is created, and inside the table, variables are created and stored. That table is the new instance of the class. In this manner, Lua classes are different from many other languages, as variables can be added to a class at any time, but their initial state is always the same.

Member Functions

Like member variables, member functions are simply functions that are contained within the class. Even more interestingly, as functions are variables in Lua, functions can differ between classes. Since tables can store functions, adjusting the value of one of those functions can change the operation for that one instance, but leave the other instances identical. Sometimes this is preferred. However, sometimes, we want a change to propagate to all classes.

Explicit Function Creation

One form of creating member functions is shown in the following example. Like member variables, these functions can be created during construction, and thus it can be quickly seen how each class has its own instance of a function, as well, thus they can be changed in the future if desired:

  function Character:new()
     local self = {};
     -- <Create variables as above here>
     self.isAlliance = function()
        if(   self.race == "Gnome"     or
              self.race == "Human"     or
              self.race == "Night Elf" or
              self.race == "Dwarf"     or
              self.race == "Draenei"      ) 
        then
           return true;
        else
           return false;
        end
     end
     return self;
  end

So, it is possible to create functions simply by defining variables. But there is an inherent problem with this system. Let's take the example that this class is being used in a character list of everyone you've ever seen. This list could be in the thousands, and as such, we will need to make thousands of instances of Character. That means creating thousands of copies of the exact same function. Certainly this function does not need to be unique to each instance of the class. So how do we avoid this obvious waste of memory? By using a metatable.

Metatables

Metatables are defined by the Lua documentation as "an ordinary Lua table that defines the behavior of the original value under certain special operations" [1]. We want to control one of these special operations. Specifically, we want to control how the table accesses variables that do not exist. While this may not seem immediately logical, it is a simple trick that we can use to convince Lua of functions that are defined for all instances of the class.

First, let's create the function. The function is created much like you would find any other function available. Normally, a function is created using the following syntax:

  function foo()
     -- Code block
  end

In our case, we need the function to be a member of the class we are defining. Thus, it needs to be contained within the base table, which in our case, is called "Character." For the above example "isAllance()," we would want to create the function like this:

  function Character:isAlliance()
     -- Code block
  end

Note the slight difference: We use a colon instead of a period here. This is typically used for non-static functions (functions which rely upon the state of the class, or the variables inside the instance of the class, to determine their operation). This is needed because we need to know the variable "race" inside our instance. This determines whether the function returns true or false. The use of this colon states that, when this function is called, a function-local variable will be created. The variable will be named "self" and is a reference to the instance of the class. Thus, we can use the variable "self.class" to determine the class of that one particular character. The Lua reference manual states the following[2]:

The colon syntax is used for defining methods, that is, functions that have an implicit extra parameter self. Thus, the statement
function t.a.b.c:f (params) body end
is syntactic sugar for
t.a.b.c.f = function (self, params) body end

We still have one problem, though. The instance of our class. While it is created by the Character table, it is not actually Character table itself, thus if we do the following:

  local me = Character:new();
  DEFAULT_CHAT_FRAME:AddMessage(me:isAlliance());

This will fail, because me:isAlliance is undefined (me.isAlliance is never created; only Character.isAlliance). This is where metatables come in. A metatable can tell a table where to find indexes that do not exist. Thus, we can use it to help find functions which appear to not exist. Specifically, we specify that the instance should look up unknown indexes in the table "Character," so that when it tries to look for me.isAlliance, and does not find it, it will proceed by searching for Character.isAlliance, which does exist.

This is done via the metatable's "__index" field (note that is two underscores, not one). The __index field states that if a variable cannot be found, it will be searched for in the table specified by __index. Thus, we can do the following during creation:


  Character = {};                   -- To create a class, we must make a table
  Character.__index = Character;    -- Set the __index parameter to reference Character
  function Character:new()
     local self = {};
     setmetatable(self, Character); -- Set the metatable so we used Character's __index
     -- Imbue the class
     return self;
  end
  function Character:isAlliance()
     -- Perform calculation
  end

Now, every instance of Character also has a way to access isAlliance(). Thus, we only create one copy of the function, so we save a lot of memory, but every character has a way to access that function and use it appropriately. Even more usefully, if another addon later hooks Character.isAlliance(), you will find that all instances will use the new, hooked function. This way, we don't have to modify every instance of the class.

Example Class

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.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);
     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

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

Notes

Calling a member function using the dot operator (.) will not implicitly pass the parent table to the function.

local table = {
    msg = "Hello World",
    func = function(self, a, b, c)
        print(self.msg)
        print(a)
        print(b)
        print(c)
    end,
}
table.func(1,2,3) -- Results in the error, "attempt to index local 'self' (a nil value)"
table:func(1,2,3) -- Results in the expected:
-- Output --
"Hello World"
1
2
3