Object oriented programming in Nasal

From FlightGear wiki
Revision as of 21:10, 10 November 2013 by BotFlightGear (talk | contribs) (Use Nasal highlighter)
Jump to navigation Jump to search


"Object Oriented Programming" is all about creating "things" (i.e. a cloud) with "actions" (transform,draw,update) (or "messages").

Understanding hashes and namespace

Please make sure to first read and understand Howto:Understand Namespaces and Methods.

Hashes as Containers for your variables and functions

Where a class (or hash in Nasal) is the "template" for a "thing" containing a number of member attributes/fields (variables and functions).

Conventionally, you would have a number of functions working with global state (variables), a class (OOP) allows you to wrap all such shared state in dedicated objects, so that related state is kept in the same place, imagine it like a container for your variables and functions.

So the class only describes the "layout" or the properties of objects that can be created. To actually use a class, it has to be "instantiated" which means creating an object using a specific "template class" (or even several different classes:multiple inheritance).

These member fields can be variables (e.g. lat, lon, alt) or functions like: setAlt() or setPos().

And the actual instance (cloud[n] in the property tree) of such a thing is then called an "object".

Basically, the point is that you have "containers" called "classes" which can be used to create new objects, called "instances". Once a new object is created, it inherits all the attributes from the parent class, which is used as a "template". This means that you can declare a simple template class with certain variables and functions, and each "version" ("instance") of your class will have these variables and functions.

Methods are Functions in a Class

Functions in a class are called "methods" because they work with instance-specific variables. Now, you can just as easily add methods (=functions) to your new position3D class template:

var position3D = {
 x:100.00,
 y:200.00,
 z:300.00,
 hello: func() {print("Hello world"); }
};

This add a new method (=function) to the hash called "hello", which takes no arguments (empty argument list).

As you can see, you can easily call a method like this:

 var test = {parents:[position3D] };
 test.hello();

Now, the interesting part is accessing object-specific (instance) variables, i.e. variables that are specific to the "version" of the class you created. This is accomplished by prepending the keyword "me." to your class variables, so that the interpreter knows that you want to access the instance data:

var position3D = {
 x:100.00,
 y:200.00,
 z:300.00,
 info: func {
  print("x:",me.x, " y:", me.y, " z:", me.z);
 },
};

Functions that work with instance specific state are called "methods", they may refer to instance specific state using a "self reference" (me) in Nasal, that ensures that access to a field or method is using the right instance-specific data.


Creating Objects from Class templates: Instantiation

var position3D = {
 x:100.00,
 y:200.00,
 z:300.00,
};

This declares a new "hash" named "position3D", now to use it as a class template, you would just need to create a new hash and declare the "parents" vector to point to the class "position3D":

 var test = {parents:[position3D] };

This tells the Nasal engine:

  • create a new variable named "test"
  • make it a hash
  • set the parents vector to point to "position3D"
  • i.e. use position3D as a "template"
  • and inherit all the fields/functions from the position3D, so that the new "test" variable has them too

Basically, this will give you an exact copy of the original position3D class, without having to specify it fully (i.e. less typing for you!).

Now, when accessing the new "test" variable, you'll see that it has inherited the fields from "position3D":

 var test = {parents:[position3D] };
 print(test.x);
 print(test.y);

Composition: Classes containing other Classes

Classes may be composed of other classes, i.e. a "cloud field" class would in turn contain "cloud" classes. This is then called "composition".

Another way is inheritance, which is really just like instantiation but with an option to overload/parametrize the original class, where a new type may inherit properties (fields:variables and methods) from a "parent" class:

 var position3D = {x:0,y:0,z:0};
 var new_class = { parents:[position3D] };

Imagine it like taking a "template" for the class and then saying "make a new class using this template", to ensure that the new class has certain fields and methods (x,y,z in this case). This could for example be the case for a vehicle class, which may have properties to define the maximum groundspeed.

Inheritance has the added advantage of providing a means to customize class behavior without having to modify the original class, because all member fields can be parametrized in your new templated class.

For example, a "cumulus" cloud class could be derived from the "cloud" class, just by parametrizing it (different cloud model, different texture, different transformations), without touching anything in the actual "cloud" class. You would just be creating a copy of the original class, using it as a template and then override anything that you want to be customized.

This is basically how OOP may be understood: things are classified according to "is a" or "has a" relationship.

Encapsulation: Hiding implementation details

In OOP, internal state is managed by wrapping everything in a class using accessor functions for modifying and getting internal values. So internal state would in turn only be modified by an abstract interface: class "methods", instead of directly accessing class-internal fields.

This provides a way for managing access to a member variable (field), such an abstract interface is also useful for keeping logic private, and internal. For example, the name of a variable "altitude" can be easily changed internally to "altitude_ft", without having to rename all users of the class - simply because all other code will refer to the methods providing access to the field, such as setAltitude() or getAltitude()

For example, instead of doing something like:

 cloud.lat=10.22; 
 cloud.lon=43.22;

...you would have a method accepting lat/lon variables:

 cloud.setPos(lat, lon);

That means that the actual variables containing the values for lat/lon are not exposed or used outside the actual object itself. This is called "encapsulation" and provides you with a way to manage state and ensure that internal state is valid at all times, because other code may only use the external interface, which may contain additional logic to verify/validate all attempts at changing internal state

This allows you for example to simply rename a class variable, without having to change any of the code that uses the object, because other code only uses public class methods.

Another important thing in OOP is separation of concerns, i.e. you don't want to end up with huge bloated "super classes" that manage all sorts of different state, but instead use different classes where appropriate, to split code into abstract "modules" with well-defined responsibilities.

So, one of the very first steps to convert procedural code to OOP code would be to group your code into a number of logical "classes" (e.g. cloud, cloud field, position3D ).

Of course, one may still use objects like conventional variables for passing and returning parameters, in other words: Your functions can also accept objects as arguments and return new objects. A simple function which returns new objects, may be an allocator to create new objects:

 var position3D = {x:0,y:0,z:0};
 var  new_position = func {
  return { parents:[position3D] };
 }

A more detailed tutorial with code examples is available here Howto: Start using vectors and hashes in Nasal (see the hash section).