Collections

From HEWIKI
Jump to: navigation, search

Contents

A fundamental design pattern in programming is creating lists (Collections) of objects, adding new objects, removing objects and iterating through those objects. Further, some generic GUI-based system for management and editing of collections is provided.

What problems do Collections solve?

What problems do Collections not solve?

Concepts

A collection is a node

The environment of HeroEngine dictates a collection is a node that is able to collect other nodes, able to perform the common operations of add/remove/list.

Elements of a collection are nodes

Nodes are HeroEngine's version of "objects" from an object oriented programming perspective. They may have code and data storage associated with them as dictated by the classes from which the node is composed. Represented in the class method scripts and fields of the classes.

Collections may have elements of unlike type

The basic collection functionality does not require elements be of any particular type (class).

Usage

Consider the fictional Fruit Basket Co. whose primary product is of course overpriced fruit baskets. Because each customer gets to choose the fruits present in the baskets, you never really know what you will find in the basket other than the fact it will have some basic fruity traits ( such as it can be eaten. ).

Fruit basket requirements:

All of which sound like exactly the kind of work collections are meant to do for us. Lets translate the concept into coding terms.

A sample collection

Ok, so it says the fruit basket is a collection...but fruit baskets probably do things that are unrelated to the types of stuff handled by collections. How do I make a fruit basket collection?

With the advent of class methods we suddenly have a really powerful and easy way to make collections. Assume for the moment you have a FruitBasket class that has some fields in it and in turn you have a FruitBasketClassMethods script that has some methods (code) that a fruit basket knows how to run. If we want to add the functionality of a collection, there are two ways we can do it.


Compositing Behaviors onto an Existing Node

For the purposes of this discussion, compositing is the act of extending the base behaviors of an object to add functionality. While composition is a very valid method of extending a node's behavior, for extensions that are always needed for your class to function inheritance is the preferred method.

HSL has an unique ability to extend objects during runtime via the act of GLOMming a class onto another object. Which means, to make a object of class FruitBasket we can simply GLOM the CollectionUnorderedSet class onto the object and immediately take advantage of all of the code inherit in the class methods of that class.

aFruitBasket as noderef of class FruitBasket = CreateNodeFromClass( "FruitBasket" )
GLOMClass("CollectionUnorderedSet", aFruitBasket )

Inheriting Behavior

The use of class inheritance is the preferred method for extending your class to include the collection behaviors. It is preferred because it causes any node created from your class to automatically have the functionality you expect it to have without requiring additional steps to composite behaviors.

Since we know all fruit baskets need to have the collection methods to function, we can instead use the CLI to add CollectionUnorderedSet class as a parent of the FruitBasket class.


 Using the appropriate prefix for the GOM in which to make the change:
 / - client temporary GOM
 | - client permanent GOM persisted on the Server
 \ - server GOM
 {prefix}mcdap FruitBasket; CollectionUnorderedSet

Creating New Pieces of Fruit

Not all collections need to know how to create the elements they collect, but we do so we need a method to create elements ( note: normally this is done in a class that inherits from collectionElementFactory, but for simplicity we are just going to implement the factoryCollectionElementInstance method directly in our FruitBasketClassMethods script.)

method factoryCollectionElementInstance( fruitBuildData as noderef ) as noderef
  newFruit as noderef
 
  where fruitBuildData is kindof fruitBuild
    when fruitBuildData.fruitType
      is "orange"
        newFruit = CreateNodeFromClass( "orange" )
      .
      is "apple"
        newFruit = CreateNodeFromClass( "apple" )
      .
      is "grapes"
        newFruit = CreateNodeFromClass( "bunchOfGrapes" )
      .
    .
  .
  return newFruit
.

Using our Fruit Basket Collection

Assume we have a class baseFruit from which apple, oranges and grapes inherit. Now that we have our FruitBasket class defined as inheriting from CollectionUnorderedSet we can start creating creating fruit to put into our collection.

Lets create a new fruit basket and add an apple, an orange and a bunch of grapes to our fruit basket.

