LOOPE - Lingo Object Oriented Programming Environment by Irv Kalb

Previous Chapter

Table of Contents

Next Chapter

 

Section 1 - Parent Scripts and Objects

Chapter 3 - Simple Object

 

A simple example

To help explain what an object really is, we'll start with a piece of code which is not object oriented, and then look as the same functionality written in an object oriented way as a parent script. That way we can discuss the similarities and differences.

Assume that we want to write a piece of code to simulate a bank account. The code would use two global variables; gPassword which would be used to store a password for the account, and gBalance which would be used to keep track of the amount of money currently in the account. For the functionality of the bank account, we would want to be able to do the following actions:

- Create the account.

- Deposit money into the account.

- Withdraw money from it.

- Check the balance.

Here is some very straightforward code which will accomplish these tasks.

-- BankAccount script with globals
global gPassword
global gBalance


on NewAccount password, initialBalance
  gPassword = password
  gBalance = initialBalance
end

on Deposit amountToDeposit
  gBalance = gBalance + amountToDeposit
end

on Withdraw amountToWithdraw, password
  if password <> gPassword then
    alert("Wrong password!")
    return
  end
if
  if amountToWithDraw > gBalance then
    alert("You don't have that much cash in your account!")
    return
  end
if
  gBalance = gBalance - amountToWithdraw
end


on GetBalance password
  if password <> gPassword then
    alert("Wrong password!")
    return
  end
if
  return gBalance
end

The code here should be very clear to you. Deposit is used to add cash to the account. It simply increments gBalance by the amount of the deposit. Withdraw is used to remove cash from the account. However, it needs to be a bit selective. First it checks to see if the password passed in matches the initial password. It also checks to ensure that the amount requested to be withdrawn is available in the account. If it passes these two conditions, then gBalance is decremented by the amount of the withdrawal. GetBalance checks the password and then returns gBalance.

Now, let's try to expand this program. Imagine if we wanted to create two bank accounts - one for Bill and another for Sue. The first thought might be to add more global variable. To be clear, we could replace gPassword with gBillsPassword and add gSuesPassword. We would also have to change gBalance to gBillsBalance and add gSuesBalance. Then we might modify the routines so you pass in a name or symbol to identify whose account to act on. Then each routine would have a case statement so it could check the password for the person making the request and affect the associated balance. For example, the Withdraw handler might look like this:

global gBillsPassword
global gSuesBalance
global gSuesPassword
global gBillsBalance

on Withdraw amountToWithdraw, password, who
  case who of
    "Bill":
      if password <> gBillsPassword then
        alert("Wrong password!")
        return
      end
if
      if amountToWithDraw > gBillsBalance then
        alert("You don't have that much cash in your account!")
        return
      end
if
      gBillsBalance = gBillsBalance - amountToWithdraw
      
    "Sue":
      if password <> gSuesPassword then
        alert("Wrong password!")
        return
      end
if
      if amountToWithDraw > gSuesBalance then
        alert("You don't have that much cash in your account!")
        return
      end
if
      gSuesBalance = gSuesBalance - amountToWithdraw
  end
case
end

Now imagine that we want to be able to build an arbitrary number of accounts. For each new account we need to add two global variables and modify code to access the proper set of those variables. You can see that the program would get out of hand very quickly. Furthermore, it isn't really dynamic. This approach does not allow us to add a new account on the fly - we must know in advance the names of all people who can create an account.

A different approach would be to create two "parallel" lists, one for passwords and one for balances. For each new account you want to create, you would add a new password to a list of passwords, and add a new balance to a list of balances. Here is the code that would implement such a scheme.

-- BankAccount script with global lists
global glPasswords -- global list of passwords
global glBalances -- global list of balances


on
startMovie
  glPasswords = []
  glBalances = []
end

on NewAccount password, initialBalance
  add(glPasswords, password)
  add(glBalances, initialBalance)
  return
count(glPasswords)
end

on Deposit whichAccountNumber, amountToDeposit
  glBalances[whichAccountNumber] = glBalances[whichAccountNumber] + amountToDeposit
end

