back Back to Downloads


An object-oriented programming (OOP) framework and packaging ecosystem. MayronObjects allows you to create well-defined packages with strict typing rules for other addon authors to use in their addon projects to improve the development experience.

downloaddownloaddownloadDownload from WoWInterfacedownloaddownloaddownloadDownload from CurseForge


MayronObjects is a framework designed to make object-oriented programming (OOP) easier for Lua developers. The framework is designed for World of Warcraft addon development and supports both Classic and Retail.

MayronObjects lets you:

  • Create classes and instantiate new instance objects modeled from those classes.
  • Protect private instance data - Each instance of a class has its own unique private instance data table that cannot be accessed outside the scope of class functions unless the developer chooses to do so. MayronObjects automatically injects this table as the first parameter for all class methods when called from an instance object.
  • Define and enforce strict typing rules for class method parameters and return value types. MayronObjects will detect invalid arguments passed to method parameters or returned from a method and will raise helpful Lua errors for the developer to fix. Checking for unexpected behavior improves the development experience and results in fewer bugs published and discovered by your users.
  • Define private and static class methods. Private methods must be called from the private data table using a unique Call method. Developers must call static methods from the class table itself. Both private and static methods support strict typing rules.
  • Create interfaces to enforce strict method and property rules to be implemented by classes and instances.
  • Define default parameter values, including both primitive types (strings and numbers) and complex types (tables, functions, Blizzard widgets, and other classes and interfaces).
  • Create reusable generic classes - Instances of a generic Class can specify the types to be used for generic parameter types defined by the generic class. Generic Classes allow developers to reuse the same class logic for multiple purposes, such as creating a list data structure that only works with number values and another list using the same class to only support string values.
  • Define attributes and attach them to class methods to apply pre-execution logic. Attributes can manipulate argument values before being passed to class method parameters. Attributes can also prevent class method executions if custom attribute conditions fail.
  • Create, export, and import packages containing entities, such as classes, interfaces, and attributes, to be shared with other developers.

Each class can inherit from at most one parent class but can implement multiple interfaces. All classes either directly or indirectly inherit from the base Object class, which provides many useful functions that all instances of any class can access. The framework also comes with standard collection classes (List, Stack, Map, LinkedList). You can remove these collection classes if you do not require them (make sure to also remove references to them insideMayronObjects.xml).

There is a Test.lua file included in the MayronObjects folder to see other working examples of how to use this framework.


Creating a Class

You must first create or import a package. Then you will be able to create a class from the package, which automatically stores the newly created class inside the package. Packages hold all the strict typing rules you define for your class methods. You can export packages for other developers to use, allowing them to access all classes inside the package, but this is optional (see section 5).

2.1 Static vs. Non-Static Methods

As mentioned in section 1, each instance has access to a private data table that is only available in the class method body when called from an instance of that class. These types of methods are referred to as non-static methods. You do not pass this data argument manually when calling the method from an instance object. Instead, MayronObjects will automatically assign this to the first method parameter.

This data table will always be the first argument of an instance-level (i.e., non-static) function even if you do not require it. You can think of this unique table in a similar way to how the self referential keyword is always available to you when calling a table function using :.

Static methods do not have access to a private instance data table because they do not belong to any class instance. The Static table attached to a class is just a handy way of declaring that something is static, meaning it belongs to the class itself. You should not call static methods from an instance object.

2.2 Creating a Class From a Package

Below is an example of how you create a class from an existing package, and how to create each method type:

local obj = MayronObjects:GetFramework(); local MyPackage = obj:CreatePackage("MyPackage"); local MyClass = MyPackage:CreateClass("MyClass"); -- This is a public instance method function MyClass:Print(data, message) print(message); -- Calling a private instance method data:Call("Bar", "This is a private message"); end -- This is a static (non-instance) class method function MyClass.Static:Foo(message) print(message); end -- This is a private instance method. function MyClass.Private:Bar(data, message) print(message); end local instance = MyClass(); instance:Print("This is a public message"); MyClass.Static:Foo("This is a static message");

Line 1 retrieves the framework from the global MayronObjects table. You can optionally specify the framework's version to retrieve in rare situations where another addon must use the same framework version as another addon when multiple versions exist. By default, GetFramework will return the last framework loaded if no version is specified. You can also pass "latest" as the first argument to GetFramework to retrieve the latest version of the framework loaded, but this approach is not recommended.

Line 2 creates a new local package. You can optionally export the package to be used elsewhere by any developer or addon (see sections 5 and 6). All entities, such as classes, interfaces, and attributes, are created from packages and are stored inside them. You can define strict-typing rules for class methods using their package (see section 4).

Line 3 creates a new class from the package with the class name MyClass. In this example, no parent class (nor any interface) is specified when creating the class. Therefore, it directly inherits from the base Object class (see section 11).

Line 6 declares a public instance method for this class. You cannot call instance methods directly from the class table (i.e., MyClass:Print()) because MayronObjects needs to locate and inject the private instance data table. Therefore, it should only be called from an instance of the class, as shown on line 24. The instance's private data can only be accessed from inside the class method body (unless given direct access by the developer, usually through getters and setters, as demonstrated in section 3).

Line 14 shows how to create a static class method, and line 25 shows you how you would call it directly from the class's Static table. These methods can and should be called directly from the class's static table rather than from a class instance. MayronObjects will not provide static methods with a data argument because there is no associated class instance involved.

Line 19 shows how to create a private instance method, and line 10 shows you how you would call it using the data table's Call method. You cannot call private methods using an instance object or directly from the class's Private table; otherwise, other code would be able to call them from outside the class method's body (and hence would defeat the goal of private methods).

Line 23 demonstrates how to create an instance from a class. You call the class as you would do for any standard Lua function.

Line 24 demonstrates how you do not manually provide a value to the data parameter of a public instance method. All arguments passed to a method call are assigned to all parameter values proceeding the data parameter. For example, Print is called with one argument value, and MayronObjects assigns this to the 2nd parameter, named message.


Private Instance Data

Developers should store all private instance data they wish to not make publicly accessible inside the data table. This table is passed to the first parameter of each class method, by MayronObjects, when called from a class instance object. MayronObjects provides each instance of a class its own unique private data table that is only accessible to that instance. Multiple instances of a class will each have their own private data table managed by MayronObjects. Changing the contents of one data table will not affect the contents of any other table. Using this data encapsulation technique ensures that outside code cannot access the instance's private data, preventing outside interference against essential class logic.

