Table of Contents
This chapter begins with a concise summary of basic features of our class system, and concludes with an essay about how LZX allows you to develop efficiently.
This chapter explains the essential concepts of object oriented
programming in OpenLaszlo applications, and the <class>
tag in particular.
Classes are at the heart of LZX programming. Depending on your background, you may consult any of several discussions in the OpenLaszlo documentation to learn how to use classes effectively in your applications.
To get a feel for how to use classes in OpenLaszlo applications, you may want to start with the Class tutorial. That's the best place to start learning, especially if you've never done any object oriented ("OO") programming.
The chapter you're now reading contains a more concise exposition of Laszlo classes and explains LZX in relation to other OOP systems. In particular, this chapter assumes that you've mastered the concepts covered in the tutorial. In this chapter we'll go beyond some of the mechanics of how to create and extend classes and explore how the design of LZX supports rapid application development.
Chapter 33, Extending Classes is for more experienced developers. It explains advanced techniques.
In its simplest form, a class can be used as a macro, where a complex structure can be reduced to a single tag. The following example defines a class square, and instantiates it twice.
Example 28.1. Defining a new tag
<canvas height="20"> <class name="square" width="20" height="20" bgcolor="red"/> <square/> <square x="30"/> </canvas>
This program is equivalent to the following program, but is shorter and easier to maintain.
Example 28.2. Without the class tag
<canvas height="20"> <view width="20" height="20" bgcolor="red"/> <view x="30" width="20" height="20" bgcolor="red"/> </canvas>
In addition, the class definition in the first program can be moved
to a separate file, through the use of the <library>
and <include>
tags:
Example 28.3. Defining a tag in a library file
<canvas height="20"> <include href="square.lzx"/> <square/> <square x="30"/> </canvas>
<library> <class name="square" width="20" height="20" bgcolor="red"/> </library>
The attributes of the class are the default attributes for each instance. The instance can override these attributes by supplying its own attributes:
Example 28.4. Overriding a class attribute
<canvas height="20"> <include href="square.lzx"/> <square/> <square x="30" bgcolor="green"/> </canvas>
A class can introduce a new attribute with the <attribute>
element:
Example 28.5. Defining an attribute
<canvas height="20"> <class name="square" width="${this.size}" height="${this.size}" bgcolor="red"> <attribute name="size" value="20"/> </class> <square/> <square size="15" x="30"/> <square size="10" x="50"/> </canvas>
For more information about attributes and class definitions, see
the <attribute>
tag and the Class tutorial.
If a class definition contains children, these children are copied for each instance:
Example 28.6. Class children
<canvas height="20"> <class name="myframe" width="20" height="20" bgcolor="red"> <view x="2" y="2" width="16" height="16" bgcolor="blue"/> </class> <myframe/> <myframe x="30"/> </canvas>
Methods defined within classes are available to all instances of
the class, and the over-ridden superclass methods can be called with
the super
keyword.
Unlike many other OO systems, Laszlo allows you to override class methods in an instance.
The <text>
and <inputtext>
tags are unique among the built in
classes in that they can hold textual content:
Example 28.8. Built-in text classes
<canvas height="50" layout="y"> <inputtext>plain text</inputtext> <text><i>styled</i> text</text> </canvas>
The <text>
may contain character text,
as well as certain XHTML markup tags (Chapter 21, Text Views ). The
<inputtext>
tag may contain character
text.
Classes that extend the <text>
and
<inputtext>
classes may contain XHTML and
plain text, respectively. The text content is available as the
text
attribute of such a class.
Example 28.9. Extending the built-in text classes
<canvas height="50" layout="y"> <class name="mytext" extends="text"/> <class name="myinputtext" extends="inputtext"/> <myinputtext>plain text</myinputtext> <mytext><i>styled</i> text</mytext> </canvas>
A tag may also contain text if it defines an attribute named
text
with a value of text
(for
plain text) or html
(for XHTML text), or if it extends a
class that does so. This technique may be used to define a tag that
contains text but does not extend <text>
or <inputtext>
.
Example 28.10. Defining new text classes
<canvas height="50" layout="y"> <class name="mytext"> <attribute name="text" type="html"/> <text text="${parent.text}"/> </class> <class name="myinputtext"> <attribute name="text" type="html"/> <inputtext text="${parent.text}"/> </class> <mytext><i>styled</i> text</mytext> <myinputtext>plain text</myinputtext> </canvas>
The internal structure of a class is generally not visible to its hierarchical children. By default, instances which appear inside a class are made children of the top level instance of the class. This is generally not desirable for container classes. e.g.:
Example 28.11. Default placement of children in a class
<canvas height="50">
<class name="myframe" extends="view">
<attribute name="bgcolor" value="red"/>
<view x="5" y="5" width="${parent.width-10}"
height="${parent.height-10}"
bgcolor="#FFFFCC" />
</class>
<!-- Create an instance of myframe with text inside it-->
<myframe width="220" height="20">
<text>This is some text</text>
</myframe>
</canvas>
This behavior can be changed using the
node.defaultplacement
attribute or the
LzNode.determinePlacement()
method. Using
defaultplacement
is simple — this is a
class attribute that identifies by name the subview where a child
should be attached. In this example, the interior view is named and
the defaultplacement attribute is given that name.
Example 28.12. Using placement within a class
<canvas height="50"> <class name="myframe" extends="view"> <attribute name="bgcolor" value="red"/> <attribute name="defaultplacement" value="'insideview'"/> <view x="5" y="5" width="${parent.width-10}" name='insideview' height="${parent.height-10}" bgcolor="#FFFFCC"/> </class> <!-- Create an instance of myframe with text inside it--> <myframe width="220" height="50"> <text>This is some text</text> </myframe> </canvas>
Constraints are a general mechanism for relating the value of one attribute to that of another, as explained in Chapter 27, Constraints.
Take care when using constraints in class definitions. Depending on the syntax you use to specify the constraint, you will either cause the creation of:
a single entity to be shared among all instances of the class, or
a unique entity for each instance of the class.
If, in a class definition, you say any of:
value="[]" value="new Array()" value="{}" value="new Object()"
you will get a single array or object that is shared by all instances.
If, in a class definition, you say any of:
value="${[]}" value="${new Array()}" value="${{}}" value="${new Object()}"
each instance will get its own array or object.
Note that
${}
is equivalent to $always{}
, but since the expression inside has no dependencies, it is also equivalent to
$once{}
. It's therefore perhaps more "honest" (and also marginally more efficient), to use the $once{}
syntax.
This will also improve readability.
LZX is designed to be a declarative language, so instantiating views from script is not the recommended method for creating Laszlo applications. Of course, from time to time you need to create views on-the-fly -- that's why it is supported -- so you need to know how to pass pointers to views in LZX. There's a couple of ways you could do that in this scenario:
The "right" way. In the PopupWindow
class declare an attribute that
will be a pointer to some view:
Example 28.13. Defining a pointer in a class definition
<class name="PopupWindow" extends="windowpanel" width="160" height="160"> <attribute name="viewToAffect" type="expression"/> —
Then when you instantiate that class, pass a pointer to the view into its constructor function:
Example 28.14. passing a pointer in constructor function
var c = new PopupWindow(canvas, {title:'popupbutton', height:200, width:200, viewToAffect: RootWindow.winbutton});
Then you could reference the view that was passed in from anywhere
in the PopupWindow
class:
classroot.viewToAffect.setAttribute('text', newText);
Now if you had just instantiated the RootWindow
class, you could pass the variable b
into the constructor function for the PopupWindow:
Example 28.15. passing a parameter to a constructor function
var b = new window(canvas, {height:80,width:80}); var c = new PopupWindow(canvas, {title:'popupbutton', height:200, width:200, viewToAffect: b});
The quick and easy way: You could always store a pointer as a global variable, e.g.
canvas.targetWindow = new window(canvas, {height:80,width:80});
or
canvas.targetWindow = RootWindow.winbutton;
You can also give a view a name no matter how you instantiate it (whether you use the constructor function or the tag), and reference it as follows:
Example 28.16. accessing views by name
<canvas debug="true"> <script> var foo = new LzView(canvas, {width: 50, height: 50, x: 50, y: 100, bgcolor: 0, name: "myview"}); </script> <button>Make window red <handler name="onclick"> var nameOfView = "myview"; canvas[nameOfView].setAttribute('bgcolor', 0xff0000); </handler> </button> </canvas>
LZX is a prototype-based language: any attribute that can be attached to a class definition, can be attached to an instance of that class instead. This is handy in UI programming, where there are a number of objects with one-off behaviors. It is also handy in prototyping and incremental program development, where it creates the possibility for a novel kind of refactoring.
The following two XML documents are complete LZX applications. Each defines a view named myview
, that contains a method named f()
. Evaluating myview.f()
in either application will result in 100
.
Example 28.17. A trivial method
<canvas> <view id="myview"> <method name="f">return 100;</method> </view> </canvas>
Example 28.18. A trivial method in a class
<canvas> <class name="myclass"> <method name="f">return 100;</method> </view> <myclass id="myview"/> </canvas>
The equivalence between the two programs above supports a development strategy we call instance-first development. In instance-first development, you implement functionality for a single instance, and then "refactor" the instance into a class that supports multiple instances.
One instance-first development technique is to use global variables. It is often easier to develop and debug a program that stores state in global variables than it is to attach variables to a state, session, or user object. This avoids threading the state object through all the API calls, and it makes it easier to find and manipulate the state during interactive debugging (Chapter 50, Debugging).
Eventually you can refactor the globals into members of a state object, but this refactoring is relatively easy, especially if you have written the program with the knowledge that this would eventually happen.
This technique is cheaper than maintaining the state threading and writing the extra access during all the other refactoring of the program (as extra parameter to each method that is added or refactored). This is a special case of the principle that if you are going to refactor a program anyway, you can optimize the order in which the refactorings are applied, and where within the development pipeline they occur.
In the global variable approach to instance-first development, the entire program is an instance. Prototype-based programming supports another approach, that doesn’t rely on global variables. With prototype-based programming, you can add functionality to and interact with a single instance of an object, refactoring its members and functionality into a class when another instance with the same or similar functionality is called for. This avoids a pitfall of programming, premature abstraction, where behavior is generalized too early and has to be rewritten at the framework level, where it’s difficult to reason and expensive to debug, rather than at the level of the specific instance. It’s easier to generalize from two examples than from one.
In a language without prototypes, you can apply this strategy for instance-driven development by implementing "singleton" classes. Prototypes simply cut out the middleman, reducing the line count, the abstraction, and the indirection that is necessary during development. This is particularly useful during user interface programming, where there may be a number of one-off visual objects. Using a singleton class for each of these can increase the size of a program (and therefore the development time) substantially, but you want the freedom to rapidly refactor these into classes. Prototypes in a language that also supports classes lets you seamlessly move between these representations.
Instance-first development is similar to test-driven development, but is orthogonal to it. (Instance-first development can be used independently from test-driven development, or to complement it.) In test-driven development, you incrementaly add test-specific functionality to encompass a growing test suite. In instance-first development, you add instance-specific functionality to an instance and then generalizes from it. Both are cases of implementing specific cases first and then generalizing from them.
In LZX, classes are user-defined XML tags that have functionality equivalent to class definitions in non-XML languages. However, LZX already has methods and attributes (similar to fields or properties in other languages) on instances definitions, so class definitions look like instance definitions.
In understanding the semantics of LZX class definitions, you may find the following principle useful:
Instance substitution principal: An instance of a class can be replaced by the definition of the instance, without changing the program semantics.
The two programs at the beginning of this entry illustrate the instance substitution principle.
The instance substitution principle can be applied at the level of semantics, or at the level of syntax. At the level of semantics, it means that a member can equivalently be attached either to a class or its instance. At the level of syntax, it means that the means of defining a class member and an instance member are syntactically parallel.
Many prototype-based languages don't obey the instance substitution principle, either because they don't have classes, or because class and instance definitions are not parallel. (Typically there's not a declarative means for defining an instance member.) JavaScript versions 1.0 through 1.5 (the versions in browsers) is also a prototype-based language, but lacks classes as a first-class syntactic entity, and lacks the hierarchical syntax that Java, C++, and LZX use to define class members. JavaScript 2.0, JScript.NET, and Python have a class definition syntax, but don’t use the same syntax to define instance members. For example, contrast the following two Python programs, which parallel the LZX programs above.
myobject = object() myobject.f = lambda f: 100
class MyClass(object): def f(): return 100 myobject = MyClass()
The syntactic version of the instance substitution principle makes a class look like a function or a macro. Class, function, and macro definitions are all mechanisms for abstracting program structure so that it can be reused.
Some languages hide this, because they're missing mechanisms to the specific or general case. For example, the instance substitution principle only makes sense in a language with both prototypes and classes; the derivation of a function definition from a sequence of statements is muddier in a language such as C++ or Java that is missing anonymous functions to represent the intermediate step.
Copyright © 2002-2007 Laszlo Systems, Inc. All Rights Reserved. Unauthorized use, duplication or distribution is strictly prohibited. This is the proprietary information of Laszlo Systems, Inc. Use is subject to license terms.