on Withdraw whichAccountNumber, amountToWithdraw, password
  if password <> glPasswords[whichAccountNumber] then
    alert("Wrong password!")
    return
  end
if
  if amountToWithDraw > glBalances[whichAccountNumber] then
    alert("You don't have that much cash in your account!")
    return
  end
if
  glBalances[whichAccountNumber] = glBalances[whichAccountNumber] - amountToWithdraw
end

on GetBalance whichAccountNumber, password
  if password <> glPasswords[whichAccountNumber] then
    alert("Wrong password!")
    return
  end
if
  return glBalances[whichAccountNumber]
end

As each bank account is created a new entry is made in both the glPasswords and glBalances lists. This certainly does work, but something seems a little bit odd about this approach. Because all of the passwords are grouped together in one list and all the balances are grouped together in another list, when we want to get the related pieces of information about one account, we have to use a common variable to index into both lists, glPasswords[i] and glBalances[i].

If we look at this information as though it were a spreadsheet, we can see that all the passwords are grouped together vertically and all the balances are grouped vertically.

However, in reality, each bank account is really represented by a row of the same spreadsheet.

Ideally, what we would like to have a way of grouping the related information so that a password and a balance are coupled together into one unit. The question is, "how can we structure the data so that a password and a balance for one account are grouped together?". Not surprisingly, the answer lies in an object.

 

A simple object

Now remember our definition of an object: data, plus code that acts on that data over time. With this definition in mind, we can see that our bank account has two pieces of data, a password and a balance, and as actions to act on that data, we want to allow a user to make deposits, make withdrawals, and to check the balance. This is a very good candidate for being turned into a parent script. Here is the same functionality written as a parent script. You will notice that the code is very similar to the first non-object oriented approach.

-- BankAccount script
property pPassword
property pBalance


on
new me, password, initialBalance
  pPassword = password
  pBalance = initialBalance
  return
me
end

on mDeposit me, amountToDeposit
  pBalance = pBalance + amountToDeposit
end

on mWithdraw me, amountToWithdraw, password
  if password <> pPassword then
    alert("Wrong password!")
    return
  end
if
  if amountToWithDraw > pBalance then
    alert("You don't have that much cash in your account!")
    return
  end
if
  pBalance = pBalance -amountToWithdraw
end

on mGetBalance me, password
  if password <> pPassword then
    alert("Wrong password!")
    return
  end if
  return pBalance
end



