2.2 Instantiating classes

Instantiating classes

In Chapter 1, I discussed how to define classes and I assumed objects could be created out of these classes. Now I'll take a closer look at how to create those objects and discuss what strings are attached.

Constructors and destructors

Often it's helpful to do something whenever a class is created or destroyed. A typical task would be to bring the environment to a proper state or clean up after the object. Often the constructor sets properties of the current object based on certain conditions, while the destructor removes things the object created in order to leave the system as it was when the object was created.

Most object-oriented languages support this in one way or another. Visual C++, for example, has a concept called constructors and destructors. C++ simply uses a naming convention that allows you to attach code to a class that runs whenever the object is created or destroyed. The constructor uses the same name as the class itself.

Visual FoxPro also supports constructors and destructors, but they are implemented a bit differently. FoxPro simply fires events that occur whenever the object is instantiated or dies. The constructor event is called Init() and the destructor event is called Destroy(). However, there are some differences between the events FoxPro fires, and real constructors and destructors as they are used in C++. This is especially true for the event-firing sequence, which I'll discuss later.

All FoxPro base classes support the constructor and destructor events. However, some classes feature other events in addition to the regular constructor. Form classes, for instance, have a Load event that fires even before the Init. This particular event allows changing settings or opening tables that are needed by member objects. Doing so in the Init would be too late, because member objects that are linked to data would have already failed to instantiate at that point.

FoxPro's handling of the constructors has a couple advantages over the C++ way, one of which is subclassing. Whenever you create a subclass in C++, the class gets new name, and therefore the constructor has a new name. This means that the original constructor doesn't fire anymore unless it's called explicitly. This makes programming by exception difficult. In Visual FoxPro, the name of the constructor is always Init. This allows you to apply regular rules of inheritance and simplifies the class handling.

Event-firing sequence for Init() and Destroy()

Let's have a look at the firing sequence of constructors and destructors. When there is only one object, the sequence is easy. An object gets instantiated and the Init event fires right away. Then the object is alive and other events may fire. Whenever the object dies, the destructor fires and that's it.

However, this gets more complex when you create composite objects, especially in container scenarios. Imagine you have a form object that contains a command button. If you create this object, the button's constructor fires first, followed by the one belonging to the form. This is pretty weird if you think about it. It would be like installing a bathtub on the second floor when building a house, and adding the framework later. However, in an object world it makes sense. Keep in mind that objects should be black boxes that can live in virtually every container. For this reason it wouldn't make sense to have the container object in place and ready to go whenever the child constructs, because the child object wouldn't have any specific knowledge about the container and therefore couldn't do anything with it. However, an object can have knowledge about member objects that it hosts. For this reason, it makes a lot of sense to have all the member objects already in place when the constructor of the container object fires.

The opposite is true for the destructor. The Destroy method fires for the container first, while the objects contained in it still exist.

Handling parameters in objects