3.1 Getting and Setting Private Data

As the developer of a class, you can control how private data is accessed. Traditionally, you would use "getter" and "setter" methods to expose such data. Such methods can also benefit from strict-typing rules offered by MayronObjects. It would be best if you never revealed the entire data table as this gives too much control to external code and breaks the Open-Closed software engineering principle.

local obj = MayronObjects:GetFramework(); local MyPackage = obj:CreatePackage("MyPackage"); local MyClass = MyPackage:CreateClass("MyClass"); function MyClass:GetTimeRemaining(data) return data.timeRemaining; end function MyClass:SetTimeRemaining(data, timeRemaining) data.timeRemaining = timeRemaining; end local instance = MyClass(); instance:SetTimeRemaining(60); local timeRemaining = instance:GetTimeRemaining(); print(timeRemaining); -- prints: 60

You can assign values to the self referential variable. Self in this case represents the instance object. The only difference between assigning values to self, as opposed to data, is that these values are publicly accessible from the instance object. For example, if you used self.timeRemaining = 123 instead of data.timeRemaining = 123 then you can access the timeRemaining field from the instance variable outside of the class method body: instance.timeRemaining.

It is usually preferable to keep your instance's private data protected to avoid bugs and not use self to access these values (except to call other instance methods). For example, you could create a "setter" method to validate a user's input value before assigning it to your data table by declaring a strict-typing rule (see section 4).

Line 14 shows an external value passed to the SetTimeRemaining "setter", which is then retrieved using a "getter" method, as shown on line 15.

3.2 Private Methods

The data table has a unique Call method that allows you to call private instance methods. You provide the private method's name as the first parameter, followed by a list of arguments to be passed to that private method's parameter list. The private method may have strict-typing rules applied to it, and so the types of argument values must match those of the expected types defined by these rules.

Like standard instance methods, MayronObjects will supply the data table as the first parameter value. The self referential keyword will be the instance object (not the Private table attached to the class table).


-- constructor function Panel:__Construct(data) data.callbacks = {}; end -- public method MyPackage:DefineParams("function"); function Panel:OnExecuted(data, callback) data.callbacks["Executed"] = callback; end -- public method function Panel:Execute(data, ...) -- Calling a private function: data:Call("TriggerCallback", "Executed", ...); end -- private method MyPackage:DefineParams("string"); function Panel.Private:TriggerCallback(data, name, ...) if (obj:IsFunction(data.callbacks[name])) then data.callbacks[name](...); end end

In the example above, you cannot call the TriggerCallback private method from the instance object, even if you use the self referential keyword. Line 15 shows how you would call this. Line 21 uses the IsFunction helper function provided by the MayronObjects framework (see section 13 for a full list of utility methods).

3.3 Accessing "Friend" Class Instance Data

If two classes depend on each other, you can declare that one class is a "friend" of another class to share private instance data between the two. As mentioned previously, you should never expose the full private instance data table. However, if two classes collaborate closely and are inside the same package, it can be desirable to share data between instances of each class.

You can only permit an instance object to access a targetted instance's private data if you know its class name (i.e., the name of the class that the target instance was instantiated from). The target must also be from the same package. To do this, you use the static "AddFriendClass" function:

local Cell = WidgetsPackage:CreateClass("Cell"); Cell.Static:AddFriendClass("Panel");

In this example, the developer needs to create the Panel class (before or after calling AddFriendClass) using the same Widgets package. Any instance of Panel will then be able to get the private data of any instance of the Cell class. To do this, use the GetFriendData function from the data table:

function Panel:GetTotalWidth(data) local totalWidth = 0; for _, cell in ipairs(data.cells) do local cellData = data:GetFriendData(cell); totalWidth = totalWidth + cellData.width; end return totalWidth; end

Strict Typing Rules

Class method definitions help enforce strict typing rules to ensure that the method parameter and return values contain the expected types. Note that including strict typing rules is entirely optional. Still, this feature can help alleviate early bugs and improve the development process while defining a strict contract for how other developers should use your code.

The data argument should be ignored when creating strict typing rules as it will always be the same table value.

local obj = MayronObjects:GetFramework(); local MyPackage = obj:CreatePackage("MyPackage"); local MyClass = MyPackage:CreateClass("MyClass"); MyPackage:DefineReturns("number"); function MyClass:GetTimeRemaining(data) return data.timeRemaining; end MyPackage:DefineParams("number"); function MyClass:SetTimeRemaining(data, timeRemaining) data.timeRemaining = timeRemaining; end local instance = MyClass(); instance:SetTimeRemaining(60); local timeRemaining = instance:GetTimeRemaining(); print(timeRemaining); -- prints: 60 -- This results in an error: instance:SetTimeRemaining("60"); -- not a number

A package contains information relating to entities and method definitions. Therefore, declaring parameter and return value types must be done using the package object.

You need to include strict typing rules using DefineParams and DefineReturns, as shown on lines 5 and 10, straight before declaring the method. Otherwise, the package may assign the rules to the wrong method.

Line 5 states that the return value must be of type "number". The developer who created the class must comply with their promise by returning a number value.

Line 10 states that the first argument passed to the SetTimeRemaining function must be of type "number". Any developer using the class must comply with this enforced rule; otherwise, MayronObjects will trigger an error as illustrated on line 21.

You can also declare that any value should be returned, as long as it is not nil:

MyPackage:DefineParams("any"); function MyClass:SetTimeRemaining(data, timeRemaining) data.timeRemaining = timeRemaining; end

4.1 Static Class Method Definitions

Static class functions can also be assigned strict typing rules to specify parameter and return value definitions in the same way as private and public instance methods:

MyPackage:DefineParams("any"); function MyClass.Static:SetTimeRemaining(timeRemaining) self.TimeRemaining = timeRemaining; end MyClass.Static:SetTimeRemaining(60) print(MyClass.Static.TimeRemaining); -- prints: 60

In the above example, the only difference compared to the example shown previously using a public instance method is that the TimeRemaining property will be accessible from the class's Static table. Static functions and properties should only be assigned to the Static table and not the class itself.

4.2 Optional Method Parameters and Return Types

Additionally, you can declare optional parameter arguments and return values by adding a question mark ? character in front, or at the back, of the definition type. The recommended convention is to declare optional parameters at the end of the declaration list after any non-optional parameter types (similar to python code conventions) but this is optional:

