Why OOP?

by notthecheatr

OOP might be a really nice convention for programmers to adopt when writing their big projects that they're being paid to write, but why should a game programmer bother with it? This is essentially the question asked by someone recently rather off-topically in a discussion. Rather than start a heated debate on the pros and cons of OOP (though there were plenty of replies and it started getting there...) I'm going to try to answer that question here.

We start with the origin of procedural programming. In the beginning, you didn't have procedures. You basically had spaghetti code - along with capital letters, line numbers, and all that ugly stuff:


Code:
100 PRINT "I AM A DOOF."
200 GOTO 100

Although it doesn't matter much in a small program like that, it gets to be extremely difficult in large programs. Hard to read, etc. Even hard to write.

So programmers somewhere, one day, decided to do this:


Code:
Do
  Print "I am a Doof."
Loop

and furthermore, they even decided to do this:


Code:
Sub printDoof
  Print "I am a Doof."
End Sub

Do
  printDoof
Loop

How very structured of them! Now for some, it may have been easiest to stick with the GOTO's. Amazing though it may seem, there are still some programmers who use it. I don't know how they can write anything useful (and really try to debug, or make any modifications to it) that way, but some do. But for the most part, programmers today have adopted much more structured syntax. Now it's no more powerful - you can still write the exact same code with GOTOs as you can procedurally - but it's much easier for we humans to understand, debug, modify, maintenance, etc. So we use it.

With structured programming, someone eventually figured out a rather neat idea. You see, your Subs and Functions might tend to get kind of long:


Code:
Sub moveSprite (sprite As FB.Image Ptr, x As Integer, y As Integer, vx As Integer, vy As Integer, suchandsuch As uInteger, thisthatandtheother As Byte)
  '...
End Sub

See what I mean? You will be passing twenty different parameters to it, and eventually you realize it would be a lot easier if you could group these parameters together. So here comes the concept of the UDT. Put a bunch of variables in a single structure, like a package, then send the whole thing to the Sub, which means you only have to pass one (or maybe a couple) parameters, instead of twenty.


Code:
Type myUDT
  As Integer a, b, c, d, e, f, g, h, i, j
End Type

'Which do you like better?
Sub mySub1 (param1 As myUDT)
End Sub

Sub mySub2 (a As Integer, b As Integer, c As Integer, d As Integer ...)
End Sub

But there's another factor we need to look at. That is scope. In the beginning, everything was global. That means you would create a variable for your program


Code:
110 DIM A AS STRING = "I AM A DOOF."
120 PRINT A
130 GOTO 120

and the variable would be accessible everywhere. Of course, that was because you didn't even have procedures yet, so it was practically pointless to hide anything from any other part of the program. But once you had procedural programming, it became useful to make variables inside procedures that weren't necessarily accessible (in fact, didn't necessarily even exist) anywhere else. Then you could have a variable called X in your main program and a variable called X in your procedure and they would be entirely different variables, and neither could access the other. If the main program needed to give some information to the procedure that the procedure didn't have, it could simply pass a parameter to the procedure. But this didn't always work, so we still had some global variables lying around:


Code:
Dim Shared As Integer mynumber

Sub mySub
  Print mynumber
End Sub

mynumber = 3
mySub
mynumber = 5
mySub

Now the same variable name is the same variable whether inside the procedure or out. And many people thought this was great, because then you don't have to have a lot of parameters passed to and from your procedures (these people didn't know about UDTs yet, of course).

But there's just a little problem. What if you have a global variable and you have twenty different procedures that access it, maybe even modify it, and who knows what happens then? Maybe a bank program storing the balance of an account as a global variable. A bug is bound to happen sometime somewhere, with all that code, and you suddenly realize that the balance is off - way off. So which one of those twenty different procedures messed it up? And remember, this is a bank program, so it's probably made up of hundreds of lines of code, each of which might potentially access the account balance. That means hours of debugging for you. And in case you think I'm only talking about business programs - what about games? What if you store the sprite position x and y in global variables, you have twenty different procedures moving the sprite around and drawing it on the screen, and you suddenly notice the sprite is going all over the place on the screen and it's not where it's supposed to be? Then what? So which of those procedures did it?

Scope solves this. Only certain procedures can modify certain things, so you know which procedures to check and see if they modified it - and most of them don't, because you only let certain ones do it. So what if those procedures need to modify it? Then you pass things ByRef, and at least now you know which one did it. But that's not always safe, so we could run into problems there. Enter the UDT. It solves the problem of too many parameters, and since you pass it as a parameter to the procedures, you don't have the problems of global variables. Everything is in its proper scope, so you have one less thing to worry about. Unfortunately, if the procedures want to modify the variables inside the UDT, you have to pass it ByRef. And then you can have problems that way just like with global variables, because if you pass it ByRef to everything, you'll still have twenty procedures potentially modifying variables and messing things up. Now what? We need more scope rules!

This is what encapsulation is all about. You put the variables inside a UDT, but make some of them Private. What's that mean? That means nobody else can access those variables! They're hidden inside the UDT, so even if I pass the UDT to a procedure, the procedure can't access those variables. In fact, even the procedure (or main program) that creates the UDT can't access the variables:


Code:
Type myUDT
  Public:
    As Integer a
  Private:
    As Integer b
End Type

Dim something As myUDT

'This is OK
something.a = 3
Print something.a

'This is NOT OK
something.b = 5
Print something.b

But of course that begs the question who CAN access the variables? Because if nobody can access the variable at all, then what's the point? In a sprite, you might hide the x and y variables so nobody can just move the sprite around whenever they want. But if nobody can access the (x, y) variables, then how does ANYONE move the sprite? And how would we know where to draw it?

This is why we add things called methods to the UDT. It's the beginning of OOP. It may seem a little strange, but it really solves a lot of problems. Now only ONE procedure can modify the sprite's position, and if there's any bugs you know who did it!


Code:
Type mySprite
  Public:
    Declare Sub move (newx As Integer, newy As Integer)
    Declare Sub draw ()
  Private:
    As Integer x, y
End Type

Sub mySprite.move (newx As Integer, newy As Integer
  x = newx
  y = newy
End Sub

Sub mySprite.draw ()
  PSet (x, y), &hffffff
End Sub

Now you could just as easily do it like:


Code:
Type mySprite
    As Integer x, y
End Type

Sub moveSprite (tsprite As mySprite, newx As Integer, newy As Integer
  tsprite.x = newx
  tsprite.y = newy
End Sub

Sub drawSprite (tsprite As mySprite)
  PSet (tsprite.x, tsprite.y), &hffffff
End Sub

But then you lose the encapsulation and you still have scope issues because anyone else can access x and y. This example also illustrates something else we need to examine: style.

You might not think it matters, but it does. For example, although indentation doesn't really have any effect on what a program does, it's still a lot easier to read


Code:
If something = 1 Then
  Do
    Print "Hi"
  Loop While something = 1
End If

than


Code:
If something = 1 Then
Do
Print "Hi"
Loop While something = 1
End If

And there are other areas this applies. Now if you have a UDT and a bunch of procedures that deal with that UDT specifically, it eventually becomes logical that those subs should be part of that UDT, rather than separate entities. It also simulates real life; for example, in the corporate structure, your executive gives out tasks for his employees to do. The boss doesn't balance finances using the employee, he tells his employee to do it! So likewise it seems much more logical to tell the sprite to draw itself, rather than to tell a procedure to draw a sprite using the UDT that contains it. Or a map object, for example:


Code:
Type mapType
  As Integer x, y
  As Any Ptr tiles
End Type

Declare Sub loadMap (filename As String, map As mapType)
Declare Sub drawMap (map As mapType)
Declare Sub moveMap (map As mapType, x As Integer, y As Integer)

Notice anything about all these subs? They all have a parameter called "map As mapType"! But if you use OOP, you don't have that. In fact, you can just call the subs "map.load, map.move, and map.draw" instead of "loadMap, drawMap, and moveMap". Nor do you pass a map parameter to them; they're part of a map object, so they have a hidden parameter passed to them.

Hopefully you're getting an idea of why OOP makes sense. It's really just another abstraction which takes some ideas to the next level. UDTs don't exist in real life; neither do procedures for that matter. At the low level, the variables are separate, the procedures in the UDTs are not "part of" anything at all, and all procedures are really just Goto statements (actually, it's a little better than that but I won't go into the details). But OOP saves you from scoping problems, allows you to work much more easily, and makes your code look nicer and more logical too!

But OOP goes beyond making code nicer and more logical. It makes a lot of things simpler, and generally lets you do some very interesting things you otherwise couldn't do. For example, strings. Normally the + is used to add two numbers, but what if you want to use it to concatenate two strings? That's what operator overloading is for! If you make a string object, you can use the + operator for the object and it does something totally different! You can do this with pretty much all the other standard operators too.

And then there's RAII.

cha0s wrote a tutorial about RAII too, but some people might not understand it so well. What it basically means is, when you create an object the object keeps track of all the resources it needs. Take for example a buffer in memory. If you create a buffer (say, to store your map data) then you need to use a pointer, and you need to Allocate/ReAllocate/DeAllocate. Now what if you forget to DeAllocate? You may eventually get memory leaks and problems! But if you put it inside an object, the object will automatically allocate the memory it needs when it needs it, and as soon as the object is destroyed (which will be at the end of the program or at the end of the procedure it's created in, depending on the scope) it will automatically deallocate the memory, thus ensuring you won't get any leaks (or wasted memory) by forgetting to deallocate a buffer when you're done with it.

This is done with Constructors and Destructors, which are two special kinds of procedures that are called automatically as soon as the object is created or destroyed. The other nice thing about them is that they let you set the object up, make sure the object is valid before you call any other methods. For example, you might want to draw your map, but what if you forgot to load it? Then the pointer to the buffer containing the map data is invalid and when you try to draw the map your program will likely crash. But if you have a Constructor, which gets called automatically no matter what, then it can set a special flag inside the object explaining that the map has not been loaded so it shouldn't be used. You can also allocate the pointer, so when the map loading routine needs to reallocate there's something to reallocate (and of course the destructor will deallocate it):


Code:
Type mapType
  Public:
    Declare Constructor ()
    Declare Destructor ()

    Declare Sub load (filename As String)
    Declare Sub draw ()
    Declare Sub move (newx As Integer, newy As Integer)
  Private:
    As uByte Ptr _map_data
    As uInteger _map_width, map_height
    As Integer _x, _y
End Type

Now if we were to call draw() without a constructor, it would check the buffer at _map_data for tiles to draw. But the map hasn't been loaded yet, so the buffer is empty! This could very well cause a crash! But if the constructor is used, the constructor sets everything up ahead of time so no errors can occur.

Now of course in practice, you would be very careful not to call draw() until you call load(). But if you forget, or make a mistake, setting things up this way can save you a lot of trouble.

Another thing OOP lets you do is Properties. Now as I said earlier we want to hide all the variables inside the object where nobody else can access them, because otherwise we can have problems. But always using a sub to access the variables may seem rather uncomforable. This is why we use properties. They seem like variables, and they act just like variables - for example, if x is a property of myUDT, then you can do


Code:
myUDT.x = 3
Print myUDT.x

but in reality, properties are actually a special kind of procedure, meaning when you try to modify a variable you aren't just directly modifying it, you're actually instructing the object to modify it. This way, for example if you had a property map.x the map could redraw itself automatically when you modify map.x. This wouldn't happen if you tried to modify the variable directly, but since a property is really a sub in disguise you can do that. This way, you avoid the scope problems but you don't have to use the rather uncomfortable sub just to modify things.

Of course, how you do things is entirely up to you. It might be easier at first not to use properties, operators, or even constructors and destructors. These things are just special extras that come with the OOP paradigm. You may want to start out just using methods and keeping all variables Private. It's still much safer and smarter than using UDTs with everything public. And you'll start to get a feel for OOP, and maybe even learn to enjoy it and prefer it to just plain procedural programming.

OOP is not the be-all and end-all solution to everything. I don't use OOP everywhere, by any means - but I do use it whenever I can, because it can save me a lot of trouble and make sure I do things right. And by the way, OOP and procedural programming can be done side-by-side. As I said, I don't use OOP for everything - for some things, a plain old-fashioned function or sub will work just fine. But when it's possible, when it's useful, when it makes things easier, safer, and cleaner, I use OOP.