Difference between revisions of "MiddleClass"
(→Main Features) |
(→Advanced Features) |
||
Line 130: | Line 130: | ||
Game_Play.lua | Game_Play.lua | ||
</source> | </source> | ||
+ | |||
=Advanced Features= | =Advanced Features= |
Revision as of 00:43, 19 February 2010
Contents
This is an object-oriented library for LUA.
If you are familiar with Object Orientation in other languages (C++, Java, Ruby ... ) then you will probably find this library very easy to use.
The code is reasonably commented, so you might want to employ some time in reading through it, in order to gain insight of Lua's amazing flexibility.
MiddleClass was developed as a key part of the PÄSSION lib.
MiddleClass has an optional module called MindState, which adds stateful capacities to objects.
Questions
Please refer to the forum post if you have any questions/issues/bugfixes.
Main Features
This library provides:
- A Root class called Object
- Classes can be subclassed(single inheritance)
- Instances are created with Class:new, and initialized with Class:initialize
- There's a super facility that enables calling methods on the superclass'. This is especially useful on constructors (super.initialize)
- The function subclassOf(Class1, Class2) returns true if Class2 is a subclass of Class1, and false otherwise
- The function instanceOf(Class1, inst) returns true if inst is an instance of class1, and false otherwise
- You can declare class methods and attributes
Example
require 'MiddleClass.lua'
Person = class('Person') --this is the same as class('A', Object) or Object:subclass('A')
function Person:initialize(name)
self.name = name
end
function Person:speak()
print('Hi, I am ' .. self.name ..'.')
end
AgedPerson = class('AgedPerson', Person) -- or Person:subclass('AgedPerson')
AgedPerson.ADULT_AGE = 18 --this is a class variable
function AgedPerson:initialize(name, age)
super.initialize(self, name) -- this calls the parent's constructor (Person.initialize) on self
self.age = age
end
function AgedPerson:speak()
super.speak(self) -- prints "Hi, I am xx."
if(self.age < AgedPerson.ADULT_AGE) then --accessing a class variable from an instance method
print('I am underaged.')
else
print('I am an adult.')
end
end
local p1 = AgedPerson:new('Billy the Kid', 13) -- this is equivalent to AgedPerson('Billy the Kid', 13) - the :new part is implicit
local p2 = AgedPerson:new('Luke Skywalker', 21)
p1:speak()
p2:speak()
--[[ output:
Hi, I'm Billy the Kid.
I am underaged.
Hi, I'm Luke Skywalker.
I am an adult.
]]
Source Code
The latest version can be obtained from the google code project page.
- Here's a link to the latest raw file
- And here you have a syntax highlighted version
If you are interested in actually understanding the code, you might want to start with this short guide. Once you understand this basic structure, you can start "digging"
-- difficulty
<...> -- 2
function Object.new() <...> end -- 7
function Object.subclass() <...> end -- 8 most complicated
function Object.includes() <...> end -- 5
<...> -- 6
function subclassOf(superClass, subClass) <...> end -- 3
function instanceOf(class, instance)<...> end -- 4
function class(name, baseClass) <...> end -- 1 easiest
I have numbered the pieces in ascending difficulty - class is the easiest to understand and Object.subclass the most difficult one. Good luck!
Naming conventions
What follows is a set of recommendations used for naming objects with MiddleClass. These are completely optional, but will be used in all the modules directly dependent on MiddleClass (such as MindState or PÄSSION)
Classes and packages
- Package names should be lowercased camelCased: package
- Class names & mixin names should begin with Uppercase, and use camelCase notation: MyClass, MyMixin
- One exception to this rule is when classes are declared inside packages in that case, they can be declared as follows:
MyClass = class('package.MyClass')
- Another exception is for internal classes (classed declared inside classes)
MyClass = class('package.MyClass')
MyClass.InternalClass = class('package.MyClass.InternalClass')
Attributes, instances and constants
- Attributes begin with lowercase, and use camelCase: MyClass.attributeOne, MyClass.attributeTwo
- An underscore can precede the initial lowercase if the attribute is supposed to be private: MyClass._privateAttribute
- Variables pointing to instances of classes should start with lowercase and be camelCased: myInstance = MyClass:new()
- Constants should be ALL_UPPERCASED, and use underscores for word separations: MyClass.MY_CONSTANT
Methods
- Methods should begin with lowercase, and use camelCase: MyClass.myMethod
- When possible, methods should use explicit verbs: getX() instead of x()
- Instance methods should be declared using the colons, so they have an implicit 'self' parameter:
function MyClass:setX = class('package.MyClass')
- Class methods should use dots, and an explicit 'class' parameter, so they are easily identifiable:
File names
- Folders containing a package should be called the same way as the package itself: myPackage should be stored under a folder called also myPackage
- If a file defines a class named MyClass, then the file should be called MyClass.lua
- Files defining mixins should also be called after the mixins they define: MyMixin will be defined in MyMixin.lua
- If a class is so big it needs to be split on several files, precede all the files defining this class with the class name, followed by an underscore and an explanation of what the file defines:
Game.lua
Game_MainMenu.lua
Game_OptionsMenu.lua
Game_Play.lua
Advanced Features
- There's a very rudimentary support for mixins
- You can specify Lua's metamethods as methods on the classes, and they will be used by the instances. In other words, if you define the method C.__tostring, then all instances of class C will be "stringizables", the Lua way.
- MiddleClass has now an standarized way to create getter and setter methods (a.k.a. mutator methods).
Mixins
Mixins can be used for sharing methods between classes, without requiring them to inherit from the same father.
HasWings = { -- HasWings is a module, not a class. It can be "included" into classes
fly = function ()
print('flap flap flap I am a ' .. self.class.name)
end
}
Animal = class('Animal')
Insect = class('Insect', Animal) -- or Animal:subclass('Insect')
Worm = class('Worm', Insect) -- worms don't have wings
Bee = class('Bee', Insect)
Bee:includes(HasWings) --Bees have wings. This adds fly() to Bee
Mammal = class('Mammal', Animal)
Fox = class('Fox', Mammal) -- foxes don't have wings, but are mammals
Bat = class('Bat', Mammal)
Bat:includes(HasWings) --Bats have wings, too.
local bee = Bee() -- or Bee:new()
local bat = Bat() -- or Bat:new()
bee:fly()
bat:fly()
--[[ output:
flap flap flap I am a Bee
flap flap flap I am a Bat
]]
Mixins can provide a special function called 'included'. This function will be invoked when the mixin is included on a class, allowing the programmer to do actions. Any additional parameters passed to class:include will be passed to mixin:included()
DrinksCoffee = {
included = function(class, coffeeTime) {
print(class.name ' drinks coffee at ' .. coffeeTime)
class.coffeeTime = coffeeTime
}
}
-- This is another valid way of declaring functions on a mixin.
-- Note that we are using the : operator, so there's an implicit self parameter
function DrinksCoffee:drink(drinkTime)
if(drinkTime~=self.class.coffeeTime) then
print(self.name .. ': It is not the time to drink coffee!')
else
print(self.name .. ': Mmm I love coffee at ' .. drinkTime)
end
end
EnglishMan = class('EnglishMan')
EnglishMan:includes(DrinksCoffee, 5)
function EnglishMan:initialize(name) self.name = name end
Spaniard = class('Spaniard')
Spaniard:includes(DrinksCoffee, 6)
function Spaniard:initialize(name) self.name = name end
tom = EnglishMan:new('tom')
juan = Spaniard:new('juan')
tom:drink(5)
juan:drink(5)
juan:drink(6)
--[[ output:
EnglishMan drinks coffee at 5
Spaniard drinks coffee at 6
tom: Mmm I love coffee at 5
juan: It is not the time to drink coffee!
juan: Mmm I love coffee at 6
]]
Metamethods
Metamethods can do funky stuff like allowing additions in our instances. Let's make an example with __tostring
Point = class('Point')
function Point:initialize(x,y)
self.x = x
self.y = y
end
function Point:__tostring()
return 'Point: [' .. tostring(self.x) .. ', ' .. tostring(self.y) .. ']'
end
p1 = Point(100, 200)
p2 = Point(35, -10)
print(p1)
print(p2)
--[[ output:
Point: [100, 200]
Point: [35, -10]
]]
Getters & Setters
MiddleClass streamlines the creation of getter/setter methods. I mean these:
Enemy = class('Enemy')
function Enemy:getStatus() -- getter for status
return self.status or 'idle'
end
function Enemy:setStatus(status) -- setter for status
return self.status = status
end
These constructions are quite useful in object-oriented programming. For example, your EnemyTroll class might want to redefine setStatus(status) function, so it has a chance to resurrect when the new status is dying (there's actually a better way of doing this, with MindState, but bare with me on this example)
Now you can accomplish the same with just two lines:
Enemy = class('Enemy')
Enemy:getter('status', 'idle') -- default value
Enemy:setter('status')
Notice that getter takes a second, optional parameter, in order to specify default values. On this example, 'iddle' will be returned, if self.status is nil. If no default parameter is needed, just use Enemy:getter('status') - that will return nil when the status is nil.
There's a compressed, still shorter way of specifying both the getter and setter at the same time:
Enemy = class('Enemy')
Enemy:getterSetter('status', 'idle') -- getter & setter, with default value
In all cases, the status can be get with getStatus and set with setStatus:
enemy = Enemy:new()
print(enemy:getState()) -- 'idle'
enemy:setState('thinking')
print(enemy:getState()) -- 'thinking'
The reason for using getters and setters instead of directly accessing the properties (doing enemy.status) is that subclasses of your class might want to 'override' the getters or setters in order to implement different behaviours. For example: if you are creating a Zombie class (subclass of Enemy) you might want to override getStatus so the default value is 'braaaains' instead of 'idle'.
This means that you should not use getters/setters in all your classes; just in the ones that are subceptible of having subclasses.
Private
There are several ways you can make private parameters with MiddleClass.
Underscoring
The simplest one is just to precede your attributes with underscores. This is actually written on the Lua 5.1 reference, section 2.1, "Lexical conversions", as a way to say "this is here, but please don't use it".
require('MiddleClass.lua')
MyClass = class('MyClass')
function MyClass:initialize()
self._internalStuff = 1
self.publicStuff = 2
end
However, this isn't really making the properties "hidden".
Private class attributes
In general, the way of "really" getting hidden functions or variables consists on using Lua's scoping rules.
The simplest way of using this is creating each of your classes on separate files, and then declaring any private variable or functions as local, on the "root" scope of the file.
Example:
-- File 'MyClass2.lua'
require('MiddleClass.lua')
MyClass2 = class('MyClass2')
local internalClassCounter = 0
function MyClass2:initialize()
internalClassCounter = internalClassCounter + 1
self.publicStuff = 2
end
function MyClass2:getCount()
return(internalClassCounter)
end
The scope of local declarations on a lua file is the file itself. If you declare something "local" in one file it is not available on others, even if they "require" that file.
-- File 'main.lua'
require('MyClass2.lua')
-- Try to change internal member...
internalClassCounter = 4 -- Attempt to modify the internalClassCounter variable
print(MyClass2:getCount()) -- prints "0"
Let me explain what happens here. The internalClassCounter = 4 line is, in reality, creating a new global variable called internalClassCounter, and assigning it 4. The "really internal" one is "out of reach" on main.lua (unless someone does really tricky stuff with the environments). So getCount() works as expected.
Private Methods
It is also possible to declare private methods. The trick here is not to "include" them on the class definition. On the following example, we will not declare it on Class3:secretMethod; instead we'll create a local function. Since we're not using the : operator any more, we have to make the "self" parameter explicit. Also, since we have to make it local, we have to deviate from the "usual" way of declaring Lua functions (the "usual" way of declaring functions makes them global):
-- File 'MyClass3.lua'
require('MiddleClass.lua')
MyClass3 = class('MyClass3')
local secretMethod = function(self) -- notice the 'local' at the beginning, the = function and explicit self parameter
return( 'My name is ' .. self.name .. ' and I have a secret.' )
end
function MyClass3:initialize(name)
self.name = name
end
function MyClass3:shout()
print( secretMethod(self) .. ' You will never know it!' )
end
-- File 'Main.lua'
require('MyClass3.lua')
peter = MyClass3:new('peter')
peter:shout() -- My name is peter and I have a secret. You will never know it!
print(secretMethod(peter)) -- throws an error - secretMethod is nil here.
This technique also allows the creation of private class methods. In MiddleClass, there's really no difference between class methods and instance methods; the difference comes from what you pass to their 'self' parameter. So if you invoke secretMethod like this: secretMethod(MyClass3) it will be a class method.
A slightly more efficient way of creating a class method would be getting rid of the 'self' parameter and use MyClass3 directly on the method's body:
MyClass3 = class('MyClass3')
local secretClassMethod = function() -- self parameter out
return( 'My name is ' .. MyClass3.name .. ' and I have a secret.' ) -- use MyClass3 directly.
end
Note that this alternative is only recommended for private class methods. Public class methods should follow the convention of adding one explicit 'class' parameter:
MyClass3 = class('MyClass3')
function MyClass3.classMethod(class)
return( 'Being a public class named ' .. class.name .. ' is not a bad thing.' )
end
This gives a bit more of flexibility when overriding public class methods on subclasses.
Private Instance attributes
Instance attributes are a little bit trickier to implement, since we only have one scope to "hide stuff in", and it has to cope with multiple instances.
One way to do this is using one private class variable as a 'stash'. If you use one table instead of just a number, you can and hide there all the private information you may need. One problem with this approach is that you need to come out with a 'key' per 'instance'.
Fortunately this is a very simple thing to do, since in lua you can use nearly any type of object as a key - So you can use the instances themselves as keys. In other words, we use 'self' as a key.
One problem with this approach is that instances might not be liberated by the garbage collector once they are not used any more (since there's a reference to them on the 'stash' keys). In order to avoid this, we can make the 'stash' a weak table.
On the following example, the name attribute is public, but age and gender are private.
Our "stash" is a variable called "private" (private isn't a reserved word in Lua, otherwise we would have had to call it 'privateStuff').
-By the way, the following example also shows how you can do "read-only-ish attributes": you make them private, and make getters for them, but not setters.
-- File 'MyClass4.lua'
require('MiddleClass.lua')
MyClass4 = class('MyClass4')
local private = setmetatable({}, {__mode = "k"}) -- weak table storing all private attributes
function MyClass4:initialize(name, age, gender)
self.name = name
private[self] = {
age = age,
gender = gender
}
end
function MyClass4:getName() -- shorter equivalent: MyClass4:getter('name')
return self.name
end
function MyClass4:getAge()
return private[self].age
end
function MyClass4:getGender()
return private[self].gender
end
-- File 'main.lua'
require('MyClass4.lua')
stewie = MyClass4:new('stewie', 2, 'male')
print(stewie:getName()) -- stewie
stewie.name = 'ann'
print(stewie:getName()) -- ann
print(stewie:getAge()) -- 2
stewie.age = 14 -- this isn't really modifying the age... it is creating a new public attribute called 'age'
-- the internal age is still unaltered
print(stewie:getAge()) -- 2
-- the newly created external age is also available.
print(stewie.age) -- 14
-- same goes with gender:
print(stewie:getGender()) -- 'male'
stewie.gender = 'female'
print(stewie:getGender()) -- 'male'
print(stewie.gender) -- 'female'
Private members on the same file
There's also a way of creating private members that other classes/methods on the same file can't access, if you ever had the need.
Just create an artificial scope with do ... end, and declare private members as 'local' inside that block. Only the methods inside that block will have access to them:
-- File 'MyClass3.lua'
require('MiddleClass.lua')
MyClass3 = class('MyClass3')
function MyClass3:initialize(name)
self.name = name
end
do
local secretMethod = function(self) -- notice the explicit self parameter here.
return( 'My name is ' .. self.name .. ' and I have a secret.' )
end
function MyClass3:shout()
print( secretMethod(self) .. ' You will never know it!' )
end
end
-- functions outside the do-end will not 'see' secretMethod, but they will see MyClass3.shout (because they see MyClass3)