lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


At my work, I needed OOP in my Lua. I'm used to Ruby, so I created a simple object/class framework that mimics it. In it, Classes are objects that may have their own methods. To see the (complex, but common-sense) inheritance rules, check out http://phrogz.net/ AnarkSamples/AKClassHierarchy.png

Features:
* Every instance of an object inherits from a common root (AKObject.prototype).
* Supports classes with (single) inheritance.
* Instances of classes share properties/methods from a common space (theClass.prototype.*). * Shared properties for instances are in a separate space from methods on the class itself. * Every instance knows who its class is, and every class knows its superclass. * Classes are objects too - they inherit from AKClass.prototype, which inherits from AKObject.prototype * Every object (including classes) can easily have their own string representation. * Classes may define an 'initialize' function, which is executed on instances when they are created. * Because I use only __index tables, I assume it has a faster runtime than solutions using __index functions.

Limitations
* No public/private methods/properties. Everything is public.
* Does not currently support any sort of 'super' mechanism for magically calling methods from an ancestor with the same name.
* No 'mix-ins' or other forms of multi-inheritance.

I've included the code below, for feedback. At the end is a tiny testcase showing how to use the framework to create classes and instances.

-- ************************************************************************ *********

AKObject = {
	-- AKObject.new is used as the new() for each class,
	-- and also to create AKClass itself
	new = function( inAncestor, ... )
		local theInstance = { }
		setmetatable( theInstance, inAncestor.instancemeta )
		
		-- If the class has an initialize() method defined,
		-- run it with the instance as self, passing along arguments
		-- supplied to new()
		if type( inAncestor.initialize ) == 'function' then
			inAncestor.initialize( theInstance, unpack( arg ) )
		end

		return theInstance
	end
}

-- Root properties/methods shared by all instances (including Classes)
AKObject.prototype = { class = AKObject }

-- In case the class doesn't supply its own tostring() for instances
function AKObject.prototype:tostring( )
	return "<"..tostring( self.class ).." instance>"
end

-- The __tostring metamethod always uses a 'tostring' property
function AKObject:tostring( ) return self:tostring( ) end

-- Class instances inherit from the AKObject.prototype
AKObject.instancemeta = { __index = AKObject.prototype, __tostring = AKObject.tostring }

-- If prototype tables used their own tostring method,
-- they'd attempt to use the method meant for instances
AKObject.protometa    = { __index = AKObject.prototype }

-- AKObject is, itself, an object.
-- Also give it a nice string representation
setmetatable( AKObject, {
	__index = AKObject.prototype,
	__tostring = function( ) return "AKObject" end
} )

-- Classes are objects too; they inherit methods from the AKObject prototype
AKClass = AKObject:new( )
AKClass.prototype = { new=AKObject.new }

-- Class objects (that inherit from AKObject) go through AKClass's prototype AKObject.classmeta = { __index = AKClass.prototype, __tostring = AKObject.tostring }

-- Class objects inherit through AKClass.prototype, and thence to AKObject
setmetatable( AKClass.prototype, AKObject.instancemeta )

function AKClass:tostring( )
	return "AKClass"
end

-- Classes use a special constructor that allows superclasses to be specified
-- The superclass defaults to AKObject
-- Instances from AKClass:new() inherit from inSuperClass's prototype
-- Instances of a class's new() inherit from that class's prototype
function AKClass:new( inSuperClass )
	-- If no superclass was supplied, this class inherits from AKObject
	if not inSuperClass then inSuperClass = AKObject end

	local theClass = {
		class      = self,           -- The class of a class is AKClass
		superclass = inSuperClass,   -- A property pointing to the owner
		prototype  = { }             -- Instances inherit from this
	}
	theClass.prototype.class = theClass

	setmetatable( theClass.prototype, inSuperClass.protometa )
	setmetatable( theClass, inSuperClass.classmeta )

	-- Instances of this class inherit from its prototype
	-- Subclass objects inherit from the class itself
theClass.instancemeta = { __index = theClass.prototype, __tostring = AKObject.tostring } theClass.classmeta = { __index = theClass, __tostring = AKObject.tostring }
	
	-- If prototype tables used their own tostring method,
	-- they'd attempt to use the method meant for instances
	theClass.protometa    = { __index = theClass.prototype }

	return theClass
end

-- ************************************************************************ *********

Rectangle = AKClass:new( )
function Rectangle:tostring( ) return "Rectangle" end
function Rectangle:initialize( inW, inH )
	self.w = inW or 0
	self.h = inH or 0
end
function Rectangle.prototype:area( )
	return self.w * self.h
end
function Rectangle.prototype:tostring( )
return string.format( '<%s w=%d h=%d>', tostring( self.class ), self.w, self.h )
end

Square = AKClass:new( Rectangle )
function Square:tostring( ) return "Square" end
function Square:initialize( inLength )
	Rectangle.initialize( self, inLength, inLength )
	-- Using self.class.superclass.initialize would break inheritance
end

UnitSquare = AKClass:new( Square )
function UnitSquare:initialize( )
	Square.initialize( self, 1 )
	-- Using self.class.superclass.initialize would break inheritance
end

print( AKObject )
print( AKClass )
print( Rectangle )
print( Square )
print( UnitSquare )

assert( tostring( AKObject )  == 'AKObject' )
assert( tostring( AKClass )    == 'AKClass' )
assert( tostring( Rectangle ) == 'Rectangle' )
assert( tostring( Square )    == 'Square' )
assert( tostring( UnitSquare )    == 'Square' )

r = Rectangle:new( 10, 20 )
print( r )
assert( tostring( r ) == '<Rectangle w=10 h=20>' )
assert( r:area( ) == 200 )

s = Square:new( 5 )
print( s )
assert( tostring( s ) == '<Square w=5 h=5>' )
assert( s:area( ) == 25 )

u = UnitSquare:new( 999 )
print( u )
assert( tostring( u ) == '<Square w=1 h=1>' )
assert( u:area( ) == 1 )

DeadClass = AKClass:new( )
print( DeadClass )
assert( DeadClass.superclass == AKObject )
assert( tostring( DeadClass ) == '<AKClass instance>' )