MyPackage:DefineParams("string", "number", "?table"); MyPackage:DefineReturns("any", "function", "?boolean");

If you prefer adding ? to the back of the definition type then this is also acceptable:

MyPackage:DefineParams("string", "number", "table?"); MyPackage:DefineReturns("any", "function", "boolean?");

4.3 Variable List Definitions

MayronObjects lets you specify strict typing rules for variable argument lists, also known as varargs, passed to methods. These rules also work for multiple return values. All you need to do is include ... in front of the definition type. This definition type is referred to as a variable list definition for the sake of simplicity.

The example snippet below enforces the rule that any values passed to the method after the initial string value should only be number values. Any arbitrary number of return values should also be number values. You do not need to pass any values to a vararg, ore return any values, when using a variable list definition. These values are always optional but if you do pass some then they must match the definition type.

MyPackage:DefineParams("string", "...number"); MyPackage:DefineReturns("...number"); function MyClass:Execute(data, message, ...) return ...; end

Below is an example of values being passed to the method using this strict typing rule:

-- this is allowed: instance:Execute("hello", 1, 2, 3, 4); -- this is NOT allowed: instance:Execute("hello", 1, 2, "3", 4); -- this is allowed (values are always optional): instance:Execute("hello");

ALthough you do not need to pass values to a vararg, or return multiple values, when using a variable list definition, if you return an explicit nil value then this rule is violated.

An "explicit" nil value means that you have explicitly passed nil to a method, or returned nil from a method (e.g., f(nil)). An implicit nil is when you do not pass in anything (e.g., f()), and so a parameter or variable that was supposed to be assigned a return value remains equal to nil.

The entire variable list is optional but individual values are not. If you want the values inside the vararg, or the multiple return values to be optional (i.e., allow for nil values to be included in the list at any position), then you must use the optional ? inside the definition:

MyPackage:DefineParams("string", "...?number"); MyPackage:DefineReturns("...?number"); function MyClass:Execute(data, message, ...) return ...; end -- this is allowed: instance:Execute("hello", 1, nil, 3, 4); -- this is also allowed: instance:Execute("hello");

You cannot declare another definition type after specifying a variable list definition, as this must be the last definition supplied. This rule is similar to how a vararg parameter must be the last parameter of a function's signature:

-- this is NOT allowed: MyPackage:DefineParams("string", "...number", "table");

Finally, a variable list definition can be declared as a union type, meaning that any value in the list can be one or more types as specified by the definition:

-- this is allowed: MyPackage:DefineParams("string", "...number|table"); -- this is allowed: MyPackage:DefineParams("string", "...number|?table"); -- this is allowed: MyPackage:DefineParams("string", "...?number|table");

If you mark any of the definition types in the union variable list definition as optional (?), then explicit nil values can be used. Lines 5 and 8 above state that any value in the vararg can either be a number, table, or nil.

4.4 Object and Widget Types

When MayronObjects validates the type of an object it uses the GetObjectType() method. All objects created with this package inherit the GetObjectType() method from the base Object class (see section 11). Because Blizzard widgets (such as Frame, Button, Texture, etc.) also have a GetObjectType() method, you can also use MayronObjects to test for both the type of object created from a package and the type of widget.

MyPackage:DefineParams("Frame", "Button", "?MySlider"); MyPackage:DefineReturns("Handler", "MyClass", "?SomeWidget");

4.5 Parent Classes and Interfaces

If a class implements an interface (see section 8), you can use the interface name as the type of object to see if an object passed to the method call implements the given interface. Similarly, suppose a class has a parent class (see section 7). In that case, you can use the parent class type as a parameter definition, and any child class from any level (if the parent class also has a parent, etc.) will pass the validation test.

local Parent = MyPackage:CreateClass("Parent"); local Child = MyPackage:CreateClass("Child", Parent); local SomeClass = MyPackage:CreateClass("SomeClass"); -- an interface: -- see section 8, "Creating an interface" for more info local IComponent = MyPackage:CreateInterface("IComponent", { ... }); local Component = MyPackage:CreateClass("Component", nil, "IComponent"); MyPackage:DefineParams("Parent", "IComponent"); function SomeClass:Foo(obj) -- do stuff end local someInstance = SomeClass(); local child = Child(); local component = Component(); someInstance:Foo(child, component); -- this is allowed

4.6 Default Primative Parameter Values

Suppose you provide nothing or a nil value to a parameter, and the strict typing rules have specified a default value for that parameter. In that case, the parameter's value will default to that default value. For example, if you call the Print method in the below code snippet without any arguments, then message will be equal to the default "foobar" value and value will be equal to the default 14 value:

MyPackage:DefineParams("string=foobar", "number=14"); function MyClass:Print(data, message, value) print(message, value); end

Of course, by calling this method with two non-nil values, the default values are ignored. You do not need to include the optional syntax (?) at the front of declared value types if you have also specified a default value.

MayronObjects lets you also specify default values for variable list definitions, as well as union type variable list definitions:

MyPackage:DefineParams("...number=14"); function MyClass:PrintNumbers(data, ...) print(...); end

However, you can only define one default value for union types. In the example below, "foobar" will never be used. It is not possible for MayronObjects to know which value the nil was supposed to be (i.e., the value might have been either a number or a string value) so the first default value will always be used if nil is supplied:

MyPackage:DefineParams("...number=14|string=foobar"); function MyClass:PrintNumbersAndStrings(data, ...) print(...); end

Therefore, the above snippet is functionally equivalent to:

MyPackage:DefineParams("...number=14|string"); function MyClass:PrintNumbersAndStrings(data, ...) print(...); end

4.7 Default Complex Parameter Values

The above approach in section 4.5 works well for simple values parsed from a string, but more complicated parameter types are challenging to describe inside a string, such as widgets/frames, tables, and functions.

For this, use the following approach:

local myDefaultTable = { msg = "foobar" }; MyPackage:DefineParams({"table", myDefaultTable}); function MyClass:Print(data, tbl) print(tbl.msg); end

The above code snippet will print "foobar" if you omit to supply another table as the first argument when calling the Print method; else, MayronObjects will use the default table instead.

You can also use this syntax for default primative types as an alternative for the syntax expressed in section 4.5:

-- these are functionally equivalent: MyPackage:DefineParams("number=14"); MyPackage:DefineParams({"number", 14});

MayronObjects lets you also specify default complex values for variable list definitions, as well as union type variable list definitions:

MyPackage:DefineParams({"...number", 14}); function MyClass:PrintNumbers(data, ...) print(...); end

As previously mentioned, you can only define one default value for union types. In the example below, the table containing msg = "foobar" will never be used. It is not possible for MayronObjects to know which value the nil was supposed to be (i.e., the value might have been either a number or a table value) so the first default value will always be used if nil is supplied:

-- for union types (obj = the framework, see section 13): MyPackage:DefineParams({"...number|table", 14, { msg = "foobar" }}); MyPackage:DefineReturns({"...number|table", 30, { msg = "foobar" }}); function MyClass:PrintNumbersAndTables(data, ...) for _, value in obj:IterateArgs(...) do if (obj:IsTable(value)) then print(value.msg); -- will always be "custom" elseif (obj:IsNumber(value)) then print(value); -- will be 10 or 14 end end return ...; end local v1, v2, v3 = instance:PrintNumbersAndTables(10, nil, { msg = "custom"}); assert(v1 == 10); assert(v2 == 14); -- will be the default value from parameter definition assert(v3.msg == "custom");

Therefore, you only need to specify one default value and the default value can be assigned to any type in the union type (either the number or the table):

MyPackage:DefineParams({"...number|table", 14}); MyPackage:DefineReturns({"...number|table", { msg = "foobar" }});

Exporting Packages

Entities such as classes, interfaces, and attributes, must be created from packages. A package cannot be imported from MayronObjects unless it has been exported. By default, packages are not made available outside of the file they are created from unless assigned a public namespace. Once a package has a namespace, any developer can import and access that package from anywhere in the codebase.

There are two methods for assigning a namespace to a package:

  1. Using the framework's Export function, and optionally assigning a parent package namespace.
  2. When creating the package by supplying the namespace of a parent package as the 2nd argument.

The latter option will only export a package intrinsically if you provide the parent package's namespace:

local MyPackage = obj:CreatePackage( "MyPackage", "RootPackage.AnotherPackage");

If the package must be exported at a later point in time, use the Export framework function:

obj:Export(MyPackage, "RootPackage.AnotherPackage");

In the examples above, both RootPackage and AnotherPackage are other packages, where "AnotherPackage" is a sub-package of RootPackage. Both packages do not have to exist to perform this operation. Instead, the exporting process creates these packages. MyPackage is now a sub-package of "AnotherPackage".

If you do not provide the parent package namespace as the 2nd argument when calling CreatePackage, then it will not be exported. However, if you do not provide the parent package namespace as the 2nd argument when calling Export, then the package will still be exported but the namespace will be the name of the package:

local MyPackage = obj:CreatePackage("MyPackage"); obj:Export(MyPackage); local SamePackage = obj:Import("MyPackage"); assert(MyPackage == SamePackage); -- this will pass

If a developer wanted to create a class that inherits MyParent from another file where no reference to MyParent exists, they would either need to supply the full namespace:

local MyChild = MyPackage:CreateClass("MyChild", "MyPackage.MyClasses.MyParent");

Or, import it first using the full namespace and then including the parent class directly as the 2nd argument:

local MyParent = obj:Import("MyPackage.MyClasses.MyParent"); local MyChild = MyPackage:CreateClass("MyChild", MyParent);

Importing Packages or Entities

Unlike the Export function mentioned in the previous section, the Import function can import not just another package with a valid namespace but also individual entities, such as classes, interfaces, or attributes.

-- File 1: local obj = MayronObjects:GetFramework(); local MyPackage = obj:CreatePackage("MyPackage"); obj:Export(MyPackage); local CheckButton = MyPackage:CreateClass("CheckButton"); local Button = MyPackage:CreateClass("Button"); local Slider = MyPackage:CreateClass("Slider"); local TextArea = MyPackage:CreateClass("TextArea"); local FontString = MyPackage:CreateClass("FontString"); local Animator = MyPackage:CreateClass("Animator");

File 1 starts by creating several classes inside a new package called MyPackage under the namespace MyAddOn.Widgets.MyPackage.

-- File 2: local obj = MayronObjects:GetFramework(); -- import an individual class from the package: local CheckButton = obj:Import("MyPackage.CheckButton"); -- import the entire package: local WidgetsPackage = obj:Import("MyPackage"); -- Then, get individual classes from the package: local Button = WidgetsPackage:Get("Button"); local Slider = WidgetsPackage:Get("Slider"); local TextArea = WidgetsPackage:Get("TextArea"); local FontString = WidgetsPackage:Get("FontString"); local Animator = WidgetsPackage:Get("Animator");

File 2 shows how to import individual entities from the package defined in File 1 by importing class CheckButton using the full namespace of that class. It also shows how to retrieve individual entities from that package using the package's Get method. Other entities can be created and added to the package from any file once imported.


Inheritance, Constructors and Destructors

A class can be assigned a constructor and a destructor. MayronObjects will execute constructors when instantiating an instance from a class and execute destructors when destroying it. Constructors are specified using the __Construct method name, and destructors are specified using __Destruct.

Instances are destroyed either explicitly by the developer or implicitly by the garbage collection process when a reference to the instance no longer exists. To explicitly destroy an instance, a developer must manually call its Destroy() method.

When calling a class method that does not exist in that class, MayronObjects will check all parent classes. If a parent class has a method with the same name, then this method is executed instead. The self keyword will point to the instance object that initially called the method, and the data parameter will be that instance's private data table. Therefore, any changes to the data table from the parent's class method will also be available in any child class method for that instance object; this also applies to both constructors and destructors as shown below:

local Parent = MyPackage:CreateClass("Parent"); local Child = MyPackage:CreateClass("Child", Parent); function Parent:__Construct(data) data.message = "Assigned by parent"; print("parent constructor triggered!"); end function Child:Print(data) assert(data.message == "Assigned by parent"); print(data.message); end local child = Child(); child:Print(); -- prints: "Assigned by parent"

Line 2 shows how to assign a parent class to a new class being created by supplying it as the 2nd argument when calling the package's CreateClass function.

