September 90 - Meta-spaghetti
Meta-spaghetti
Donald E. Carlile
(BILL: I FORGOT TO GET A COMPANY NAME FROM DON. WANT TO GIVE IT A SHOP?)
I'm a fairly conscientious programmer-I like to think I write good clean code; I take
the trouble to use modular techniques; I even stood on my head to learn object-oriented
techniques. But in spite of all this, I found out I was writing spaghetti.
Spaghetti code! Someone still uses GOTO!?
Well, possibly I exagerrate: I don't mean spaghetti code in the old BASIC GOTO
sense-```I mean spaghetti code that is only possible when you are writing in an OOP
language. Maybe we'd better call it meta-spaghetti.
I found I was writing meta-spaghetti when I was asked by someone if I would let him
use some of my objects. I had known I had only one unit, but I hadn't realized how
tangled it was. It was then that I developed the term meta-spaghetti to define the
condition of my code.
What I mean by meta-spaghetti is objects which refer to each other recursively. That
is, objects which in their definitions include fields of each other's type, as in
TMyDocument = OBJECT(TDocument)
fStar : TStar;
TStar = OBJECT(TObject)
fMyDocument : TMyDocument;
This leads to strange loops and to units which cannot easily be decomposed into simpler
entities.
As anyone who has read even part of Hofstader's Gödel, Escher, Bach knows, it is
impossible to entirely banish strange loops: you made that reference in the object
definition for some reason, and it cannot be easily abandoned without some thought and
work. That being said, you might still want to introduce some measure of isolation
between your objects.
I can tell you from experience that it is difficult to undo the damage once it's done. As I
was unraveling my code, however, I developed some guidelines for avoiding and dealing
with meta-spaghetti. Perhaps they will prove helpful to you as well, as you design
your code from the beginning to avoid this condition.
1. References should only to be to objects down the
ownership chain.
As I was defining my set of rules, this is the first one that became apparent: when
defining an object, references can be made to the internals of the objects it owns. This
is what I mean by references down the ownership chain. (For a more complete
discussion of the ownership chain, see the author's Chains Required, Whips Optional,
an unpublished, and indeed unwritten, article.)
As an example, in my Trek-style game I had a method to add text to a textedit window.
This method belonged to my document object. In order for my ship objects to add text
about their status to the window, they had to have a reference to my document object.
This caused a problem when I wanted my ship objects in a separate unit so that others
could use them.
I solved this problem by changing the way I added text to the window. Instead of calling
the document method, I added a text handle field to the ship object. I then changed my
document DoIdle to check the texthandle of each ship object. In this way I followed the
rule of having only references down the ownership chain.
If this rule is followed, the separation of objects into units becomes very clear and
simple. There is no confusing interdependence of objects, and it is clear what the unit
dependencies should be.
There is only one problem with this rule: it's not always possible or desirable to
follow it. Some objects need to be interrelated; when this is the case, they probably
should not be separated. This brings us to the next rule:
2. When it is impossible to remove references between
objects, include them in the same unit.
The universe of my game is defined by two object types, TGalaxy and TQuadrant. These
two object types refer to each other quite a lot, and it makes a great deal of sense for
them to both be in the same unit. It is also unlikely that they would need to be
separated to be reused. Therefore, I have put them into their own unit.
3. When it is impossible to untangle references and they
must be in separate units, define a new object to make
the references and push it down the ownership chain.
Don't be afraid to revise your structure. Although it will save you work to think out
your structure ahead of time, sometimes this proves impossible. When you find you
have painted yourself into a corner, cut a new doorway.
In my original design, my TMyDocument object owned all the lists of quadrants and
other data containers. Thus, all my data objects referred back to it in order to refer to
each other. When I wanted to separate out my ships, stars and quadrants into other
units to make them usable elsewhere and to decrease compile time, I found the task
impossible.
I then defined a new object, a TGalaxy, to contain all the objectionable fields and
methods (no pun intended) and made a new field in the revised TMyDocument object to
refer to an instantiation of TGalaxy. This made it possible to cleanly make the
separation.
Make vanilla references when Rule 1 is impossible.
Another strategy for avoiding meta-spaghetti is to make references to the parent type
rather than the type that is giving you trouble. This is not always possible, but
sometimes the method or field you need to reference is contained in the object's parent
or ancestor.
In my game, I call the draw method of my TQuadView object type from one of the
methods of TQuadrant. I do this to avoid the flicker introduced when I use the invalid
rectangle methods of MacApp. Originally TQuadrant included a field fMyDocument of
type TMyDocument. TMyDocument, of course, had a reference to an fQuadView, of type
TQuadView. TQuadView was a specialized descendant of TView. When I needed to do a
drawing, I called fMyDocument.fQuadView.Draw. In this way, TQuadrant absolutely
needed to be in the same unit as TQuadView. It was also specifically geared to my
application only.
To untangle this particular mess, I added a field fQuadView, of type TView, to my
TGalaxy object. Since variables of an ancestor type can contain any descendent, this
field can contain an object of the specialized type TQuadView. The only limitation is
that specialized fields and methods may not be called using this reference.
At the time an object of type TQuadView is made, it is passed to fGalaxy of the
TMyDocument. Then I can call Draw, since Draw is one of the methods of the ancestor
TView class.
5. Define proto-objects for objects up the chain when
Rule 1 is impossible.
Sometimes all the above rules fail: you still want to break up objects into different
units but they can't come completely apart. When this happens, it is important to
remember that it is permissible to define objects or abstract classes which have no
instantiation. You can define these "proto-objects" to contain those fields and methods
which are necessary for the objects lower down the ownership chain and can include
these classes in the same unit with those objects. Then USE that unit in the unit which
defines the real objects and make them descendants.
As I mentioned earlier, TGalaxy and TQuadrant define the universe of my game. I found
it impossible to completely divorce these objects from the objects I refer to, i.e., my
ships, stars, bases,and torpedoes. I reasoned further that any structure which made
use of my entities would also need some kind of quadrant and galaxy. I then defined a
TProtoGalaxy and TProtoQuadrant, which had references for the fields and methods
necessary for my entity unit, while not burdening the unit with the heavy details and
the bulk of the fields and methods. I then defined TGalaxy as a descendant of
TProtoGalaxy and TQuadrant as a descendant of TProtoQuadrant.
This one might be just a bit tricky to follow. Let me try and restate it thusly: the
Galaxy and Quadrant objects have several fields and methods which ships and things
need to reference. Two examples of such fields are the galaxy array and the list of
other entities in the quad. An example of a referenced method is the one which detects
when collisions occur. I therefore defined objects, which I call proto-objects, which
contain references to these things. I don't define the methods-e.g., the collision
procedure in the protoQuad consists of a Begin and an End, as do the rest of the methods
in my proto-objects. In a different unit I then create descendants of the proto-objects
with all the fields and fleshed-out methods fully defined.
I don't claim that these rules are the simple once-and-for-all solution to the problem
of meta-spaghetti, but they are a start. You can probably come up with a few rules and
methods of your own. Good luck!
Rules for avoiding meta-spaghetti:
1. References should only to be to objects down the ownership chain.
2. When it is impossible to remove references between objects, include
them in the same unit.
3. When it is impossible to untangle references, and they must be in
separate units, feel free to define a new object.
4. Make vanilla references when Rule 1 is impossible.
5. Define proto-objects for objects up the chain when Rule 1 is impossible.
If you have any questions or comments for Don, he can be reached on AppleLink at
N0231.