Class Features
For all LOOP modules, classes are fundamentally metatables. For this reason, you can use ordinary tables as classes in all LOOP modules. For example, you can create a class that inherits from a module:
Array = oo.class({}, require "table") function Array:length() return #self end a = Array{ "proto", "base", "simple", "multiple", "cached", "scoped" } print(a:length()) --> 6 print(a:sort()) print(a:concat(" ")) --> base cached multiple proto scoped simple
Moreover, classes created with LOOP modules behave as ordinary metatables, so you can index them to get or set metamethods.
However, each module implements classes differently to support additional features, like superclasses or access scopes.
Each module introduces new functions to manipulate these additional class features, like iterating over the superclasses of a class.The downside of these functions is that they only manipulate classes created with that module.
Therefore, if we use functions from module loop.simple
to inspect classes created with module loop.multiple
then things might not work as expected because the each module implements classes differently.
To minimize this problem when using different LOOP modules, all LOOP modules adopts a basic class model that allow us to manipulate them in a general way. This section describes such model, as well as some useful operations over classes and caveats of mixing classes from different models.
General Manipulation
Most of the time it is not a problem to use different LOOP modules in a application because, unlike objects, classes created in one portion of the code usually are not exported to other portions of the code that might use a different module. Libraries generally only export objects but not classes.
Anyhow, all LOOP modules adopts a basic class model where the metatable of the class provides the implementation of all operations that can be performed on that particular class. Therefore, to get the superclass of any class you can do:
local getsuper = getmetatable(class).getsuper if getsuper ~= nil then super = getsuper(class) end
LOOP provides the special module loop.classops
to manipulate classes created with any of its modules using this general model.
So, instead of the code above, you can do:
local oo = require "loop.classops" super = oo.getsuper(class)
Inheritance Interoperability
There are some drawbacks when mixing classes from different LOOP modules in a single class hierarchy.
In particular, classes from modules other than the loop.scoped
module only inherits public members from classes of module loop.scoped
.
Other limitation is that fields of classes from loop.cached
and loop.scoped
modules always override the fields of classes from other modules, no matter their order in the list of super-classes. For example, consider the following code.
local BaseSuper = loop.base.class { attribute = "simple" } local CachedSuper = loop.cached.class{ attribute = "cached" } local MultipClass = loop.multiple.class({}, BaseSuper, CachedSuper) local CachedClass = loop.cached.class ({}, BaseSuper, CachedSuper) local nocache = MultipClass() local cached = CachedClass() print(nocache.attribute) --> simple print(cached.attribute) --> cached
The nocache
object does not use a cache of fields, so it looks up the class hierarchy to find the value of field attribute
and finds it in the BaseSuper
class.
However, the loop.cached
uses a cache of inherited fields that only contains fields from cached classes (i.e. loop.cached
and loop.scoped
modules) therefore it finds the value of field attribute
provided by class CachedSuper
even though it is after the BaseSuper
class in the list of super-classes of CachedClass
.
Super-Class Access
LOOP modules provide functions for introspection of the class hierarchy, allowing the application to get the super-class of a particular class. However, the access to super-class in method implementations can be cumbersome in some situations. One main problem is that a class method is usually a plain Lua function and therefore is not bounded to a particular class. This way, there is no simple way to determine the class of a method in order to figure out its super-class and the inherited method implementation. The most straightforward solution is to place super-classes in local variables and access the inherited methods using the class stored in that variable, like in the example of the following code.
local Square = oo.class() function Square:draw() self.canvas:setcolor(self.color) self.canvas:rect(self.xpos - self.width /2, self.ypos - self.height/2, self.xpos + self.width /2, self.ypos + self.height/2) end local Button = oo.class({}, Square) function Button:draw() Square.draw(self) -- calling inherited method self.canvas:setcolor(self.labelcolor) self.canvas:setfontsize(self.labelsize) local tw, th = self.canvas:textdim(self.label) self.canvas:text(self.xpos - tw/2, self.ypos - th/2, self.label ) end
The first line of method Button:draw()
calls the method Square:draw()
over the object instance using the value stored in upvalue Square
to retrieve the implementation of operation draw
available in class Square
.
However, this explicit reference to the super-class Square
may be troublesome in the scenario of maintaining the code in face of changes on the class hierarchy.
To avoid such problem the acquisition of the super-class can be done through the Button
class stored in variable Button
, like in the code below.
local Button = oo.class({}, Square) function Button:draw() oo.superclass(Button).draw(self) -- calling inherited method self.canvas:setcolor(self.labelcolor) self.canvas:setfontsize(self.labelsize) local tw, th = self.canvas:textdim(self.label) self.canvas:text(self.xpos - tw/2, self.ypos - th/2, self.label ) end
Hierarchical Initialization
By default, LOOP classes work as Lua-like constructors, which are functions that receives a table containing named values to be used to create the new object, like in the example below:
Date = oo.class() function Date:__tostring() return string.format("%02d/%02d/%d", self.day, self.month, self.year) end print(Date{day=30, month=5, year=2013}) --> 30/05/2013
However, we can use the __new
metamethod to implement other styles of constructors.
For example, the class can create an instance from a sequence of argument values instead of the values from a table, like in the example below:
Date = oo.class() function Date:__new(day, month, year) local obj = { day = day, month = month, year = year, } return oo.rawnew(self, obj) end function Date:__tostring() return string.format("%02d/%02d/%d", self.day, self.month, self.year) end print(Date(30, 5, 2013)) --> 30/05/2013
The __new
metamethod is a very basic mechanism and might not be easy to use in complex scenarios like the initialization of a class hierarchy.
In such case, the class being instatiated might inherit from some classes that define define their own __new
metamethod and also from others that do not.
So the implementor of the subclass must know which __new
must be called and which not, and in the proper order.
To make this easy, LOOP provides module loop.hierarchy
with implementations of the __new
designed to work with class hierarchies.
Such implementations use the introspection mechanisms provided by LOOP (see General Manipulation) to inspect the class hierarchy and invoke a special initialization metamethod of each class of the hierarchy in order to initialize the instance properly.
Two implementations are provided.
Function mutator
is designed to work as Lua-like constructors.
This function basically iterates through the class hierarchy from the superclasses down to the actual class, calling the method __init
of each class that provides such method.
This way, each class that requires the initialization of an attribute must provide the method __init
that initializes the attribute correctly.
For example, we could rewrite the example presented in here using this feature as illustrated below:
oo = require "loop.multiple" hierarchy = require "loop.hierarchy" Object = oo.class{ __new = hierarchy.mutator } Contained = oo.class({}, Object) function Contained:__init() assert(self.name, "no name for object") assert(self.container, "no container for object") self.container:add(self.name, self) end Container = oo.class({}, Object) function Container:__init() self.members = self.members or {} end function Container:add(name, object) self.members[name] = object end function Container:search(path) local container, newpath = string.match(path, "(.-)/(.+)$") if container then container = self.members[container] if container and container.search then return container:search(newpath) end else return self.members[path] end end ContainedContainer = oo.class({}, Contained, Container) . . . Root = Container{} Folder = ContainedContainer{ container = Root, name = "my_folder", } File = Contained{ container = Folder, name = "my_file.txt", data = "Hello, I'm a file" } print(Root:search("my_folder/my_file.txt").data) --> Hello, I'm a file
The other implementation provided is function creator
, which is designed to resemble class constructors of Java or C++.
This function basically creates an empty instance of the class and then calls the method __init
of this instance if such method exists.
The __init
method receives all the parameters provided to the constructor.
In this case only a single __init
method is called.
So each class should implement a __init
method that receives the constructor parameters for that class.
Moreover, it must also call the __init
function of each one of its superclasses providing the parameters expected by each superclass constructor, as illustrated in the example below:
oo = require "loop.multiple" hierarchy = require "loop.hierarchy" Object = oo.class{ __new = hierarchy.creator } Contained = oo.class({}, Object) function Contained:__init(container, name) assert(name, "no name for object") assert(container, "no container for object") container:add(name, self) self.name = name self.container = container end Container = oo.class({}, Object) function Container:__init() self.members = {} end function Container:add(name, object) self.members[name] = object end function Container:search(path) local container, newpath = string.match(path, "(.-)/(.+)$") if container then container = self.members[container] if container and container.search then return container:search(newpath) end else return self.members[path] end end ContainedContainer = oo.class({}, Contained, Container) function ContainedContainer:__init(container, name) Contained.__init(self, container, name) Container.__init(self) end . . . Root = Container() Folder = ContainedContainer(Root, "my_folder") File = Contained(Folder, "my_file.txt") File.data = "Hello, I'm a file" print(Root:search("my_folder/my_file.txt").data) --> Hello, I'm a file