When instantiating a new instance object from the Child class, as shown on line 14, MayronObjects will check for a __Construct method on the Child class first. If found, it will execute this and ignore any parent constructors. If it is missing, MayronObjects will keep searching up the parent hierarchy until it finds one or the base Object class is reached. The Objects class is the only class with no parent and marks the endpoint for traversing the hierarchy. The Object class does not have a constructor, and so unlike other functions, nothing occurs if MayronObjects cannot find a constructor. Usually, if a method called from an instance object does not exist on the class or any of its parent classes, MayronObjects will raise an error. However, constructors as well as destructors are entirely optional and are an exception to this rule.

Line 15 will use the child class's Print method. However, if this method did not exist and were instead part of the parent class, this method would be used instead, but the self and data values would be identical. For example, by changing line 9 to function Parent:Print(data), then the line 15 would have the same effect.

However, if a child class method, constructor, or destructor "shadows" one found in a parent class (meaning the child method has the same name as a parent method), then MayronObjects will pick the child's method to use instead. The example below shows how MayronObjects uses the child's constructor instead of the parent's constructor. The child instance then calls the Talk method, provided by the parent, and prints "I am a child!". Finally, the destructor is called by executing the Destroy method, which is inherited from the base Object class (see section 11 for all Object class methods):

local Parent = MyPackage:CreateClass("Parent"); function Parent:Talk(data) print(data.dialog); end function Parent:__Construct(data) data.dialog = "I am a parent."; end local Child = MyPackage:CreateClass("Child", Parent); function Child:__Construct(data) data.dialog = "I am a child!"; end function Child:__Destruct(data) data.dialog = nil; print("Child object Destroyed!"); end local child = Child(); child:Talk(); -- prints: "I am a child!" child:Destroy(); -- prints: "Child object Destroyed!"

7.1 Calling Parent Methods From Child Shadow Methods

If both the child class and parent class share the same method name, then the child method will always be used by MayronObjects. However, it is possible to call the parent method using the unique Parent property attached to the instance object:

local GrandParent = MyPackage:CreateClass("GrandParent"); local Parent = MyPackage:CreateClass("Parent", GrandParent); local Child = MyPackage:CreateClass("Child", Parent); function GrandParent:Print(data) data.total = data.total + 1; print("Print method called:", tostring(data.total), "times!"); end function Parent:Print(data) data.total = data.total + 1; self.Parent:Print(); end function Child:Print(data) data.total = 1; self.Parent:Print(); end Child():Print();

Line 20 triggers a chain of Print calls up the hierarchy and prints: "Print method called: 3 times!". This example demonstrates how the data table is shared between each class method, no matter where it is in the hierarchy.

7.2 Calling a Parent Constructor

To call a parent constructor, use the Super class method inherited from the base Object class:

MyPackage:DefineParams("table", "number"); function GrandParent:__Construct(_, tblValue, numValue) print("message:", tblValue.value, "- id:", tostring(numValue)); end MyPackage:DefineParams("table"); function Parent:__Construct(_, tblValue) self:Super(tblValue, 123); end MyPackage:DefineParams("string"); function Child:__Construct(_, strValue) self:Super({ value = strValue .. "bar" }); end Child("Foo");

The above code snippet also shows that class methods with the same name connected hierarchically can have different strict typing rules. The child constructor only accepts a string argument value for its first parameter, but its parent requires a table and a number value.

The results of the above code snippet prints: "message: Foobar - id: 123".


Creating an Interface

Interfaces can be a great way of specifying rules and patterns for a system or API. Interfaces cannot inherit or implement other entities. However, a class may optionally implement one or more interfaces to guarantee that a class implements a method or property defined by the interface.

Interfaces avoid unexpected errors from missing expected properties or methods and avoid the need for multiple conditional checks. This feature improves error handling as errors occur at the right point in time, such as during the illegal reassignment of property values. An addon can also use interfaces to manage multiple classes that may differ in their implementations. By sharing method signatures, each class can be managed by calling and accessing the same methods in the same way. For example, an addon's module system could manage modules by calling module lifecycle methods (e.g., enable, disable, initialize). Each module may differ significantly in their implementation but inherit the same interface to state that they must contain these lifecycle methods, and therefore, external code can interact with them.

We can specify strict typing rules using interface types when the concrete class type of an argument is not required. If we know that a given class implements a specific method, set of methods, and possibly some properties, we should be able to work with this without knowing the specific type of class. Interfaces allow us to be flexible and pass any concrete implementation of that interface to a method by declaring that a parameter accepts an object implementing that interface.

The package's CreateInterface method takes two arguments: the interface's name and an interface definition table. The definition table specifies all interface methods and properties that must be defined before an instance of a class can be successfully created; else, MayronObjects will throw an error. For methods, the class must implement them before the player logs into World of Warcraft. For properties of an instance, these must be assigned the correct value type inside the constructor (__Construct). If the interface property definition says that the property can be optional (i.e. ?table), these do not need to be assigned immediately in the constructor. However, if assigned a value, then the type of that value must still be of the correct type (see section 8.2 for more information).

8.1 Interface Functions

A that implements an interface with defined methods must setup those methods in the same way that a class usually sets up methods. If any function is missing, then an error will be throw when logging into the game. MayronObjects will also throw an error if an attempt at redefining the same method already specified by the interface, using the DefineParams and DefineReturns package methods, is detected.

When a value is said to be an instance of an interface, meaning the instances' class implements that interface, we should expect it to contain an implementation of all methods and properties defined by that interface. For example, say we have several ConfigHandler classes containing a RenderMenu method. It would be tedious to write checks for whether a table contains the required method because it may change by mistake in the future. Instead, we can assign it an IConfigHandler interface to enforce the rule that the method should exist and guarantee that it still accepts the required parameter types and returns the expected return types. It might not always be the same class but this should not matter. These guarantees help to preserve the integrity of whatever system is being built and maintained.

MyPackage:DefineParams("IConfigHandler") function ConfigMenu:SetUp(data, handler) data.menu = handler:RenderMenu(); end

Assume that the IConfigHandler specifies a RenderMenu method and must return a Frame. Any instance of the IConfigHandler passed to the method, as illustrated above, is expected to implement this method. Therefore, any concrete ConfigHandler class should not be allowed to change the interface's definition of that method because we need to guarantee that data.menu is assigned a Frame. Concrete classes should remain entirely anonymous for enforcing the dependency inversion principle. However, this does not mean that the class cannot add new definitions to other methods not previously defined by the interface. It merely means that classes cannot override already defined ones.

