Class Design
© Robert P. Cook 2014
The class design topic is covered in the Lua textbook at professorcook.org; however, the subject is complicated enough that a second example might help with your understanding.  There are software engineering courses devoted to class design principles and patterns.  For the beginner, we concentrate on providing a recipe that has proven useful.

Also, remember our maxim "The best way to write correct code is to copy correct code."  For example, all Java and C# SDK class definitions are available online.  Google "java rectangle" to compare the Java class definition to the one in the book.

This Section illustrates the class concept with the design of a fraction class (integer numerator divided by an integer denominator).  Remember that the goal of a class definition is to extend a language's built-in types (e.g. float, integer, string).  A complementary goal is to enable software reuse by other programmers. Every class implementation has a large (and hidden) intellectual component that is also reused.  In many cases, the thinking component can take much longer to evolve than writing the code that embodies it.

We use a top-down, design approach; that is, start by identifying the methods then choose a data representation.  The goals are to achieve functional completeness, to provide easy-to-use methods to the user, and to be a one-and-done solution.

Functional completeness requires a class definition that encompasses all operations that you might want to perform on a fraction.  For example, omitting a reciprocal method would be a design deficiency.

Classes are well-thought-out, long-term solutions.  Typically, a class definition has many more methods than any example that has been presented so far.  Following our own maxim, we will use the Apache Fraction class in Java as a model.

A class will, at the minimum, have the following components, which we describe in turn.
  1. data definitions,
  2. constructor (new),
  3. constants,
  4. copy constructor (copy),
  5. setters, getters, (set, get)
  6. mutators (modify class data and state),
  7. predicates including isInstance,
  8. parse and tostring methods.
Class Syntax
First, the Lua syntax for class definitions is discussed. A class definition is represented as a table. Both data and methods can be defined as the fields of a table,

In Lua, functions can be treated as values, just like data such as integers or strings. Lua supports a shorthand notation that allows functions to be defined and accessed as table fields. Once defined, the functions can be accessed with the colon-notation e,g, num,dem=frac:getND().

Fraction = {
    numerator = 0,
    denominator = 0,
    getND = function(self) return self.numerator, self.denominator end
} --Fraction

The getND function can also be defined separately from the table as follows:

Fraction = {
    numerator = 0,
    denominator = 0
}  --Fraction
function Fraction.getND(self) return self.numerator, self.denominator end

The even better, cool Lua notation that we recommend is the colon form of a function declaration that hides the "self" parameter on the declaration side and that automatically inserts the "self" argument on the call side. Within the function getND, the fields of  Fraction can be accessed without qualification.

Fraction = {  --<<-- use this syntax
    numerator = 0,
    denominator = 0
}  --Fraction
function Fraction : getND() return self.numerator, self,denominator end

Data and Constructor
Copy the following code into a main.lua file and then execute it.  The code implements, one and only one, Fraction.  Remember that the goal is to allow the user to add new data types to the language.  To achieve that goal, there must be a way to create new copies of what might be thought of as the canonical, or default, definition of a Fraction.  In almost every language, the method name chosen to create class instances is "new", which is referred to as a constructor.

Fraction = {
    numerator = 0,
    denominator = 0
}  --Fraction
print(Fraction.numerator, Fraction.denominator)
OUTPUT
0        0

All class methods will be defined as components of Fraction.  Copy the following code into a main.lua file and then execute it. The modf function from the math library is applied to enforce the constraint that the numerator and denominator are integers.  Furthermore, a minus sign is only allowed for the numerator.

Fraction = {  --prototype, stores data and function definitions
    numerator = 0,
    denominator = 0
}  --Fraction

function Fraction:new(num, den)
 local f = { numerator = math.modf(num),
                  denominator = math.abs( math.modf(den) ) }
  return f
end --new

local frac = Fraction:new(3,4)
print(frac.numerator, frac.denominator)
OUTPUT
3        4

It would be very desirable to be able to invoke the "new" method without having a reference to Fraction (e.g. frac2=frac:new(5,6) ).  However, this generates an error message: "attempt to call method 'new' (a nil value)".  The reason is that the name "new" is defined in the Fraction table but was not included in the "frac" table.  Luckily, Lua provides some "magic" to allow multiple tables to share definitions. Any name (not just functions) that is missing from "frac" will be queried in the Fraction table. Copy the following code into a main.lua file and then execute it.

Fraction = {  --prototype, stores function definitions
    numerator = 0,
    denominator = 0
}  --Fraction

function Fraction:new(num, den)
  local f={numerator = math.modf(num),
           denominator = math.abs( math.modf(den) ) }
  setmetatable(f, {__index = Fraction} )  --refer to Fraction for unknown names
  return f
end --new

local frac = Fraction:new(-3.1,4.8)   --note illegal input will be truncated to 3 4
print(frac.numerator, frac.denominator)

local frac2 = frac:new(6, 7)
print(frac2.numerator, frac2.denominator)
OUTPUT
-3        4
6         7

Examine the Apache Fraction definition (click on the previous link).  It includes a constructor that implements whole-number fractions.  Obviously, this is just for convenience as new(integer, 1) accomplishes the same purpose.  There is rarely a single "right" class design.  Note that the single "new" is functionally complete as the integer option can be programmed with it.

Another option would be to define a default constructor newDefault() that just assigned default values to the numerator and denominator.
Constants
Examine the Apache Fraction definition again (click on the previous link).  Notice that it defines multiple class constants: ZERO, ONE, TWO, MINUS_ONE, ONE_HALF, ONE_THIRD etc.  In most languages, the coding style guide recommends uppercase letters for constant names (Lua style guide).  We just add a couple of constants to the Fraction class as an example. Add the following code into the main.lua file and then execute it.

Fraction.ZERO = Fraction:new(0,1)
Fraction.ONE = Fraction:new(1,1)
Fraction.ONE_HALF = Fraction:new(1,2)

local frac = Fraction.ZERO
print(frac.numerator, frac.denominator)
local frac2 = Fraction.ONE
print(frac2.numerator, frac2.denominator)
local frac3 = frac2.ONE_HALF
print(frac3.numerator, frac3.denominator)
OUTPUT
0        1
1        1
1        2

Notice that the "class constants" are accessible to all fraction variables.  Be careful with constants as their reference in assignment statements does not create a copy!!  All copies refer to the same table so any change would destroy the constant!!!  Use a copy constructor to assign constants to variables that may later be modified.

local frac = Fraction.ZERO
frac.numerator = 3   --bad!! changes all variables with a reference to ZERO

Copy Constructor
The previous examples discussed the "new" constructor method.  A copy constructor duplicates the table of an existing object. Think of it as a cloning robot.  If you want to assign a class constant to a variable that will then be modified, use the copy constructor to get a new copy of the constant.

After cloning, any changes to the new copy have no effect on the original variable. In the following example, frac2 is set to ONE and then modified.  When frac3 is set to ONE, notice that the constant has remained untouched. Copy constructors can be used to duplicate any fraction, not just constants. Add the following code into the main.lua file and then execute it.

function Fraction:copy()
  local u = { }
  for k, v in pairs(self) do u[k] = v end
  setmetatable(u, {__index = Fraction} )
  return u
end --copy

local frac = Fraction.ZERO:copy()
frac.denominator = 6
print(frac.numerator, frac.denominator)
local frac2 = frac.ONE:copy()
frac2.numerator = 3
print(frac2.numerator, frac2.denominator)
local frac3 = frac2.ONE
print(frac3.numerator, frac3.denominator)
OUTPUT
0        6
3        1
1        1
Getters and Setters
The member variables of any fraction (numerator or denominator) can be accessed or modified by using a qualified name. Why define a "set" method when table.field=value will do the same thing? The goal is information hiding.  Defining a setter method hides the format and even the existence of a field.

A getter is a method that returns the values of a class' property, or a derived property.  For the fraction class, a getter would return the numerator, denominator. For the rectangle class, a getter might return the center, or top left, coordinates and a setter might change the coordinates.  For the rectangle class, the area or circumference would be examples of derived properties. The Fraction class also includes a function to return the real-number value (a derived property) of a fraction. Add the following code into the main.lua file and then execute it.

function Fraction:getND()
  return self.numerator, self.denominator
end --getND

function Fraction:setND(num, den)
  self.numerator, self.denominator = num, den
end --setND

function Fraction:getValue()
  return self.numerator / self.denominator
end --getValue

local frac = Fraction:new(3,4)
print(frac.numerator, frac.denominator, frac:getValue())
frac:setND(5,6)
print(frac:getND())
OUTPUT
3        4    0.75
5        6
Mutators
A mutator is any method that modifies a class, such as adding or multiplying fractions.  The design and implementation of mutators typically involves what is termed "domain knowledge" that is specific to the application.  For example, do you remember how to add 3/4 and 5/6 by finding the least common multiple?  Can you reduce a fraction?

The algorithms and choice of data types for use in mutators have been studied since the invention of computing.  In computer science curricula, there is always a data structures and typically an algorithms class.  The following "add" method is only a sampling of the possible fraction methods.

A perennial issue in designing mutators is whether to create a new object to hold the result or to modify the "self" variable.  For example, should the addition of two fractions return a "new" fraction or just add the second fraction to the first?

The code implements "chaining" (see the book), which supports the combination of methods. Without chaining, every operation would require a separate assignment statement.  A chaining function returns "self", which causes its input variable to be passed along to the next function in the chain.

Search online to find the code for additional mutator methods. Add the following code into the main.lua file and then execute it.

function Fraction:add(frac)
  if self.denominator == frac.denominator then
    self.numerator = self.numerator+frac.numerator
  else
    self.numerator = self.numerator*frac.denominator + self.denominator*frac.numerator
    self.denominator = self.denominator*frac.denominator
  end --if
  return self
end --add