//
// Small method in FruitBasketClassMethods that handles the creation and addition of a new fruit to the basket
//
method AddFruit( fruitType as enum FruitTypes )
  // factoryCollectionElementInstance takes noderefs so it can take any class of construction data you might
  //   choose to pass.  Consequently we need to create a noderef to pass.
  fruitBuild as noderef of class fruitBuild = CreateNodeFromClass( "fruitBuild" )
  fruitBuild.fruitType = fruitType
 
  // Notice how we can add the collectionElement inline with the creation of the element to add
  //
  me.AddCollectionElement( me.factoryCollectionElementInstance( fruitBuild ), true )
 
  // clean up the fruitBuild node that we no longer need
  //   This is necessary to avoid leaving the node dangling in memory.
  DestroyNode( fruitBuild )
.

Now that we did all that work, adding a new piece of fruit is incredibly easy...

fruitBasket as noderef of class fruitBasket = CreateNodeFromClass( "fruitBasket" )
fruitBasket.addFruit( "orange" )
fruitBasket.addFruit( "apple" )
fruitBasket.addFruit( "grapes" )

The FruitBasket and Fruit Class Diagrams

FruitBasketandFruitClassDiagrams.jpg


Why should a fruit basket be a collection?

Making the FruitBasket a collection lets us reuse a bunch of code that was written by someone who really thought about the design of a collection of elements. As the creators of the FruitBasket class we do not need to worry about how elements are handled by the fruit basket, knowing that we can add/remove/list and iterate through the elements is sufficient for everything we need.

While we could have written our own custom solution to do all of the work handled by collections, there is normally little to no gain in doing so.

So let's talk real code

For a Hero's Journey example: A developer was tasked with making a wyrSpecOracle on the client, it needed to add/remove/list elements (wyrSpecs) and then provide additional capabilities that were specific to the wyrSpecOracle dealing with asynchronous access to the information and finding a particular spec by a key value. Because the developer knew collections do all of the work involved with add/remove/list, she created a wyrSpecOracle class that inherits from CollectionUnorderedSet.

Since she was able to inherit the behaviors of the collection class, she was able to concentrate on the rest of the code required for the wyrSpecOracle.

Technical Resources for this Implementation:

Reference

CollectionClassDiagram.jpg

Types of Collections

All HSL collections have the Collection class or one of its children as their parent ( for programmers consider the CollectionClass an abstract class that exposes the interface which child classes may override ). Each type of collection may optionally add or change the behavior of the interface defined in the Collection class. In general, collection types describe a collection implementation that has broad application.

Type Description
UNORDEREDSET Adds the behaviors: preventing an element from being in the collection twice
ORDEREDSET Adds the behaviors: knowing the order of elements in the collection and preventing an element from being in the collection twice
LINKEDLIST *not yet implemented LINKEDLIST collections are faster for inserting/adding elements but slower when attempting to access by an index

Collection types are enumerated in collectionEnums in the GOM, new collection types MUST be added to this enumeration to function.

Creating a new type of Collection

Do I Need To Create a New Collection Type?

It is usually unnecessary to create a new collection type unless you are adding to or changing the behavior of the interface methods. Collections are agnostic as far as what elements they contain are or what those elements might do.


Creating a New Collection Type

Note: CLI commands are shown, although you can use the DOM Editor as well.

Naming Collection classes

All collection classes must use the naming convention of:

Collection + <CollectionType>

That means the collection class representing the ORDEREDSET collection type is the class named CollectionOrderedSet.

Using Collections in your scripts

Create a Collection

To create a collection:

collection as NodeRef of Class Collection = CollectionUtils:Create( "ORDEREDSET", false )

Make an existing node into a Collection

  // current options for collection type are ORDEREDSET and UNORDEREDSET
  CollectionUtils:MakeNodeACollection( theNode, ORDEREDSET )

Adding an element to a Collection

  // second parameter is a boolean indicating add to back when true
  //   event unordered collections use this parameter to keep the
  //   method signatures the same
  collection.AddCollectionElement( element, true )

Removing an element from a Collection

There are three options for determining what should happen to the removed element:

Removal Behavior Description
KEEP Removed element is not deleted
DESTROY_IF_OWNED Elements are considered "owned" by a collection if they are base_hard_associated to the collection node. This only happens if there is no other base_hard_association for the element at the time it is added to the collection.
DESTROY This should rarely be used because elements may be part of multiple collections. While collections are capable of handling elements that have been destroyed, it is preferred to use the DESTROY_IF_OWNED option.

  someElement as noderef = someElementID
  collection.RemoveCollectionElement( someElement, KEEP )

Iterating through a Collection