ComponentsPackage:CreateInterface("IComponent", { Update = "function"; SetEnabled = {type = "function"; params = "boolean" }; IsEnabled = {type = "function"; returns = "boolean"}; __Construct = {type = "function", params = { "?table", "IEventHandler", "?Frame" }}; }); local Component = ComponentsPackage:CreateClass( "Component", nil, "IComponent"); function Component:Update(data) -- implement the interface function here end function Component:SetEnabled(data, enabled) -- "enabled" must be a boolean. -- Do NOT use MyPackage:DefineParams(); data.enabled = enabled; end function Component:IsEnabled(data) -- must return a boolean value return true; end -- "labels" must be nil or a table -- "eventHandler" must be an object of type IEventHandler -- "frame" must be nil or a Frame widget function Component:__Construct(data, labels, eventHandler, frame) end A class cannot implement from two or more interfaces that share the same method name as this would clash and result in an error.

When calling CreateInterface, we pass an interface definition table as the 2nd argument. In the example above, we declare four methods (one being the constructor). The class assigned this IComponent interface must implement all four of these and cannot override their definitions.

Suppose you do not need to define parameter types or return value types for a method in the interface definition table. In that case, it is possible to add the method name as the key to the interface definition table and assign it the value "function":

Update = "function";

If you want to assign a parameter and/or return type for that method then you should use a table:

SetEnabled = { type = "function"; params = "boolean" };

If you want to assign multiple parameter and/or return types, then params and/or returns should be assigned a table as well:

__Construct = { type = "function", params = { "?table", "IEventHandler", "?Frame" } };

A class can implement many interfaces but only inherit from one parent class. Interfaces can be specified using their full namespace address or by supplying the interface table itself:

local MyClass = MyPackage:CreateClass( "MyClass", MyParent, IInterface1, IInterface2, IInterface3);

8.2 Interface Properties

We can expand on the previous code to add interface properties. Properties are considered public, so we know what to expect when attempting to access them. Therefore, they do not belong inside the private instance data table and are assigned to the self reference variable. Required properties must be assigned inside the constructor (i.e. __Construct); else, MayronObjects produced an error stating that the instance has not successfully implemented the interface. Optional properties can be assigned at any point as long as the property value type is correct.

Unlike methods, you can reassign the value of a property at any time as long as it still matches the specified type:

ComponentsPackage:CreateInterface("IComponent", { -- properties: MenuContent = "Frame"; -- a required Frame widget MenuLabels = "?table"; -- an optional table TotalLabelsShown = "?number"; -- an optional number Button = "Button"; -- a required Button widget }); function Component:__Construct(data, labels, eventHandler, frame) -- required = must be implemented in the constructor -- optional = can be implemented now or later. -- properties are assigned to `self` self.MenuContent = frame or CreateFrame("Frame"); self.MenuLabels = labels; -- this will be nil self.Button = CreateFrame("Button"); -- this is not a property data.eventHandler = eventHandler; end local C_EventHandler = Lib:Import("MyEngine.Events.EventHandler"); local myComponent = Component(nil, C_EventHandler()); -- this can be reassigned to another Button widget: myComponent.Button = CreateFrame("Button");


You can assign attributes to class methods to execute pre-execution logic. Attributes are custom classes that run code inside an OnExecute method before MayronObjects calls the class method. These OnExecute methods are called with the instance, the instance's private data, the name of the method to be called, and all parameters the method will be called with.

Attributes allow you to manipulate the class method's arguments before it is called or even prevent it from being called by returning false from the OnExecute function. A useful example of this is to cancel a method's execution if the player is in combat.

The attribute described below has already been built and can be found in the `MayronObjects > Attributes` folder. -- Import the attributes package: local Attributes = obj:Import("Framework.System.Attributes"); -- Create attribute (must implement IAttribute interface) local InCombatAttribute = Attributes:CreateClass( "InCombatAttribute", nil, "IAttribute"); function InCombatAttribute:__Construct(data, executeLater, silent) data.executeLater = executeLater; data.silent = silent; end function InCombatAttribute:OnExecute( data, instance, instanceData, methodName, ...) if (not InCombatLockdown()) then -- continue executing the method return true; end if (data.executeLater) then -- Add some logic to call the methodlater -- (e.g. callLater:Add(instance, methodName, ...); return false; end if (data.silent) then -- do not call the method return false; end Lib:Error("Failed to execute %s.%s: Cannot execute while in combat.", instance:GetObjectType(), funcName); end

Once an attribute has been defined, we can attach it to any method, and the OnExecute method will be called before the method it is applied to is called. In this case, if the player is in combat, we return false, and the method is not called but will be cached and called after the player exits combat (triggered by the "PLAYER_REGEN_ENABLED" event):

MyPackage:SetAttribute("Framework.System.Attributes.InCombatAttribute"); function MyClass:SensitiveOutOfCombatCode() -- calling this code while the player is in combat -- will cause addon taint. InCombatAttribute prevents this! end

Of course, similar to CreateClass, you can import the attribute and then use the object as the first parameter of SetAttribute:

local InCombatAttribute = obj:Import( "Framework.System.Attributes.InCombatAttribute"); MyPackage:SetAttribute(InCombatAttribute); function MyClass:SensitiveOutOfCombatCode() -- calling this code while the player is in combat -- will cause taint. InCombatAttribute prevents this! end

Generic Types

Generic types specify what types of values an instance of a Generic class can use. Use the Of class method when instantiating an instance to specify what types to use:

local KeyValuePair = TestPackage:CreateClass("KeyValuePair<K, V>"); TestPackage:DefineParams("K", "V"); function KeyValuePair:Add(data, key, value) -- do stuff... end local values = KeyValuePair:Of("string", "number")(); values:Add("test", 123); local callbacks = KeyValuePair:Of("string", "function")(); callbacks :Add("OnEnable", function() print("hello!"); end);

Generic types are useful for reusing the same core class logic for different types of values. Rather than defining the same class twice and copying/pasting the same code but tweaking the values to use strict typing rules, it is easier to use generic types. As shown above, two versions of the same KeyValuePair class are used by instantiating two instances, but one store's strings and numbers, and the other stores strings and functions.


Base Object Methods

Classes are assigned a parent class explicitly by the developer, or MayronObjects assigns them the Object class as their default parent class. This means that all classes inherit from Object directly or indirectly (e.g., Object might be the parent of the parent's, parent's class). Therefore, all Object methods are accessible by all instances created using the framework.

Below is a list of all available Object methods:


  • @return (string): the class name (string).


  • @param objectName (string): The class or interface name.

Returns a boolean value indicating if the instance type is directly, or indirectly, the same type as the objectName supplied. If the instance inherits from a parent (or a parent's parent, etc.), or implements an interface whose name is equal to the objectName argument, then the instance is said to be of that type and return true.


  • @param other (table): Another MayronObjects class (or value).

Returns a boolean value indicating whether two instances are equal. Two classes can be equal if all private data key and value pairs match regardless of whether the instance table references are different.


  • @return (Class): the parent class of the instance.


  • @return (Class): the class of the instance.


  • @return (Object): a new instance whose private data key and value pairs match the original, cloned instance.


local clonedInstance = originalInstance:Clone(); print(originalInstance:Equals(clonedInstance)); -- prints true


Removes all private instance data keys and calls the classes destructor (__Destruct).


Package Methods

Below is a list of Package class methods. The library Import and CreatePackage methods return instances of this Package class.


  • @param class (Class): The class created by MayronObjects to be protected.

If you create a class, you can mark it as protected. Protected classes cannot have methods re-assigned or modified.


  • @param instance (Object): Any object instantiated from a class.
  • @param propertyName (string): The name of the instance property to protect.

This method prevents a property attached to an instance of a class from being deleted or changed. For example, LibMayronDB uses this to prevent the profile and global table properties attached to the database object from being deleted or changed as this could cause huge problems.


  • @param package (Package): The package object to be added to the currently selected package.

Adds a previously created package as a sub-package of another package without needing to export either of the packages. Usually, you would use obj:Export(MyPackage, "MyAddOn.Widgets"), makingMyPackagea sub-package of theWidgets` package as indicated by the namespace. However, you may wish for your package only to be used by you and do not want other Lua files or addons to access it.


-- @returns (Package): The parent package of the currently selected package as defined by the namespace address. The child package is said to be a sub-package of the parent package.

This method returns the parent package containing the current package. If the current package's namespace is MyAddOns.Widgets.MyPackage, then this will return the Widgets package.


  • @param entityName (string): the entity's name to retrieve from the package.
  • @param silent (boolean): prevents MayronObjects from throwing an error if the entity cannot be found.

This method returns an entity (e.g., a class, interface, or sub-package) contained within the package. Throws an error if the entity is not found unless silent is set to true.


  • @param className (string): the name of the class to create for this package
  • @param parentClass (Object): a parent class to inherit from
  • @vararg ... (Object|string): a variable argument list of optional interface entities (or interface names to be imported as entities) the newly created class should implement.
  • @return Object the newly created class

Creates a new class and adds it to the package.


  • @param interfaceName (string): the name of the interface to create for this package.
  • @param definition (table): a table containing property and/or function names with type definitions for property values, function parameters and return types.
  • @return (Object): the newly created interface.

Creates a new interface and adds it to the package. The definition object provides strict typing rules that the class must enforce. For example, if the class does not implement a defined method, an error is thrown as soon as the player logs into the game. See section 8, "Creating an interface", for more information.


  • @vararg ... (string|table): A variable argument list of either strings or tables to depict strict typing rules for parameter values.

Use this method right before defining a class method belonging to the package to enforce strict typing rules for parameter arguments.

For example, the following code snippet enforces the rule that argument message requires a string value. For more information, please see step 4, "Strict Typing Rules":

MyPackage:DefineParams("string"); function MyClass:Print(data, message) print(message); end


  • @vararg ... (string|table): A variable argument list of either strings or tables to depict strict typing rules for return values.

Use this method right before defining a class method belonging to the package to enforce strict typing rules for return values.

For example, the following code snippet enforces the rule that the return value total must be a number value. For more information, please see step 4, "Strict Typing Rules":

MyPackage:DefineReturns("string"); function MyClass:GetID(data) return 123; end


Use this method right before defining a class method belonging to the package to mark the method as "virtual". There are rare situations where a parent class must call one of its methods, but a child class must handle the implementation of that method. This is inspired by the template method pattern.

Usually, a child cannot override a parent class method or change the definition of a previously defined method, but you can do this when the parent class method has been defined as virtual. This method tells MayronObjects that the parent class has consented to the reimplementation of one of its methods by any child class

In the example below, the child class redefines the parameter definition of the virtual parent class method. The parent can then call this virtual method, and the child method with the same name is called instead. Usually, you wouldn't redefine method definitions and would only use virtual methods for call methods lower down the hierarchy.

MyPackage:DefineVirtual(); MyPackage:DefineParams("string"); function Parent:CallMe(_, _) error("Parent.CallMe should NOT be called!"); end function Parent:Execute(data) -- calls the child CallMe function with the child definition self:CallMe(123); assert(data.value == 123); -- this will pass. end MyPackage:DefineParams("number"); -- override the parent function definition function Child:CallMe(data, num) data.value = num; end

Currently, you cannot create virtual private instance methods. This only works for public instance methods.


  • @param attributeClass (Object|string): Can either be the namespace of the attribute class to use, or the attribute class itself.

Use this right before defining a class method belonging to the package. The attribute's OnExecute method will be called right before the instance function (belonging to that class) is called.

The package will handle creating an instance of this class for you. You can pass a variable argument list of arguments to be passed to the attribute's constructor (__Construct) if needed.

For more information on attributes, see step 9, "Attributes".


  • @param func (function): Apply a function to all entities found inside the package.

Can be used to print all entity names using GetObjectType().


  • return (number): the size of the package, i.e. the total number of items found inside the package.


  • @return (string): the name of the package.

Framework Utility Methods

These methods are attached to the framework table and are used for custom error handling and utility purposes. The framework table also contains the Import and Export methods discussed previously, but these are not considered utility methods.


  • @param silent (boolean): Errors triggered by MayronObjects will be added to the error log if set to true, rather than being alerted in-game.


  • @param condition (boolean): A predicate to evaluate.
  • @param errorMessage (optional string): An error message to throw if condition is false.
  • @vararg any: A list of arguments to be inserted into the error message using string.format.

Runs the Lua assert function on the condition. If the condition is false, a formatted error message (using string.format with the arguments passed to the variable argument list) will be thrown unless obj:SetSilentErrors(true); is called beforehand.


  • @param errorMessage (string): The error message to throw.
  • @vararg any: A list of arguments to be inserted into the error message using string.format.

Immediately throws a formatted error message (using string.format with the arguments passed to the variable argument list) using the lua error function unless obj:SetSilentErrors(true); is called beforehand.


Returns the error log (table), consisting of all accomulated errors triggered while MayronObjects was set to silent mode (i.e. obj:SetSilentErrors()).


Empties the error log.


  • @return (number): the number of errors collected.


-- __@vararg ... (any): A variable argument list to iterate over.

This utility method is a more efficient way of iterating over a variable argument list or for any situation where a table is needed to hold values only for iterating purposes such as:

for id, value in ipairs({...}) do -- do stuff end

For example, if this for-loop is called every 0.2 seconds, you are creating a new anonymous table each time, resulting in a memory leak. Use IterateArgs to avoid this:

for id, value in Lib:IterateArgs(...) do -- do stuff end


  • @return (table): an empty table from an internal table stack (a reusable table). Returns a new table if the stack is empty.


  • @param tbl (table): The table to add to the internal stable stack.

Add a table to the internal table stack for later reuse. This method will remove any metatable set to the table and empty it in the process.


  • @param tbl (table): The table to print.
  • @param depth (?number): An optional number specifying the depth of sub-tables to traverse through and print.
  • @param spaces (?number): An optional number specifying the spaces to print to intent nested values inside a table.

Use this utility method to print the contents of a table. Set depth to 1 if you know the table will be huge because otherwise, it will not fit inside Blizzard's chat frame, and you will lose part of the printed output.


  • @param tbl (table): The table to convert to a long string.
  • @param depth (?number): An optional number specifying the depth of sub-tables to traverse through and append to the long string.
  • @param spaces (?number): An optional number specifying the spaces to print to intent nested values inside a table.
  • @return string: A long string containing the contents of the table.

Returns the contents of a table as a long string, similar to what the PrintTable utility method prints except it does not print it.


  • @param tbl (table): The table to be emptied.

This utility method will empty the contents of the supplied table.


  • @param tbl (table): The table to add all values into from otherTbl.
  • @param otherTbl (table): The other table to copy values from and place into the first table (tbl).
  • @param preserveOldValue (boolean): If true and a key is found in both tables, the value will not be overridden by the one in the other table (otherTbl).

This utility method will add all values from one table to another table. It also works with nested tables with matching keys. Note that this does not create copies of values, but instead, it assigns values to tbl by reference.


  • @param tbl (table): The table to unpack.

This utility method will unpack the table (like using the unpack Lua function) and place it in the table stack for later reuse.


  • @param value (any): Any value to check whether it is of the expected type.
  • @return (boolean): true if the value is a table. Equivalent to type(value) == "table".


  • @param value (any): Any value to check whether it is of the expected type.
  • @return (boolean): true if the value is a number. Equivalent to type(value) == "number".


  • @param value (any): Any value to check whether it is of the expected type.
  • @return (boolean): true if the value is a function. Equivalent to type(value) == "function".


  • @param value (any): Any value to check whether it is of the expected type.
  • @return (boolean): true if the value is a boolean. Equivalent to type(value) == "boolean".


  • @param value (any): Any value to check whether it is of the expected type.
  • @return (boolean): true if the value is a string. Equivalent to type(value) == "string".


  • @param value (any): Any value to check whether it is nil.
  • @return (boolean): true if the value is a nil. Equivalent to value == nil.


  • @param value (any): Any value to check whether it is of the expected object type.
  • @return (boolean): true if the value is an object created by LibMayronObjects.


  • @param value (any): Any value to check whether it is of the expected widget type.
  • @return (boolean): true if the value is a Blizzard widgets, such as a Frame or Button.


  • @param (value): The value to check the type of (can be nil).
  • @param (expectedTypeName): The exact type to check for (can be a custom ObjectType or Widget name).
  • @return (boolean): Returns true if the value type matches the specified type.

A utility method that offers the same functionality as all of the above IsX utility methods but requires the name of the type to check for.


Using FrameWrapper

MayronObjects comes with one other built-in class you can inherit from, called the FrameWrapper, which can be imported from Framework.System.FrameWrapper.

It provides a proxy for calling methods not found on the class to an underlining Blizzard widget (or possibly any table, but it was designed to be used with widgets). For example, if you have not implemented a SetSize method on your class and have assigned a frame to your instance object, then SetSize will be called on the frame instead.

First, make sure you inherit from the FrameWrapper class by either importing it:

local obj = MayronObjects:GetFramework(); local FrameWrapper = obj:Import("Framework.System.FrameWrapper"); ---@class Panel : FrameWrapper local Panel = MyPackage:CreateClass("Panel", FrameWrapper);

Or, by using its namespace:

local obj = MayronObjects:GetFramework(); ---@class Panel : FrameWrapper local Panel = MyPackage:CreateClass("Panel", "Framework.System.FrameWrapper");

14.1 Assigning a Frame

You can assign the frame internally by assigning it to the data.frame private data key:

function Panel:__Construct(data, frame) data.frame = frame or CreateFrame("Frame"); end

Or, by using the SetFrame function provided by the parent FrameWrapper class. You can also retrieve the frame at anytime using GetFrame:

local frame = CreateFrame("Frame"); local panel = Panel(); panel:SetFrame(frame); local returnedFrame = panel:GetFrame(); assert(frame == returnedFrame); -- this will pass

Always use GetFrame when you need to use the frame as an anchor point. If you use panel instead of panel:GetFrame() when using it in a SetPoint call for anchoring, such as anotherFrame:SetPoint("TOPLEFT", panel, "TOPRIGHT"), it will cause a Lua error because Blizzard's code will attempt to use the panel table instead of the frame stored in its private data.

14.2 Calling Widget Methods from your Object

You can then call methods like you usually would for a Blizzard Frame widget, but on an instance of your object. You can also override widget methods to do additional logic and can then call the real methods on the widget if needed:

function Panel:CreateTexture(data, globalName, layer) -- do some extra work before calling it on data.frame return data.frame:CreateTexture(globalName, layer); end -- calls the class method: local texture = panel:CreateTexture(nil, "BACKGROUND"); -- calls the real Blizzard Frame widget: panel:SetSize(100, 100);
back Back to Downloads

© 2021 MayronUI.com, All rights reserved.