Difference between revisions of "MiddleClass"

m (Undo revision 2115 by Muckypup (Talk))
(Source Code)
Line 65: Line 65:
  
 
==Source Code==
 
==Source Code==
The latest version can be obtained from the [http://code.google.com/p/lua-middleclass/ google code project page].
+
The latest version can be obtained from its [http://github.com/kikito/middleclass github page].
* Here's a link to the latest [http://lua-middleclass.googlecode.com/svn/trunk/MiddleClass.lua raw file]
+
* Here's a link to the latest [http://github.com/kikito/middleclass/raw/master/MiddleClass.lua raw file]
* And here you have a [http://code.google.com/p/lua-middleclass/source/browse/trunk/MiddleClass.lua syntax highlighted version]
+
* And here you have a [http://github.com/kikito/middleclass/blob/master/MiddleClass.lua 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"
 
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"

Revision as of 23:10, 21 April 2010

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 its github page.

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
  • Private attributes should be preceded with an underscore: _myPrivateAttribute

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(x)
  self.x = x
end
  • Class methods should use dots, and an explicit 'theClass' parameter, so they are easily identifiable
function MyClass.classMethod(theClass)
  print('I am the ' .. theClass.name .. ' class. I am awesome')
end
  • Private methods should be preceded with an underscore: _myPrivateMethod

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(theClass)
  return( 'Being a public class named ' .. theClass.name .. ' is not a bad thing.' )
end

This gives a bit more of flexibility when overriding public class methods on subclasses.

Finally, a subtle point regarding recursive private methods. If you need to create a private method that calls himself, you will need to declare the variable first, and then (on the next line) initialize it with the function value. Otherwise the variable will not be available when the function is created

MyClass3 = class('MyClass3')

local _secretRecursiveMethod -- variable declared here
_secretRecursiveMethod= function(self, n) -- and initialized here
  if(n<=0) then
    print( 'Last recursion')
  else
    print ( 'Recursion level ' .. n )
    _secretRecursiveMethod(self, n-1)
  end
end

MyClass3:recurseOver(n)
  _secretRecursiveMethod(self, n)
end

----

m = MyClass3:new()
m:recurseOver(5)

--[[ Output:
  Recursion level 5
  Recursion level 4
  Recursion level 3
  Recursion level 2
  Recursion level 1
  Last recursion 
]]

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 'secret stash' in the following example will be called _private.

-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.name()) -- 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)

See Also