To make a script like the above work correctly, you must do two things with the member. First, the script cast member must have a name. The name of this script is "BankAccount". This name is used when you actually create an object from this script (we'll get to that in a just a bit). Second, the type of the script (available through the Property Inspector) must be set to "Parent".

The first part of our definition of an object is: data. If you compare this script to the first non-object oriented script, the first difference you see is that instead of two global variables we have declared two property variables, pPassword and pBalance, to keep track of the data for this object. In the original script, because gPassword and gBalance were global variables, they could be accessed not only by the current script, but also by any script anywhere in the program. We will see that this is potentially a very dangerous thing. In the earlier discussion about scope, we said that the scope of a property variable is limited to the current script - no more, no less. Specifically, that means that any value stored into the property variables pPassword and pBalance are remembered and are available to all the methods of the BankAccount script, but are not accessible anywhere outside of this script.

The second part of our definition of an object is that there is code which acts on the data. This script has four methods to act on the data: new, mDeposit, mWithdraw, and mGetBalance.

The "new" method is a special method which all parent scripts must have. We will get deeply into the details of what this method does in a little bit, but for now, all we need to know is that the "new" method is always the first method called in a parent script. In fact, calling the "new" method of a parent script creates or instantiates an object. In this case, when we actually call the "new" method of the BankAccount parent script, we are creating a BankAccount object.

The "new" method can be written to take parameters, just like any other handler. This gives you a way to supply starting values (also known as initial values) for an object. In the "new" method of the BankAccount parent script, we are passing in two values, an initial value for a password and an initial balance for the account. The code of our BankAccount script's "new" method simply copies these values into the related property variables for storage.

mDeposit does the same work as the Deposit routine in our original script, but it modifies the property variable pBalance instead of gBalance. Similarly, mWithdraw and mGetBalance check against the property variable pPassword instead of the global gPassword.

 

Instantiating (creating) an object

The third part of the definition of an object is time. An object has a lifespan. As I said earlier, a parent script defines an object. However, a parent script by itself does not create an object. The programmer decides when to create an object and when to eliminate or "dispose" of an object.

If you put the BankAccount script into a Director movie and run the movie, you would not create any objects. An object only comes into being when a programmer explicitly makes a call to the "new" method of a parent script. The "new" method of a parent script must be called from somewhere outside of that parent script. Each object that you create from a parent script is often referred to as an instance of that object.

A good analogy is to think of the parent script as a "high-tech" cookie cutter, it provides a shape for a cookie, but it only creates a cookie when you pick it up and press it into some dough. To extend the analogy, our high-tech cookie cutter can also be given some additional details for each cookie that will be stamped out so that all cookies are not exactly alike. The cookie cutter might be able to set a color for the cookie, and a choice of a topping (chocolate, sugar, or sprinkles) to the cookie. That way, although each of the cookies created by our cookie cutter has the same shape, you can use the same cookie cutter to make blue chocolate covered cookies, or pink sugar covered cookies, or purple cookies with sprinkles. Each of these is an instance of the same cookie, but each has a different set of properties.

When you want to create a new bank account object you use a line of code like this.

oBankAccount = new(script "BankAccount", "xyzzy", 400.00)

Or with the new "dot syntax":

oBankAccount = script("BankAccount").new("xyzzy", 400.00)

This statement calls the "new" method of the script "BankAccount", and passes in two parameters: a password whose value is "xyzzy", and an initial balance of four hundred. When this line executes, Director creates an object for us and then calls the "new" method of the given parent script. In our BankAccount's "new" method, we simply assign the initial values to the associated property variables.

The last line of code in the parent script is:

return me

"me" in this line has very important meaning. When you ask Director to create an object by calling "new", Director allocates memory for the object and creates a special type of variable called an object reference. The object reference can be thought of as a handle for, or a pointer to the object. When you create an object, in its "new" method, the object needs to pass back a value which points to the newly created object, that's what "me" is. In effect, it is the address of (or a pointer to) the memory taken up by that object. When the call to "new" returns, that value is then stored into a variable, in this case, oBankAccount. It is important to understand the difference between the object reference and the actual object. The object reference is a variable which contains a pointer to an object. The object is memory which has been allocated to store the data of object.

 

Whenever you want to call any methods of that object, you need to use the object reference which was returned from the "new" method. For example, assume that you create a BankAccount object and store the object reference into a variable called oBankAccount. Then, if you want to make a deposit into that account, you would use the line:

oBankAccount.mDeposit(250.00)

(or in the older style before "dot syntax": mDeposit(oBankAccount, 250.00)

This says to identify the BankAccount object which is pointed to by the object reference variable, oBankAccount, and pass it a parameter of 250.00. On the receiving end of this call, in the mDeposit method, oBankAccount matches up to the parameter "me", and 250.00 matches up to the parameter amountToDeposit. So, inside this method, "me" refers to the instance of the BankAccount object. Notice that the first parameter to all methods of an object is always "me". We will discuss a whole lot more about "me" in a later chapter.

 

Instantiating multiple copies of an object

Earlier, we attempted to show how we could use globals or global lists to create an arbitrary number of bank accounts. Now that we have the BankAccount parent script, let's see how we can use it to create multiple bank accounts. As in the earlier example, let's say that we wanted to create two bank accounts, one for Bill and one for Sue. Here's how we could do this using parent scripts:

oBillsBankAccount = new(script "BankAccount", "donuts", 1000.00)

oSuesBankAccount = new(script "BankAccount", "asparagus", 1200.00)

When these two lines of code are executed, Lingo instantiates two different objects from the same parent script. The first object represents Bill's bank account with a password of "donuts" and an initial value of one thousand. The second object represents Sue's bank account with a password of "asparagus" and an initial value of twelve hundred. There are two copies of the object, each with its own values for the property variables pPassword and pBalance. We have found a way to create a unit, an object, which groups together one password and one balance for each account. If you think back to the spreadsheet view of the data, each object that we define contains the associated data of one row of our spreadsheet.

Now if we wanted to allow for an arbitrary number of bank accounts (each represented by a bank account object), here is what we need. We start with one global list of bank account object references. Then we need a utility handler that creates a new instance of a BankAccount object and adds the object reference to the global list of object references:

-- BankAccount script with global lists
global gloBankAccounts -- global list of BankAccount object references

on
startMovie
  gloBankAccounts = []
end

on NewAccount password, initialBalance
  oNewAccount = new(script "BankAccount", password, initialBalance)
  add(gloBankAccounts, oNewAccount)

  nAccounts = count(gloBankAccounts)
  return nAccounts
end

 

Disposing of an object

The lifespan of an object is entirely up to the programmer as dictated by the needs of the program. Depending on what the object is used for, the object's lifespan may be anywhere from a millisecond to the life of the whole program. A programmer could create an object for temporary storage and minimal computation and dispose of that object only moments later. Or, a programmer could create an object when the movie starts, and make constant references to it throughout the life of the program. Often, a programmer creates an object in response to a user action. Further, different objects may be instantiated to model the data of a program - so the number of objects is related to the amount of data needed.

As we have seen, to create an object, you call the "new" handler, Director creates the object and gives you back an object reference. When you create an object, the object takes up memory. When you no longer need the object, it is a good idea to dispose of the object so that Director can reuse the memory that was taken up by the object. Getting rid of an object (the memory taken up by an object) is actually very simple. Internally, Director keeps track of how many variables point to any given object. This is called a "reference count". The rule is that Director will dispose of an object as soon as there are no object references that point to that object.

This is best understood with an example. First, we'll create a Widget object from the Widget parent script (it really doesn't matter what this object does).

oWidget = new(script "Widget")

So now, we have the object reference variable, oWidget, which points to the Widget object. Director sets the reference count of the Widget object to one (that is, there is one object reference variable which points to this object). Then we have some useful code which uses the methods of the Widget object. Finally, it comes time for us to get rid of the Widget object. We can cause Director to dispose of the Widget object by just doing this:

oWidget = VOID

We simply change the value of oWidget to no longer point to the Widget object. VOID is a good thing to assign the variable to because it effectively "resets" the variable back to no value at all. By setting the object reference variable oWidget to no longer point to the Widget object, Director subtracts one from the reference count of the object which was pointed to by oWidget. The reference count goes down to zero. At that point, Director realizes that there are no more object reference variables that point to the memory taken up by the Widget object, and frees up the memory which it had allocated for that object.

Now, let's make it a little more complicated. We'll create a Widget object, but then we'll make a copy of the oWidget object reference variable:

oWidget = new(script "Widget")

oWidgetAlso = oWidget

The first assignment statement causes Director to allocate memory for the Widget object. The reference count of the Widget object is set to one. As Director executes the second assignment statement, Director increments the reference count of the Widget object to two. That is, there are two object reference variables which point to the same object. Now in order to dispose of the Widget object, we must make sure that all object reference variables which point to the object are cleared out. If we wanted to dispose of this Widget object, we must write:

oWidget = VOID

oWidgetAlso = VOID

When Director executes the first line, the reference count gets decremented to one. Because of this reference count, Director "knows" that there is still a variable somewhere which contains an object reference to the Widget object. When Director executes the second line, the reference count is decremented down to zero and Director recognizes that it can now safely dispose of the memory taken up by the Widget object.

This reference count thing may seem like a tricky thing, but in practice it becomes simple and routine. Just remember that every time you assign an object reference, that object reference must eventually be cleared out. There are some special cases to understand. If you assign an object reference to a local variable the reference count for the object will be incremented by one. When the currently executing handler exits, the local variable is eliminated and the reference count will be decremented automatically. If you place an object reference in a list, that "counts" as a variable pointing to the object. If the list is a local variable, when the currently executing handler ends, the reference count will be decremented also. However, if you assign an object reference to a global variable or into a global list, when you are trying to dispose of that object, you must ensure that you have accounted for all references to the object in these globals or the memory or the object will not be released.

 

Previous Chapter

Table of Contents

Next Chapter