local frac2 = frac:new(3,4)
print('3/4 + 5/6 =')
frac2:add(frac)
print(frac2:getND())

frac:add(frac2:new(3,6)):add(frac2:new(2,6))  --chaining
print('5/6 + 3/6 + 2/6 =')
print(frac:getND())
OUTPUT
3/4 + 5/6 =
38      24
5/6 + 3/6 + 2/6 =
10      6

Predicates
A predicate is a Boolean function that answers questions about a class.  Typically, a "compare" method is defined, at the minimum.  The common definition is to return -1 for less than, 0 for equal, and +1 for greater.

Even though "compare" covers all cases, some classes also define an "equals" predicate. Add the following code into the main.lua file and then execute it.

function Fraction:compare(frac)
  local result
  if self.denominator == frac.denominator then
    result=self.numerator-frac.numerator
  else
    result=self.numerator/self.denominator-frac.numerator/frac.denominator
  end --if
  if result < 0 then return -1 end
  if result == 0 then return 0 end
  return 1
end --compare

local frac=Fraction:new(1,3)
print('1/3 compare 1/2')
print(frac:compare(frac.ONE_HALF))
print('1/2 compare 1/3')
print(frac.ONE_HALF:compare(frac))
print('1/3 compare 33333/99999')
print(frac:compare(frac:new(33333,99999)))
OUTPUT
1/3 compare 1/2
-1
1/2 compare 1/3
1
1/3 compare 33333/99999
0

The Lua "type" function only returns the name of basic types such as 'string' or 'table'.  Knowing that a variable is a table does not provide sufficient information about classes.  Thus, an "isInstance" predicate is defined that answers the questions "am I a class instance?" or "am I a Fraction?".  Since Lua does not provide the information, it needs to be stored in the class' prototype as listed next. Modify the class definition and add the test code into the main.lua file and then execute it.

Fraction = {
  class = ',Fraction,class,'  --add this line
}
function Fraction:isInstance(name)
  if self.class == nil then return false end
  local where=string.find(self.class, ',' .. name .. ',', 1, true)
  return where ~= nil
end --isInstance

local frac=Fraction:new(1,3)
print(frac:isInstance('class'))
print(frac:isInstance('Rectangle'))
print(frac:isInstance('Fraction'))
OUTPUT
true
false
true
Parse and tostring
The coding of a class typically proceeds according to the step-wise refinement principle.  First, implement the "new" constructor then the "tostring" method.  The tostring method can then be used to test each of the other methods as they are implemented. It is advisable to retain these tests (termed regression tests) as the code is expanded and as bugs are found and corrected.

We also recommend defining a setFormat method so that users have control over the appearance of output.  If the "format" field is set in the prototype, it would apply to all instances.  However, any instance can override the default by setting its own format. Add the following code into the main.lua file and then execute it.

function Fraction:tostring()
  if self.numerator == 0 then return '0' end
  local q,r = math.modf(self.numerator/self.denominator)
  if r == 0 then return q.."" end
  if q == 0 then return self.numerator..'/'..self.denominator end
  return q..' '.. math.abs(self.numerator-q*self.denominator)..'/'..self.denominator
end --tostring

local frac=Fraction:new(0,3)
print(frac:tostring())
frac=Fraction:new(2,3)
print(frac:tostring())
frac:setND(-14,6)
print(frac:tostring())
frac:setND(-12,6)
print(frac:tostring())
OUTPUT
0
2/3
-2 2/6
-2

Lua has some more "magic" in store because printing is such a frequent occurrence.  By adding a special definition, the ":tostring" can be omitted entirely when printing objects.  The update is included in the linked file Fraction.lua.

The "parse" method is somewhat the opposite of tostring in that it converts a string to the internal data representation of an object. This method is left as an exercise.

Software Reuse
Writing, testing and debugging the code for this tutorial took a number of hours.  For example, the tostring implementation went through a false start or two before evolving to its current form.  There is rarely a valid reason to rediscover all the principles and nuances that compose the design of a class.  We subscribe to the one-and-done philosophy.

As a result, a coder should be able to search the web and find a class implementation for almost any object in any language.  Sadly, this is not the case.  Apparently, programmers like to write the same code over and over.  I practice what I advocate.  I have over 1100 Java modules posted here.

It would be annoying if the use of a Fraction object required copying and pasting its source code. The Lua language includes the syntax to support the definition of classes in separate modules (text files such as Fraction.lua).  In the main.lua program, a "require" function can be executed at any time to retrieve a copy of a class definition, which can then be used to create new instances or to access constants.  Further, all instances can also be used to create new instances and to access class constants.

main.lua
Fraction = require('Fraction')
local frac=Fraction:new(0,3)
print(frac)
frac=Fraction:new(2,3)
print(frac)
frac:setND(-14,6)
print(frac)
frac:setND(-12,6)
print(frac)
print(frac:copy())

To create the Fraction.lua module, the prototype and methods were copied to file Fraction.lua and then a "return Fraction" statement was added to the end of the file.