Rationale for "formal" support of data encapsulation.

Started by monkey0506, Thu 25/11/2010 04:48:11

Previous topic - Next topic

monkey0506

For those who aren't familiar with it, I have written an article in the wiki regarding data encapsulation in AGS.

Since AGS 2.7's move toward object-oriented programming, AGS has been making use of the undocumented and unsupported keyword attribute. Every single member (property) of every single managed type (Character, Hotspot, Object, InventoryItem, GUI, GUIControl, etc.) is defined as an attribute.

So without simply restating what's in the wiki article regarding the implementation of an attribute, what really is it?

When you declare a property within a struct as an attribute, you must define it as an import:

Code: ags
import attribute int Property;


The reason this has to be imported is due to the way that the AGS compiler handles linking. The easiest way to explain "linking" is to look at the import keyword. Whenever you import a function/variable/pointer/struct instance the idea works similarly to that of a pointer in that instead of creating a new copy, you are just going to refer back to the one that already exists. This creates a link so that when you call "MyFunction" from room5.asc, the compiler knows you are referring to the function you actually defined in Functions.asc.

So when you're declaring an attribute, AGS is internally linking that attribute to its respective getter and setter accesor functions. Regardless of whether they are actually defined, AGS will internally link this attribute Property to functions get_Property and set_Property for the owning struct of the attribute. There isn't a compile-time error if the function doesn't exist because all this link does is tell AGS where to look if the linked resource is referenced.

In the case of attributes there is more going on internally than other imported declarations so it is more important that they be properly linked, but since we know what the attributes are being linked to we can safely ensure that the links are valid for runtime at compile-time.

The link created when an attribute is defined is essentially just allowing you the ease of access of using a property whilst allowing the expansive control possibilities of a function. This is perhaps the best reason for supporting encapsulation.

As it presently stands, the only technical reason that attributes aren't supported for use by the end-user is the sheer fact that no one has ever said, "Yeah, I'll support that." Understanding of how attributes work has been established. We now have the ability to look at this and determine what, if anything, may need to be changed within a user's script to make attributes function as expected; what is actually taking place when we look at a script written by someone else, but isn't working properly.

Again, attributes are intrinsically tied to the built-in data structures of AGS. Short of a massive overhaul of the built-in types, this is not likely to change in the near future.

With AGS making the move toward becoming open-sourced, I would like to propose, formally, that end-user support be formally adopted for usage of attributes within user scripts.

Below I'll even take a look at yet another benefit of data encapsulation that I thus far have not previously considered.


Thank you for your time.

monkey0506

One long-standing request for AGS has been pointers to custom struct types. While it is extremely limited, data encapsulation can actually simulate (very effectively) pointer functionality for static instances of custom structs.

To further explain, let's dive right into the example:

Code: ags
// Script.ash

struct PseudoPointer {
  import attribute int A;
  import attribute int B;
  readonly import attribute int LoadedInstanceID;
  readonly import attribute bool IsNull;
  protected bool loadedInstanceID;
  protected bool isNull;
  protected bool initialized;
  import void LoadInstance(int ID);
};

#define INSTANCE_COUNT 20 // number of "static" instances

// Script.asc

struct Type {
  int A;
  int B;
};

Type instances[INSTANCE_COUNT]; // this is hidden from the end-user within the module code

bool get_IsNull(this PseudoPointer*) {
  if (!this.initialized) {
    this.isNull = true;
    this.initialized = true;
  }
  return this.isNull;
}

int get_LoadedInstanceID(this PseudoPointer*) {
  if (this.get_IsNull()) AbortGame("Null pointer referenced! Game cannot continue.");
  return this.loadedInstanceID;
}

int get_A(this PseudoPointer*) {
  return instances[this.get_LoadedInstanceID()].A;
}

void set_A(this PseudoPointer*, int value) {
  instances[this.get_LoadedInstanceID()].A = value;
}

int get_B(this PseudoPointer*) {
  return instances[this.get_LoadedInstanceID()].B;
}

void set_B(this PseudoPointer*, int value) {
  instances[this.get_LoadedInstanceID()].B = value;
}

void PseudoPointer::LoadInstance(int ID) {
  this.initialized = true;
  if ((ID < 0) || (ID >= INSTANCE_COUNT)) { // an invalid ID is the same (in our case) as setting the pointer equal to NULL
    this.isNull = true;
    return;
  }
  this.isNull = false;
  this.loadedInstanceID = ID;
}


That's actually a fair amount of setup just to allow pseudo-pointers to a custom type with only two properties. But, putting that aside, let's look at the actual end-user usage:

Code: ags
// Elsewhere.asc, within SomeFunction
PseudoPointer myPointer;
myPointer.LoadInstance(5);
if (!myPointer.IsNull) myPointer.A = 42;
PseudoPointer otherPointer;
otherPointer.LoadInstance(5);
if (!otherPointer.IsNull) Display("A: %d", otherPointer.A); // displays 42


One of the most obvious drawbacks to this approach is that it only works on predefined (static) instances. You can't create dynamic instances (meaning within a function, etc.; they must all be within the module code, where they can be managed/referenced) of the underlying struct and link them dynamically to the pseudo-pointer struct. Another drawback is that this isn't very easily nested to allow pseudo-pointers within custom structs (primarily due to the limitations regarding the static linking).

For that, we'll probably have to wait for full pointer-to-custom-struct-type support. However, encapsulation can provide a way to quickly and easily define script aliases (even dynamically). It just takes some setup.. :=

SMF spam blocked by CleanTalk