BigTwisty's Base Class API!!!
…for all your OOPy goodness…
This class structure can provide you with solid, protected interfaces for all your API classes.
pastebin link: http://pastebin.com/EMDVh4ub
Description:
- Provides a clean, size optimized interface to instances of custom classes.
- Provides basic debugging information to users of classes
- Provides configurable protection for your interface
Supported interface:
- Instance table items Because instances of your class are really just modified tables, they support basic table items by default. Unless the class has been locked or a class interface has already been defined by that name, creating table items works just like normal.
- Members Just like table items, but with type validation!
- Methods Just like table functions, but cannot be overwritten by the user.
- Properties Just like members but with extra behind-the-scenes functionality
- Metamethods All Lua supported metamethods except __index, __newindex and __metatable
Spoiler
Lets create a basic event class for the purpose of capturing event data to send to other objects. It should have a way to read the event id and parameters, and a way for the destination function to pass back whether the event has been handled.Code:
function Event(...)
local args = {...}
-- automatically pull an event if no arguments are passed
if #args == 0 then
args = {os.pullEvent())
end
local self = {
getId = function()
return args[1]
end
getParam = function(i)
return args[i+1]
end
getParamCount = function
return #args - 1
end
handled = false
getString = function()
return table.concat(args,",")
end
}
return self
end
Usage:
e = nil
repeat
e = Event()
print("Id:",e.getId()," ParamCount:",e.getParamCount())
print(e.getString())
until e.getId()=="char" and e.getParam(1)=="q"
This is a typical class that protects its data in a closure. The problem is that all of these functions can be overwritten by the user of your API! Not only that, but what if they set event.handled to "Fo-Shizzle"! Hmmm… Enter BigTwisty's Base Class API!!!
Now let's look at the same class, written using the Base Class API:
Code:
os.loadAPI("btClass")--<link>https://www.dropbox.com/s/lo00wg78voq3uyx/btClass.lua</link>
-- ignores params set to ""
local function eventCompare(e1, e2)
if e1.id ~= e2.id then return false end
for i=1,math.min(e1.n, e2.n) do
if e1[i] ~= "" and
e2[i] ~= "" and
e1[i] ~= e2[i] then
return false end
end
return true
end
function Event(...)
local _args = {...}
if #_args == 0 then _args = {os.pullEvent()} end
local self = btClass.new()
self._newProperty = { "id", get = _args[1] }
self._newProperty = { "n", get = #_args - 1 }
self._newMember = { "handled", false }
self._newMethod = {
"_getIndex",
function(i) return _args[i+1] end,
locked = true }
self._newMetamethod = { "__tostring", function() return table.concat(_args, ",") end }
self._newMetamethod = { "__eq", eventCompare }
self._lock()
return self
end
Usage:
e = nil
eQuit = Event("char", "q")
repeat
e = Event()
print("Id:",e.id," ParamCount:",e.n)
print(e)
until e == eQuit
print(e[1])
e[55]=3
Notice how the user accesses the items as you would expect. Unlike with the previous method, your users can read any of the data, but can only change e.handled. If they try to assign anything but a boolean to e.hanlded, they will get useful debugging error data letting them know exactly how the pooch was screwed.
Note: The _getIndex method is a special case method that provides a function to return numerically indexed values directly from your class instance, if there has been no numeric key created for what is requested. If that method is locked, users cannot create new numeric keys for that class.
Note 2: If you want another class to inherit this one, you can still override the members, methods and properties, but the typical Joe would not be able to simply overwrite them with regular table items.
Detailed instructions:
Spoiler
Creating a new instance:
instance = class.new()
Members:
Spoiler
Features:- Lock to the type of the initial value
- Respond just like table items but are type validated when writing to
instance._newMember = {
"memberName",
<initial value>,
[locked = boolean] }
Example:
instance = class.new()
instance._newMember = { "mem", 3 }
print(instance.mem) --> 3
instance.mem = 10
print(instance.mem) --> 10
instance.mem = "I'm gonna die!" --> Error: Expected number, got string
Methods:
Spoiler
Features:- Respond like function members of the instance table
- Cannot be overwritten accidentally
- Can be obtionally configured to send a private table or variable (self) for instance size optimization
function(self, ...) -- if using self
function(...) -- if not using self
instance._newMethod = {
"functionName",
function,
[self = table or value],
[locked = bool] }
Example 1 (not using self):
instance = class.new()
instance._newMethod = { "multiply", multFunc }
instance.multiply(6) --> 12
instance.multiply = 2 --> Error: Attempted to write to a defined method
Example 2 (using self):
function multFunc(self, val)
print((self.multiplier) * val)
end
instance = class.new()
instance._newMember = { "multiplier", 1 }
instance._newMethod = { "multiply", multFunc, self=instance }
instance.mult(6) --> 6
instance.multiplier = 3
instance.mult(6) --> 18
Properties:
Spoiler
Features:- Responds like a variable member of the instance table
- Provides additional behind-the-scenes functionality when reading from or writing to
- Cannot be overwritten accidentally
- Can be configured as read/write, read-only or write-only
- Can be configured to always return a specific value when read from (no get function needed)
- Can be configured to provide a "self" object to get and set functions
instance._newProperty = {
"name",
[get = function or value],
[set = function],
[self = table or value],
[locked = boolean] }
Example:
local value = 3
function setVal(v)
value = v
end
function getVal()
return math.abs(value)
endfunction getReadOnly(self)
return "Value of abs: "..tostring(self.abs)
end
instance = class.new()instance._newProperty = {
"abs",
get = getVal,
set = setVal }
print(instance.abs) --> 3
instance.abs = -55
print(instance.abs) --> 55instance._newProperty = {
"ro",
get = getReadOnly,
self = instance }
print(instance.ro) --> Value of abs: 55
instance._newProperty = {
"ro",
get = "ReadOnlyResult" }print(instance.ro) --> ReadOnlyResult
instance.ro = 3 --> Error: Attempted to write to a read-only propertyinstance._newProperty = { "wo", set = setVal }
instance.wo = -20
print(instance.abs) --> 20
print(instance.wo) --> Error: Attempted to read from a write-only property
Metamethods:
Spoiler
Description:- Because the class object relies on metatable magic, the metatable is locked down.
- Inserting metamethods is supported through the base class interface
instance._newMetamethod = {
"metamethod name",
function }
Example:
function mystring(self)
return self.x
end
instance = class.new()
instance._newMember = { "x", "It worked!" }
instance._newMetamethod = { "__tostring", mystring }print(instance) --> It worked!
instance._newMetamethod = { "__asdf", printString } --> Error: Unsupported metamethod: __asdf
Lastly, please do not turn this thread into an argument on the principles of object oriented programming. If you think encapsulation and protection of your members isn't important, I will not argue the point with you. If you have constructive criticism about this implementation, however, I beg you to share it! I'm always excited to see new ways to do things…