The process of "looping through" the contents of a collection is known as "iterating" and is done with an iterator:

All collections are able to instantiate an iterator ( node of class collectionIterator ) that understands how to iterate through the collection. While all collections implement a method listCollectionElements, it is generally prefered to use the iterator because it handles changes to the collection during iteration.

  iterator as Noderef of Class CollectionIterator = collection.CreateCollectionIterator()
  currentNode as NodeRef
  while iterator.NextCollectionElement( currentNode )
    // do stuff, the current noderef will be set to the element for the current iteration.
  .
 
  // Note when you are done with the iterator you must clean up after yourself by calling
  //   the iterator's DestroyIterator method.   Failure to do this leaves the iterator node
  //   in memory ( in effect the HSL version of a memory leak.
  iterator.DestroyIterator()

Note: [DMW] Is this true? Won't it just clean up at the end of the script execution? And if it is true, shouldn't it be "MUST" instead of "SHOULD" in the description?

Response: [CWL] I considered automatic cleanup, but it occured to me that people might want to process 100 elements of a collection per minute or something to spread the load of a 100000 element collection, in which case they would want an iterator that could surive beyond script execution. I adjusted the comment to be MUST because either way it requires you clean up the iterator eventually.

Additional Iterator capabilities

Number of Elements

numElements as Integer = iterator.length()

Get the first element in the collection

Iterators use an internal representation of the collections elements as a list, which means you can get the first element of any type of collection even if they do not technically have a real first element. In other words, the firstElement of an unordered collection may vary from one iterator you create for the collection to the next.

element as NodeRef
if iterator.FirstcollectionElement( element)
  // element is now a reference fo the first element in the collection
else
  // no first element found
  // element = None
.

Get the last element in the collection

As in FirstCollectionElement, getting the last element works on unordered collections with the same caveats.

element as NodeRef
if iterator.LastCollectionElement( element)
  // element is now a reference fo the last element in the collection
else
  // no last element found
  // element = None
.

Clean up the Iterator

Iterators are nodes, as such if you fail to clean them up they clutter up memory. Fortunately, cleanup is simple.

iterator.DestroyIterator()

Note: While it is not enforced by the engine, you **must** use the iterator's DestroyIterator function rather than HSL's DestroyNode because the iterator may have additional code it needs to run prior to destroying itself.

Extending a Collection

The collection Types handle fundamental behaviors common to collections, but there is often a need to extend those capabilities to enable the collection to marshal its elements, to transmit the collection to the client for editing, and to add system specific behaviors. I have opted in design to make the extension of a collection's capability to be through composition rather than a rigid hierarchy of inheritance. That means, for specialized behaviors we favor GLOMming a new class with its appropriate classMethods over creating a new collectionType with system specific behaviors.

Consider a basic collection of fruit ( a fruit gift box with only one of any given type of fruit ) with the elements: an apple, an orange and a bunch of grapes. The basics of add/remove/list are all handled by the collection class ( //CollectionUnorderedSet// ), if the collection needs additional behaviors such as the ability to send itself through the mail ( transmit ) we composite those behaviors onto the collection by GLOMming a helper class or a child of that helper class.

CollectionExtensionDiagram.jpg

Important When extending a collection you intend to transmit to the client, it is often necessary that you create dummy classes that are GLOMmed onto the serverside version of the collection for any extension in abilities you need on the client. For example, if you implement a child class of the collectionInterface class on the client, you would want to create the same class on the server and GLOM it to the collection so that when the collection is transmitted it AUTOMATICALLY has the extended capabilities on the client.

Structuring your collection in this manner provides extremely powerful capabilities when using methods, because the client version of your collection has the proper classes it has the code you want it to be capable of executing.

Transmission to Client

Collections that are to be edited on the client will add the collectionTransmission class (or one of its children classes) to the collection via GLOMming. The collectionTransmission class handles moving a collection or its elements between client and server. It is unlikely that you would need to create a custom child class to handle transmission, but the capability to do so is there should you need it.

CollectionTransmissionClassDiagram.jpg

Note transmission requires your collection be of class collectionMarshal, typically during collection creation you GLOM on both collectionTransmission and collectionMarshal classes ( or the child(ren) class(es) you created to override the base functionality ).

Naming a CollectionTransmission Child Class

The naming convention used is: collectionTransmission + <YourClassName>

ClassMethods Script: collectionTransmission + <YourClassName> + ClassMethods

Example: collectionTransmissionAreaStates

Requesting the transmission of a Collection to the client

Requesting a collection from the client necessitates you register a listener with the event system to listen for the arrival of your collection.

  // Create a listener, node or script listener
  listener as NodeRef of Class ObserverSystemListener = EventSystem:CreateNodeListenerForSpecificNode( "oncollectionreceived", collectionInterface, collectionID )
  // when the control disppears we wnat to get rid of the listener
  AddAssociation( collectionInterface, "base_hard_association", listener )
 
 
  // Send a request to the server requesting collection by its ID
  CollectionUtils:RequestHeaders(collectionID)

When the collection is received, the registered node or script will get a callback in the shared function:

  shared function EventNotification( eventObject as NodeRef of Class eventObject )
    when eventObject.eventType
      is "oncollectionreceived"
        // do stuff
      .
      default
        // events we do not care about
      .
    .
  .

Invoking the Collection Interface GUI to view a Collection

(does this work with Clean Engine?)

rout as Class RemoteCallClientOut
rout.toPlayer = me
rout.toScript = "collectionInterface"
rout.toFunction = "RemoteOpenGUI"
rout.failScript = debugUtils
rout.failFunction = "ClientRMCFail"
rout.args["collectionID"] = collectionID
RemoteCallClient(rout)

Marshalling the Collection

If you need to transmit your collection, either to the client or to another server the collection needs to be able to marshal ( turn its node representation into a "transmitable" representation ) and unmarshal itself. For collection whose elements are simple, with at least some of the class(es) existing in the destination GOM, the collectionMarshal base class should be capable of handling the marshalling of the collection, and this is all you need. For complex elements, such as node hierarchies with complex releationships dictated by associations, you will need to create your own marshaling and unmarshallling technique by implementing a child class of collectionMarshal.

CollectionMarshalClassDiagram.jpg

Note marshalling is most likely not needed if you have no plan to ever transmit the collection from its current GOM. CollectionTransmission requires the collection also be of class collectionMarshal.

Naming a CollectionMarshalling Child Class

The naming convention used is: collectionMarshal + <YourClassName>

ClassMethods Script: collectionMarshal + <YourClassName> + ClassMethods

Example: collectionMarshalAreaStates

Special Handling for "Script" Fields

Marshaling a node to be transmitted from the client to the server (or vice versa) is a fairly striaghtforward process, except in the case of Script fields. This is because the server and clients have completely different scripts, so if this field is transmitted from (say) the server to the client it's meaning would be wrong or undefined over there and vice versa.

To deal with this, the external function MarshalNode specifically excludes the script and subscript fields because the odds are good that the scripts contained therein are not present in the destination GOM (client vs. server). But, many times you want to have the information about what these fields contained still transmitted. For example, if you creating a GUI to edit a server node. This node has to be transmitted to the client for the GUI, and the GUI might expose changing the script. So to deal with these situations the following special handeling is performed:

The collectionMarshal class handles this automatically by remapping the Scripted class and the contents of the script and subscript fields to the client class ScriptedAsString with the fields ScriptAsString and SubscriptAsString. This allows you to edit those fields without worrying about the value changing because a script does not exist in the client GOM.

When you transmit a collection or its elements back to the server, a second remapping is done to translate the ScriptedAsString class into their proper fields.

Displaying the Collection in a GUI

Collections support a common GUI for listing their elements along with a common interface for add/edit/delete. Because of the infinite complexity of potential elements, it is not possible for base collection classes to know how to create GUIs for editing elements. Consequently, any collection that wishes to make full use of the collection interface GUI needs to implement a child class of the collectionInterface class.

CollectionInterfaceClassDiagram.jpg

The Collection Interface GUI

The collection interface gui has a number of extremely useful and expected capabilities:

CollectionCommonInterface.jpg

Enabling the Collection to Create New Elements

Sometimes it is useful for a collection to be able to factory up new elements for the collection. This is most commonly required for using the collection interface to add new elements. If your collection is never displayed on the client, your collection may not need this capability. The helper class for factorying new elements is the collectionElementFactory class, it implements the methods necessary for the collection to factory up an element.

CollectionElementFactory.jpg

The methods accept a noderef that may represent any information required for the method to create a properly formed element.

Note Creating an element does not automatically add it to the collection.

DOC TODO List

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox