All Rectangles Are Squares

Sometimes Function follows Form.

Contents


Introduction and Warning

As developers, we've hammered into our own brains the notion that a good program is one that is programmed smartly. We make objects which multitask, are flexible, and can be handled in a variety of ways. While we're writing our 'super objects', time is passing by and our game is going nowhere. This article outlines a way to prevent a great deal of wasted time and needless code, but first we have to change the way we think about programming.

Achtung! This may not be the article for you. If you're not well versed in inheritance, or you don't spend a large amount of time thinking through complicated systems before programming them, this article will be misleading and teach poor habits. If you've ever wasted countless hours programming extra parts of a system your game never used, or had to reprogram a system you put a lot of time into, read on.

Top

Being Too Smart Can Produce Bad Code

Imagine we're programming a system which makes use of 'square' objects. These squares need to know how long their sides are, and need a function for calculating their area. Hold up! What if our game needs rectangles, or some other shape, at some point in the future? We should program those now, right? So, let's start with a 'polygon' class, then program a 'quadrilateral' subclass (also, let's make triangle and pentagon classes, just in case), then a 'rectangle' subclass, and then we finish with the square subclass (well, we also program some trapezoid and rhombus classes, too). Now, our program can handle anything!

Most of us have done this, at some point, only to never use a single polygon, rectangle, rhombus, or any other part of our complex geometry systems. We release the buggy system as a 'mini library' or 'demo' in the hopes that someone else will find it useful. Or, even worse, we do use those objects, but later developments in our program demand that they behave differently; we now have to go back and rewrite those classes. The 'good development' practice we were so eager to learn has cost us valuable time.

Top

Less Is Better

Now imagine we're working on a limited schedule. We have half an hour between courses at school, and we want to get this square class working come hell or high water. Let's create a square class:

square
    var
        length = 1
    proc/area()
        return length * length
			

There, done. Might not be pretty, might not be future oriented, but it's exactly what the program needs at this point in time. "But wait!" you say, "Here's a situation where we do need rectangles; we should have programmed that geometry library after all!" Don't give up hope just yet, I have a solution. First, we're just going to program very "poorly".

// Here's our old square code
square
    var
        length = 1
    proc/area()
        return length * length

// Now we just cut and paste, and modify a bit
rectangle
    var
        length = 1
        width  = 1
    proc/area()
        return length * width
			

There, done. There is a lot which seems negative about what we just did, though. We had to hard code two classes, and our square really should be inheriting from rectangle. Even more importantly, it seems like we're reverting back into programming practices we learned the first day we started -- namely, cut, paste, modify, and forget. I've got an ace up my sleeve, though.

Top

Making Bad Code Good

Once we find that we've made three very similar classes (square, rectangle, and rhombus, for instance), we should go back and see if there's a way to make them all work with proper inheritance. That may sound a bit crazy if you're not used to working with the parent_type var, but trust me, it's simple.

First, we take a look at our classes and determine what is common to them all, and put all of that into an ancestor class with a temporary name.

square
    var/length = 1
    proc/area()
        return length * length

rectangle
    var
        length = 1
        width  = 1
    proc/area()
        return length * width

rhombus
    /* We strayed from our naming conventions here, but the variables and
        procs serve the same purpose, so we'll say that they're common. */
    var
        l = 1
        theta = 60
    proc/find_area()
        return rhombus_magic()

common
    var
        length = 1
    proc/area()
			

The next step is to delete the common traits from the classes, and make them all inherit from the common ancestor. We can do this by changing their type path (changing /square to /common/square, for instance) or we can use the parent_type variable. Generally, I would change the type path, but I'm going to use parent_type here so I don't have to do clean-up in other areas of the program.

common
    var
        length = 1
    proc/area()

square
    parent_type = /common
    area()
        return length**2

rectangle
    parent_type = /common
    var/width = 1
    area()
        return length * width

rhombus
    parent_type = /common
    var/theta = 60
    area()
        return rhombus_magic()
			

The third step is choosing a name for your common ancestor. This may seem trivial, but it's an important step. This ancestor is not just a container, but a simple class which you will probably find a use for elsewhere. In this case, the name for our ancestor should probably be 'square'. That's right, square isn't different from the common ancestor in any significant way, so we're going to make it the base of our inheritance tree. The final product will look something like this:

square
    var/length = 1
    proc/area()
        return length**2

rectangle
    parent_type = /square
    var/width = 1
    area()
        return length * width

rhombus
    parent_type = /square
    area()
        return rhombus_magic()
			

There, done. Now we have a geography system which makes sense, is programmed well, and (most importantly) works exactly the way our program needs it to. This geography 'mini-library' is guaranteed to work, because we made it to suit the exact needs of our program. Perhaps most importantly, though, we programmed this quickly. A game isn't a game if it's never made, and we can't spend all day programming squares when we have a combat system to write.

Before we're done here, let's look at adding a new class to our mini-library. Because we know that our square, rhombus, and rectangle classes all work perfectly in our program, we have a good basis for programming something like a parallelogram. I'd probably start with the rhombus (though here the rectangle is just as valid) and define a subtype with different sized sides. Because we're working from a proven base, the new object is almost guaranteed to have a future in our program.

Top

Form and Function Rethought

"But wait!" you say, "You've got this all wrong! Rectangles don't inherit from square; a square is a special type of rectangle." This may be true in geometry, but it doesn't make much sense in terms of programming a game. In your game, you're not going to be using squares and rectangles as much as things like NPCs and merchants. Merchants are like NPCs, but they have extra functionality; ie., they sell stuff. So we subtype merchants under /NPC.

Squares and rectangles have a similar relationship. They are both four sided figures with right angled corners and a variable which represents the length of one side. Rectangles are just further defined, with an extra variable which represents the width of another side. In the same way, a rhombus is a special instance of a square, where we have to redefine the square to take into account squares that have angles different from ninety degrees.

Redefine is the important word here. We start with the simplest thing possible, a square, and make a new class whenever we have to redefine it to make it work in a different situation. The opposite of this would be creating a polygon class with a variable for the number of sides, a list for the length of each side, and a list for the angle between any two sides, and then subtyping square (etc.) underneath polygon. In this case, the square wouldn't really be subtype of polygon (it wouldn't define any extra behavior), it would just be a dummied down polygon, or a polygon wearing a square's mask. If you're going to make a complete geometry for other people to use, it's probably a good idea to start with polygon and work up. If you're just interested in having simple trees, and maybe trees that drop fruit later on, the method described here is faster.

Top


IainPeregrine