The Init method can also be used to pass parameters to an object. You could do this with an (L)PARAMETERS statement in the first line of the Init method. Parameters are passed right away, when creating the object (as you'll see a bit later). From this point on, parameters are handled as usual. This includes the fact that the visibility of parameters is either private or local. For this reason, parameters go out of scope after the Init has fired. For this reason, parameters are often saved to custom properties of the object to make them available throughout the object.

When using composite objects, parameters can only be passed to the container objects. If you need to pass parameters to a member object (which usually indicates bad design), you can receive the parameters in the container object's Init and pass them on to the child object. The following example demonstrates this:

DEFINE CLASS MyContainer AS Container

* oCustom is the child object

ADD OBJECT oCustom AS MyCustom

FUNCTION Init

LPARAMETERS Parameter1
* We fire the init of oCustom again
THIS.oCustom.Init(Parameter1)

ENDFUNC

ENDDEFINE

DEFINE CLASS MyCustom AS Custom

FUNCTION Init

LPARAMETERS Parameter1

IF Parameters() < 1

** No parameters yet...

RETURN .T.

ENDIF

** This time we received parameters

WAIT WINDOW Parameter1

ENDFUNC

ENDDEFINE

When you instantiate the MyContainer class (which hosts one instance of the MyCustom class) and pass a parameter, the Init of the custom class fires first, followed by the Init of the container. (I'll show you the syntax shortly.) Of course, when the Init of the custom class fires, it doesn't receive any parameters, but since it checks for the parameters in the first line, it deals with that and simply does nothing. Then the Init of the container fires and it receives one parameter. It then passes this parameter on to the Init of MyCustom by firing the method one more time. This works, because the custom object now exists, which demonstrates how handy the firing sequence of the constructors is.

The constructor's return value

Not only can the Init method handle parameters, but it also can handle return values. However, the programmer does not have access to the return value because Visual FoxPro itself reacts to this value.

Whenever the Init method returns .T. (which is the default), Visual FoxPro assumes that everything worked out well and finishes creating the object. If the return value is .F., FoxPro assumes something went wrong and destroys the object right away, so it basically looks like the object was never created.

Very often I see programs where people try to decide in the constructor whether or not the object should be created. If it shouldn't, they simply return .F. from that method. However, this is not a smart way to go for reasons of performance and resources. Keep in mind that the object and all its members have already been constructed whenever the form's Init fires. For this reason, the Init should only return .F. when something went wrong while constructing the object. Here's an example that shows when to use the constructor to decide whether an object should be created:

FUNCTION Init
IF NOT VarType(Customer.Name) = "C"
* The data we need isn't there
RETURN .F.
ENDIF
ENDFUNC

In this example, the object checks for some data it depends on. If it isn't there, the object won't work, and for this reason we don't even instantiate it. Here is another example that shows a scenario where I wouldn't use the constructor to decide whether the object can be instantiated:

FUNCTION Init
IF oApp.oUser.nLevel < 1
* The user doesn't have access to this object
RETURN .F.
ENDIF
ENDFUNC

In this case the object checks whether the user has rights to use it. There might be some good reasons to do this, but if at all possible, I would do this before instantiating the object. Most likely, I would use a special security-manager object or something similar. Checking for access rights at the end of the instantiation process is only a waste of resources.

CreateObject()

Using the CreateObject() function is one of the simpler ways to create an object. You simply pass a class name and it returns a reference to the created object, as in the following example:

oForm = CreateObject("Form")

In this case, I'm creating an object based on the FoxPro base class Form. Of course I can use CreateObject() to create objects based on classes I created myself. Whenever I do that, I have to make sure that the class definition is in scope. To do so, I can use the SET CLASSLIB (if I use visual class libraries) or the SET PROCEDURE command (if I use PRG files). The class definition would also be in scope if the class was defined in the PRG that actually does the CreateObject(), or if the PRG that has the class definition is in the call stack. For the scope issue, the same rules apply as for finding function definitions. Here's an example that instantiates a user-defined class:

SET CLASSLIB TO MyLib.vcx ADDITIVE
oForm = CreateObject("MyForm")

NewObject()

Visual FoxPro 6.0 introduces a new function to instantiate objects called NewObject(). It basically does the same job as CreateObject(), but it allows you to specify the class library as well. Here is the same example as before using NewObject():

oForm = NewObject("MyForm","MyLib.vcx")

Using NewObject(), Visual FoxPro makes sure the class definition is in scope. You can specify visual class libraries (VCX) as well as PRG files. Also, NewObject() allows you to specify a compiled application that hosts the class definition. This allows you to create class libraries that are distributed in compiled versions rather than in source code.

This all may not look like a big deal, but it really is. Making sure the class library is in scope for CreateObject() becomes complex in real-life applications. The easiest way usually is to make sure all the class libraries are in focus in the startup program. However, this does not work once you start using third-party tools or self-contained components. In this case, the component needs to check if the definition is in scope. If not, it has to set it and eventually release it to leave the environment as it was (remember that objects should never change the surrounding environment).

NewObject() takes care of all these issues for you, but of course it has to do that every single time a new object is created, and this takes time. So you should still use CreateObject() for performance reasons whenever possible. If you know that a library is already set, or if the current object is the only one using it and therefore you can set it without having to check whether it was set before there is no reason to use NewObject(). CreateObject() can be used just as easily, and it's a lot faster.

.AddObject() and .NewObject()

The CreateObject() and NewObject() functions you've seen so far create new, independent objects. However, you'll also face situations where you have to add objects to an existing container (such as adding a new button or textbox to a form). To do this, you can use the .AddObject() and .NewObject() methods. Every container class (container, form, page, and so on) features these methods.

.AddObject() and .NewObject() work similarly to the functions introduced above, but they require another parameter, the object name, since they don't return a new object reference but add it to the container object. If we had a form object called oForm, we could simply add a button (on the fly) like so:

oForm.AddObject("cmdButton","CommandButton")
oForm.cmdButton.Visible = .T.

oForm.NewObject("cmdButton2","MyCommandButton","MyButtons.vcx")
oForm.cmdButton2.Visible = .T.

As you might have already guessed, .NewObject() allows you to specify the library just as the NewObject() function, while .AddObject() requires that the class definition is already in scope. Because the behavior of these methods is similar to the equivalent functions, the same performance issues apply. Also, note that objects added to objects are by default invisible, so you have to explicitly make them visible.

Passing parameters to objects

As I mentioned earlier, objects can receive parameters. All four functions and methods used to create objects support passing parameters. All the parameters are passed to the methods and functions as additional parameters. You simply need to know how many parameters each of the methods and functions support, and you can pass your parameters in addition to that. Let's have a look at how this works in each case:

oForm = CreateObject("MyForm","Parameter1")

The CreateObject() function is pretty simple and supports only one internal parameter, so parameter 2 would already be passed on to the instantiated object, where it would end up as parameter 1.

oForm = NewObject("MyForm","forms.vcx",,"Parameter1")

NewObject() supports two more parameters (the class library and the hosting application). In the example above, I didn't use a library from a compiled application, so I simply left this parameter out.

oForm.AddObject("cmdButton","MyCommandButton","Parameter1")
oForm.NewObject("cmdButton","MyCommandButton","MyButtons.vcx",,"Parameter1")

These methods work according to their function counterparts, but they require one more parameter in front (the name of the object being added), so the rest of the parameters are shifted back one position.

SCATTER NAME

There are more commands that create objects, one of which is SCATTER NAME . This command takes the current record of a table or cursor and creates an object that has one property per table field. It also puts the current field values into the properties. Here is an example:

USE Customer
LOCAL loData
SCATTER NAME loData MEMO

The resulting object doesn't have any methods not even constructors or destructors. Also, you can't specify a class that is used to create this object because everything is constructed on the fly. For this reason, it would not make sense to provide methods, events, or constructors and destructors.

If property values have been changed, that object's new property values can be saved back to the table using the GATHER NAME command.

Using data objects has many advantages over using regular data. You can access multiple records at a time using multiple data objects. Also, data can be handed over to other objects, even across data sessions and through OLE connections. However, this would fill a whole chapter and this book is not about handling data. For this reason, I'll leave this discussion for somebody else.

Besides the SCATTER NAME command, there are some others that create objects in a similar fashion. BROWSE NAME is one of them. Another one is DEFINE WINDOW NAME . As you can see, they all share the added NAME clause, which is an indicator that objects are created. Most of these commands are included for backward compatibility, and you should use the newer counterparts instead.



Advanced Object Oriented Programming with Visual FoxPro 6. 0
Advanced Object Oriented Programming with Visual FoxPro 6.0
ISBN: 0965509389
EAN: 2147483647
Year: 1998
Pages: 113
Authors: Markus Egger

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net