Chapter 28. Classes

Table of Contents

1. Overview
2. Summary of Class syntax
2.1. Attributes
2.2. Children
2.3. Methods
2.4. Text Classes
2.5. Placement
3. Constraints in class definitions
3.1. Creating objects to be shared
3.2. Creating unique objects for each instance
4. Creating objects from script
4.1. Reference views by name
5. A Prototype-based language
5.1. Instance-First Development
5.2. The Instance Substitution Principal
5.3. Comparison of LZX to other languages

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.

1. Overview

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.

2. Summary of Class syntax

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>

2.1. Attributes

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.

2.2. Children

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>

Example 28.7. Using attributes and class children together

<canvas height="40">
  <class name="myframe" extends="view"1>
    <attribute name="bgcolor" value="red"/>
    <view x="5" y="5" width="${parent.width-10}" 
          height="${parent.height-10}" 
          bgcolor="blue"/>
  </class>
  
  <myframe width="40" height="40"/>2
</canvas>

1

The extends is optional here. The default base class is LzView.

2

Create an instance of myframe.

2.3. Methods

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.

2.4. Text Classes

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>

2.5. Placement

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.

[Warning] Warning

Known bug: Values for the defaultplacement attribute must be double quoted.

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>

3. Constraints in class definitions

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.

3.1. Creating objects to be shared

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.

3.2. Creating unique objects for each instance

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.

4. Creating objects from script

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:

  1. 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}); 
    

  2. 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;

4.1. Reference views by name

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> 

5. A Prototype-based language

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>

5.1. Instance-First Development

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.

5.1.1. Global variables

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.

5.2. The Instance Substitution Principal

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.

5.3. Comparison of LZX to other languages

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.