- Introduction
- What is a PropObj
- Uses for PropObj
- PropChanged
- PropDef
- Fields and PropDefs
- Sub PropObjs
Introduction
You might have come across a lot of classes having PropObj
as one of its parent classes. What is this? Why is this the case? And how do we effectively use this? We will outline all these questions in this article.
To summarize, PropObj
is introduced as the foundation for dynamic capabilities that are gated by inconveniences of any Object-Oriented Programming. This gives us convenient information sharing of classes without casting, dynamic data encapsulation, safe data diversification and modern development ideologies and methodologies within CET revolve around the PropObj
concept.
What is a PropObj
A PropObj
is a class directly extending from Object
which houses the propData
field and PropDefs
, hence the name PropObj
. The propData
field, which is simply a str->Obj map that can hold almost any data you can think of. All important classes such as Snappers, Vessels, and Animations inherits indirectly from PropObj
, meaning that they can retrieve/add/remove data from each other without the need to cast into classes that are able to access the field.
Object -- cm.lang
PropObj -- cm.props
PreCorePropObj -- cm.props.advanced
CorePropObj -- cm.core
CoreObject
Entity
Snapper Vessel Animation
Here's a very simple example of how you can add/retrieve data to/from your propData
map:
use cm: core, win; /** * Prop example. */ public class PropExample extends PropObj { /** * Constructor. */ public constructor() { this."height" = 5; } }
{ PropExample ex(); pln(ex."height"); pln(ex.propData); }
this."height" = 5inch
is actually a simplified version of this.put("height", 5inch)
, and ex."height"
is a simplified version of ex.get("height")
As you can see, adding the height prop is as simple as a single line without the need to declare the field name and type. This conveniently adds an entry to the propData
str->Object map of "height"->5.
Accessing the field is also as simple as a single line. Do note that the retrieved field will be an Object
class, and therefore needs appropriate casting for proper usage.
Uses for PropObj
One of the main reasons to use PropObj
is to avoid handling fields manually for each class. We can deal with large groups of snappers easily using PropObj
, because we need not worry about the fields the snappers contain anymore. We'd just have to go through the contents of the propData maps in these snappers.
Let's look at the following example. We have two snappers that are of type Vehicle
and a snapper called Apple
that is completely unrelated to vehicles. I want to know how many tyres my vehicles have, but I do not want to iterate through my collection and cast each object to ensure that I only end up working with snappers that are of type Vehicle
.
use cm: core, win; /** * Vehicle. */ public class Vehicle extends ModelSnapper : abstract { /** * Constructor. */ public constructor() { this."tyreQty" = numberOfTyres(); } /** * Number of tyres. */ extend public int numberOfTyres() : abstract { } } /** * Car. */ public class Car extends Vehicle { /** * Number of tyres. */ public int numberOfTyres() { return 4; } } /** * Bike. */ public class Bike extends Vehicle { /** * Number of tyres. */ public int numberOfTyres() { return 2; } } /** * Apple (unrelated to Vehicle). */ public class Apple extends ModelSnapper { } { ModelSnapper[] snappers(); snappers << Car() << Bike() << Apple(); for (s in snappers) pln(#s.class; #s."tyreQty"); }
Output:
s.class=Car, s."tyreQty"=4 s.class=Bike, s."tyreQty"=2 s.class=Apple, s."tyreQty"=null
No casting needed! We're simply asking the Apple
's propData map if it has a value associated with the key "tyreQty". Since it doesn't, we receive nullas opposed to 4 and 2 from Car
and Bike
respectively.
PropChanged
When the value of a prop is changed, propChanged()
will be called. We commonly use this to "update" anything that is dependant on that prop's value.
/** * Property changed event. */ public void propChanged(str key, Object value, Object previous, Object env=null) { super(..); if (key == "height") updateHeightRelatedStuff(); }
PropDef
As the name suggests, a PropDef
gives some definition to an individual propData item (prop). A PropDef
always has a Type
associated with the value of the prop and always has the capability to hold a symbol set of attributes that the prop classifies itself with.
These attributes are commonly used to denote streamability and copiability of the prop. A CorePropDef
and CoreFieldPropDef
have capabilities beyond that of the normal PropDef
. These specializations offer initialization capability for domains, default values (field on the PropDef
), and a PropDef
setting (persistent environment).
There is a syntax designed for adding these PropDef
s to PropObj
s -- making the process of knowing what to add and how to do advanced manipulations much easier (caching PropDefs, construction/appending, overridden methods, etc..). Below are some examples of how to add PropDefs
and how to control the behavior of a prop.
By default, propData
entries are streamed (saved). Let's add a definition to prevent the tyreQty entry from being streamed by placing this in the Vehicle
class(refer to Uses of PropObj section of this article).
/** * Append prop defs. */ public void appendPropDefs(PropDefs defs) { super(defs); defs.put(PropDef("tyreQty", int, {#stream_null})); }
Alternatively, the PropDef
could be added to the PropObj
like so (See also Append PropDefs):
/** * Props. */ public props : stream=null { int "tyreQty"; }
Time to test our propDef!
- Make sure there are
Bike
orCar
snappers in the drawing, if there isn't insert one. - Save the drawing
- Reload the drawing.
- Run the following test:
{ Space space = mainSpace(); if (space) { for (Vehicle s in space.snappers) pln(#s, s."tyreQty"); } }
null should be returned for all the 'get' calls, because the values in the propData
map was not saved.
Fields and PropDefs
We can actually store the data in a field, and access it through the PropObj
interface. The relationship is handled by the FieldPropDef
, as shown below.
Alternatively (See also Append PropDefs):
/** * Material. */ private GMaterial3D _material; /** * Append prop defs. */ public void appendPropDefs(PropDefs defs) { super(defs); defs.put(FieldPropDef(class, "material", "_material", function materialSubsetCB, function materialDefaultCB)); }
/** * Material. */ private GMaterial3D _material; /** * PropDefs. * Create the FieldPropDef "material" and link to the field "_material". * On PropObj.domain("material"), the function materialSubsetCB is called and returned. * On PropObj.default("material"), the function materialDefaultCB is called and returned. */ public props { "material" : fieldName="_material", domain=function materialSubsetCB, default=function materialDefaultCB; }
We can also use methods on our subclassed PropObj (OurPropObj) instead of functions for getting the domain and default values associated with the field "_material":
/** * Material. */ private GMaterial3D _material; /** * PropDefs. * Create the FieldPropDef "material" and link to the field "_material". * On PropObj.domain("material"), the method OurPropObj.materialSubset() is called and returned. * On PropObj.default("material"), the method OurPropObj.materialDefault() is called and returned. */ public props { "material" : fieldName="_material" { Object domain(..) { return that.materialSubSet(); } Object default(..) { return that.materialDefault(); } } } /** * Material subset. */ extend public SubSet materialSubSet() { return null; } /** * Material default. */ extend public Object materialDefault() { return intenseBlueMaterial3D.gm; }
The link between the key "material" and the field "_material" is created from appending the PropDef alone. The default value is handled by materialDefaultCB()
, and we'll retrieve our material domain from the materialSubsetCB
. We can now change the material just by calling the put method in our propObj, instead of referring directly to our field.
this."material" = intenseRedMaterial3D.gm;
Or in the case we implemented the PropDef via methods for our default and domain values, we can set the value of "_material" to its default (intenseBlueMaterial3D.gm) like so. One advantage for doing it this way is that if "_material" happens to not be defined for the PropObj we call this on, either anonymous data is created or nothing happens. The same is true for PropObj.get("material")
, which in our case, would return the field "_material".
this."material" = default("material");
Sub PropObjs
A PropObj that 'owns' another PropObj (either as a field or maybe in propData) may expose the sub-PropObj's PropDefs, so as to appear as if it also has the PropDefs. This allows a more complex object structure to appear to have a flat structure (from the consumer's perspective). This pattern is useful when creating Core Properties from PropDefs on complex classes/objects.
See also PropDef Exposure.
Comments
0 comments
Please sign in to leave a comment.