Saturday, November 29, 2008

ActionScript toString() with Interfaces

ActionScript 3.0 includes many object-oriented features that have an obvious Java-like syntax, but there are some nuances of ActionScript's object-oriented support that are not so Java-like. In this blog entry, I'll look in some detail at the use of ActionScript's toString() method in conjunction with ActionScript interfaces.

The value of the toString() in Java development and debugging is generally accepted and widely understood (see Item #10 in Joshua Bloch's Effective Java, Second Edition, or Item #9 in the First Edition). Similarly, use of toString() in ActionScript can provide similar advantages as discussed in Debugging Basics in Flex 3 (beta).

While an ActionScript toString() looks similar to a Java toString and while use of the toString() can be highly beneficial in working with both Java and ActionScript, there are some important differences to understand. I have previously covered some nuances of the ActionScript toString() implementation in Flex: ObjectUtil.toString() Versus Inherited Object's toString() and When An ActionScript Object Does Not Provide toString(). I'll briefly summarize some points from those two previous blog entries before demonstrating use of ActionScript's toString specifically in conjunction with an ActionScript interface.


Characteristics of the ActionScript Object.toString()

* ActionScript classes that directly extead the Object class (don't extend any other ActionScript class) and provide a toString() implementation do not need to use the override keyword.

* Related to the first point above, ActionScript's Object.toString() is not inherited by descendant ActionScript classes.

* The static utility class mx.utils.ObjectUtil provides a toString() that is highly useful for displaying String representations of ActionScript class instances that do not have their own implemented toString() methods).


The observations summarized above play out when using ActionScript interfaces and implementing those ActionScript interfaces. The remainder of this blog entry will be focused on the relationship of toString() to the ActionScript interface.

ActionScript makes use of the interface concept throughout its standard library. The blog entry ActionScript 3 Class Interface Implementations lists the ActionScript classes that implement interfaces in the Flex 3 API, the AIR 1.0 API, and the Flash API.

The ActionScript interface syntax is pretty straightforward for anyone familiar with the Java interface. The next code listing demonstrates a simple interface in ActionScript and uses the commonly adopted Flex coding convention of having an interface's name begin with "I".

IState.as


package examples
{
/**
* Simple interface example demonstrating use of interfaces in Flex 3.
*
* Methods defined in a Flex interface cannot have any modifiers.
*/
public interface IState
{
/**
* Provide name of the state.
*
* @return Name of the state.
*/
function getStateName():String;

/**
* Provide the state's abbreviation.
*
* @return Abbreviation of the state.
*/
function getStateAbbreviation():String;

/**
* Provide the capital of the state.
*
* @return Capital of the state.
*/
function getStateCapital():String;
}
}



Most Java developers should have very little trouble reading this interface. Like a Java interface, the "public" modifier is not required for the methods defined as part of the interface. In fact, these method modifiers are not even allowed in the ActionScript interface. The interface keyword is the same as in Java and the concept of method signatures without implementations is the same for both languages' interfaces.

ActionScript uses the same implements keyword that Java uses to designate when a class implements an interface. This is demonstrated in the next code listing, for the State class which implements the IState interface.


State.as


package examples
{
/**
* Implementation of IState interface that is mainly intended to demonstrate
* use of interfaces in Flex.
*/
public class State implements IState
{
/** Name of the state. */
private var stateName:String;

/** State's abbreviation. */
private var stateAbbreviation:String;

/** Capital of the state. */
private var stateCapital:String;

/**
* Constructor.
*
* @param newStateName Name of the state.
* @param newStateAbbreviation State two-digit abbreviation.
* @param newStateCapital Capital of the state.
*/
public function State(
newStateName:String,
newStateAbbreviation:String,
newStateCapital:String)
{
this.stateName = newStateName;
this.stateAbbreviation = newStateAbbreviation;
this.stateCapital = newStateCapital;
}

/**
* Provide name of the state.
*
* @return Name of the state.
*/
public function getStateName():String
{
return this.stateName;
}

/**
* Provide the state's abbreviation.
*
* @return Abbreviation of the state.
*/
public function getStateAbbreviation():String
{
return this.stateAbbreviation;
}

/**
* Provide the capital of the state.
*
* @return Capital of the state.
*/
public function getStateCapital():String
{
return this.stateCapital;
}
}
}



To demonstrate behavior of ActionScript interfaces in conjunction with the toString() method, I will use a simple Flex application to invoke an object's toString() implementation directly and to also expose the object's contents via the ObjectUtil.toString() method. The simple Flex application is shown next.


Main.mxml (without use of interface)


<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
width="900" height="900"
applicationComplete="runExample();">
<mx:Script>
<![CDATA[
import mx.utils.ObjectUtil;
import examples.IState;
import examples.State;

/** Set up a state for the interface example. */
public function setUpState():State
{
const state:State = new State("Colorado", "CO", "Denver");
return state;
}

/**
* Display state information.
*
* @param state State whose information is to be displayed.
*/
public function displayState(state:State):void
{
stateNameText.text = state.getStateName();
stateAbbreviationText.text = state.getStateAbbreviation();
stateCapitalText.text = state.getStateCapital();

stateToStringText.text = state.toString();
stateObjectUtilToStringText.text = ObjectUtil.toString(state);
}

/**
* Set descriptive information on this example type. This is a
* convenience function related to demonstration of principles in
* a blog entry, but is not significant to the functionlity of this
* class.
*/
private function setExampleType():void
{
exampleTypeText.text = "No toString() method implemented in class.";
}

/** Run the example of using Flex interfaces. */
public function runExample():void
{
setExampleType();
displayState( setUpState() );
}
]]>
</mx:Script>

<mx:Panel id="mainPanel" title="State Example: Using Flex Interfaces">
<mx:Form id="stateForm">
<mx:FormItem id="stateNameItem" label="State Name">
<mx:Text id="stateNameText" />
</mx:FormItem>
<mx:FormItem id="stateNameAbbreviation" label="State Abbreviation">
<mx:Text id="stateAbbreviationText" />
</mx:FormItem>
<mx:FormItem id="stateCapital" label="State Capital">
<mx:Text id="stateCapitalText" />
</mx:FormItem>
<mx:FormItem id="exampleType" label="Example Description">
<mx:TextArea id="exampleTypeText" height="75" />
</mx:FormItem>
<mx:FormItem id="objectToString" label="toString()">
<mx:TextArea id="stateToStringText" />
</mx:FormItem>
<mx:FormItem id="objectUtilToString" label="ObjectUtil.toString()">
<mx:TextArea id="stateObjectUtilToStringText" />
</mx:FormItem>
</mx:Form>
</mx:Panel>
</mx:Application>



The above MXML file displays the results of calling a toString() directly on the provided object (instance of State) as well as the results of calling ObjectUtil.toString() on the provided object (instance of State). The IState interface is not used in the code above.

The mxmlc complier does not allow this code to compile, but instead reports the error message "Error: Call to a possibly undefined method toString through a reference with static type examples:State." and specifically cites the following line of code: "stateToStringText.text = state.toString();" This is shown in the following screen snapshot:



This error message makes it fairly obvious that a toString() implementation needs to be added to the State class. This also is evidence of the point made earlier about a class extending Object not being able to explicitly access toString() without an implementation of it. In other words, the State class that extends Object without an explicit toString() implementation cannot have toString() called explicitly. This is definitely different than in Java where the toString() would call Java's root Object's toString() in this situation.

We'll add a toString() implementation to the State class and try it again. The method looks like this:


toString() added to State.as


/**
* Provide String representation of me.
*
* @return String representation of me.
*/
public function toString():String
{
return "The capital of " + this.stateName + " ("
+ this.stateAbbreviation + ") is " + this.stateCapital;
}



With this toString() implementation in place, the Flex application Main.mxml (which has been modified only to change the text returned from the setExampleType() method) compiles. When this compiled SWF is executed, it appears as shown in the next screen snapshot.




Based on the above, we see the need to implement the toString() on the class directly rather than relying on it to get that method from its Object parent class. Now it is time to look at what happens when we try to call toString on the interface this State class implements. Note that the interface does not define a toString() method. In Java, this does not matter and the appropriate toString() gets called on an object even if its interface doesn't specifically call out a toString(). To try this out the Main.mxml application is changed to use IState interface rather than the State class. The slightly modified Main.mxml is shown next.


Main.mxml (using interface)


<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
width="900" height="900"
applicationComplete="runExample();">
<mx:Script>
<![CDATA[
import mx.utils.ObjectUtil;
import examples.IState;
import examples.State;

/** Set up a state for the interface example. */
public function setUpState():IState
{
const state:IState = new State("Colorado", "CO", "Denver");
return state;
}

/**
* Display state information.
*
* @param state State whose information is to be displayed.
*/
public function displayState(state:IState):void
{
stateNameText.text = state.getStateName();
stateAbbreviationText.text = state.getStateAbbreviation();
stateCapitalText.text = state.getStateCapital();

stateToStringText.text = state.toString();
stateObjectUtilToStringText.text = ObjectUtil.toString(state);
}

/**
* Set descriptive information on this example type. This is a
* convenience function related to demonstration of principles in
* a blog entry, but is not significant to the functionlity of this
* class.
*/
private function setExampleType():void
{
exampleTypeText.text =
"toString() method implemented in class but not interface; "
+ "call attempted via the interface.";
}

/** Run the example of using Flex interfaces. */
public function runExample():void
{
setExampleType();
displayState( setUpState() );
}
]]>
</mx:Script>

<mx:Panel id="mainPanel" title="State Example: Using Flex Interfaces">
<mx:Form id="stateForm">
<mx:FormItem id="stateNameItem" label="State Name">
<mx:Text id="stateNameText" />
</mx:FormItem>
<mx:FormItem id="stateNameAbbreviation" label="State Abbreviation">
<mx:Text id="stateAbbreviationText" />
</mx:FormItem>
<mx:FormItem id="stateCapital" label="State Capital">
<mx:Text id="stateCapitalText" />
</mx:FormItem>
<mx:FormItem id="exampleType" label="Example Description">
<mx:TextArea id="exampleTypeText" height="75" />
</mx:FormItem>
<mx:FormItem id="objectToString" label="toString()">
<mx:TextArea id="stateToStringText" />
</mx:FormItem>
<mx:FormItem id="objectUtilToString" label="ObjectUtil.toString()">
<mx:TextArea id="stateObjectUtilToStringText" />
</mx:FormItem>
</mx:Form>
</mx:Panel>
</mx:Application>



The mxmlc compiler will not allow this to compile for the same reason that it did not allow compilation before toString() was added to the State class. The reason for this is that the IState interface does not have toString() defined in it.




The following can be added to the IState interface:

toString() Definition to Add to IState Interface


/**
* Provide String representation of me.
*
* @return String representation of me.
*/
function toString():String;


With this toString() defined in this interface, the code compiles. The resultant SWF renders as shown in the next screen snapshot.




What can be gleaned from this is that the interface must spell out toString() as an available method if any class implementing that interface is expected to provide its String representation via the interface. Rather than adding a toString() method to every interface one might ever implement in ActionScript, a slightly cleaner approach is to define a base interface with toString() and have all interfaces extend it. For example, one could define the IBase.as as shown in the next code listing.


IBase.as


package examples
{
/**
* Base interface providing a toString definition for all child interfaces
* to use.
*/
public interface IBase
{
/**
* Provide String representation of me.
*
* @return String representation of me.
*/
function toString():String;
}
}



With IBase.as defined above, the IState.as code becomes slightly simpler because it no longer needs to define toString(). The new and improved IState that extends IBase is shown next:


package examples
{
/**
* Simple interface example demonstrating use of interfaces in Flex 3.
*
* Methods defined in a Flex interface cannot have any modifiers.
*/
public interface IState extends IBase
{
/**
* Provide name of the state.
*
* @return Name of the state.
*/
function getStateName():String;

/**
* Provide the state's abbreviation.
*
* @return Abbreviation of the state.
*/
function getStateAbbreviation():String;

/**
* Provide the capital of the state.
*
* @return Capital of the state.
*/
function getStateCapital():String;
}
}


As with Java, ActionScript classes can implement multiple interfaces, so this approach will often work very well, even if other interfaces need to be implemented.


Other Possibilities

Another possible approach for getting to a toString() implementation on a class is to cast the interface to the specific class that implements that interface. In this example, this would mean casting the IState interface to State and then calling toString() on that State instance.

The casting approach is demonstrated by the following modified version of the Main.mxml's displayState() method.

Main.mxml displayState() Method Using Casting


/**
* Display state information.
*
* @param stateInterface State whose information is to be displayed.
*/
public function displayState(stateInterface:IState):void
{
const state:State = stateInterface as State;
stateNameText.text = state.getStateName();
stateAbbreviationText.text = state.getStateAbbreviation();
stateCapitalText.text = state.getStateCapital();

stateToStringText.text = state.toString();
stateObjectUtilToStringText.text = ObjectUtil.toString(state);
}


This example demonstrates ActionScript's alternative casting syntax (use of 'as'). Note that I could have used "const state:State = State(stateInterface);" in place of "const state:State = stateInterface as State;".

Although casting obviously works, I am generally against casting because it is often a red flag of bad design. Perhaps even more significant is the possibility of the cast throwing a runtime error or returning a null. It is uglier and more dangerous to cast than to simply provide the toString() definition in the interface.

Another alternative would be to make the properties of State public. If this was done, then the ObjectUtil.toString() would print these public attributes of that class as shown in the next screen shapshot.



I don't like this approach because it requires the properties to all be public, which defeats some of the purpose of class-based encapsulation. So, I still prefer the approach of using a super interface with a toString() method defined.

A nice side effect of having toString() specified in an interface is that implementing classes must implement it. Perhaps the biggest drawback I've seen with Java toString() is that it is often not implemented by developers. Although Java does give Object's toString() representation in such cases, that is often not very useful.

Conclusion

The use of toString on an ActionScript class accessed by its interface is somewhat different from the same situation in Java. However, it is easy to address this issue by providing a basic super interface that provides for toString() and having all business interfaces extend that super interface. The side benefit of this is that classes which implement the super interface or any of its extended interfaces will be forced to implement a toString() method.

No comments: