Tuesday, September 3, 2013

First Level Abstractions

The Situation:

When I was much younger, I had written a simple program that was designed to have a couple of text characters (as in 'people' as well as literal ascii letters) on the screen who moved upon a particular event.  I had another character who you controlled using the keyboard.  The character you controlled, the good guy, was chased by the 'AI' of the other characters, the bad guys.  The characters would all chase down the good guy, but for some reason, if the good guy was in a location relative to the bad guy (say he was below and to the left of the bad guy), the bad guy failed to chase him.  The system used a x-coordinate and a y-coordinate that I had to keep track of the location for each bad guy and one set of coordinates for the single good guy on the screen.  Now I want to give a little pseudo code to give you an idea of what this looked like.

The Code:


if(badguy[1]_X<goodguy_X) {
    if(badguy[1]_Y>goodguyY) {
    //... do something that is a bug.
   }
 }
 if(badguy[2]_X<goodguy_X) {
     if(badguy[2]_Y>goodguyY) {
     //... do something that is a bug.
    }
 }
 if(badguy[3]_X<goodguy_X) {
     if(badguy[3]_Y>goodguyY) {
       //... do something that is a bug.
    }
 }
 if(badguy[4]_X<goodguy_X) {
     if(badguy[4]_Y>goodguyY) {
      //... do something that is a bug.
    }
 }
 if(badguy[5]_X<goodguy_X) {
     if(badguy[5]_Y>goodguyY) {
       //... do something that is a bug.
    }
 }

What I Knew:

Now keep in mind, my game used loops and arrays.  My main game loop was something like: do { /* game */ } while(key!='q');

So clearly I had some understanding of loops, but I didn't get how loops and arrays could work together.  I understood that "I could just fix the code via find and replace."  However, when the code gets complex, find and replace started to fail me.  I understood that I could in fact loop, but how would a do/while or while loop help me?  I thought for-loops were silly... why would I ever want to count up like how  a for loop structure would want you to.  Heck, why would you use that confusing for(X;Y;Z) when you only need to evaluate a boolean at the end or beginning of a loop.

I think the problem ultimately stems from a lack of understanding abstractions.  To me, I needed no abstractions, I had 5 concrete bad guys, each whom had the same behaviour.  If I wanted a 6th bad guy, I would simply need a 6th copy and paste.  Arrays to me were a way of not having to write out the same variable 6 times, which was nice, but they were not a 'collection' of data but rather individual pieces of data each unique unto themselves.  The tie between arrays and for loops had not even occurred to me.   Then I went to fix a bug in my code and kept having to fix it over and over again...  and found I had only fixed 4 out of the 5 bad guys and I thought...

 'There has to be be a better way!'

Abstractions I find often come from that conclusion.  Often abstractions actually mean writing more code and adding more complexity at first was odd, but is not shocking anymore.  Ultimately, abstractions will save in code (in that you don't repeat yourself) and likely will save in maintenance at the cost of complexity.  So lets consider my example again.  How was it written?
  1. Write the logic: if (badguy[1])  ...
  2. <copy> & <paste>
  3. replace [1] with [2]
  4. <copy> & <paste>
  5. replace [1] with [3]
  6. ... up to [5]
  7. test game
  8. debug game, and find error
  9. fix all 5 places that the error exists in
  10. test game and find bug still exists with 1 / 5 bad guys
  11. debug game and find error
  12. fix in the one place copy and paste failed
  13. test game
Now how should that code be written?

for(int i = 1 to 5) {
if(badguy[i]_XgoodguyY) {
//... do something that is a bug.
}
}
}

Once you have this really nice piece of code written out, you can see the abstraction.  We don't have 5 individual bad guys, each with his or her own set of logic but rather 5 bad guys with 1 set of logic.  The bad guy count didn't change but the logic now exists once. Every time you write code, you are using abstractions, but when you avoid copying and pasting, your probably adding at least one more layer. As a programmer, I don't even really see them as abstractions anymore, but they are there.  So what do we have now as far as steps go to create and fix that bug?
  1. Write the logic: if (badguy[1])  ...
  2. reconsider and then refactor the logic to make it a loop
  3. test game
  4. debug game, and find error
  5. fix one place that the error exists in
  6. test game

The Takeaway:

Clearly the abstraction made the steps easier, even if I had to think a little harder to get there.  However, the nice thing is, once you have this new tool in your tool box, you can start seeing patterns.  Duplication is a bad thing.  Lets consider my loop code again:

for(int i = 1 to 5) { /* Logic */ }

What happens when we want to change the number of bad guys?  Well now we have to change at least 3 places.  The badguy_x[], badguy_y[] definitions and the "to 5" part of the for loop.  How do we fix that?  Well maybe we make a BADGUYCOUNT constant.  Maybe we detect the size of the badguy_x size and assume x and y are always the same.  What about creating a badguy object?  What about ArrayLists?  It became rapidly clear that I could solve this multiple ways, but the solution wasn't the important part, the important part was that I started to be able to detect problems in my code before they really became a problem.  Later I learned the term for this was code smell.  Detecting code smells is something that can be taught to some degree, but ultimately I think it is something that is learned from doing.  So next time your in your code and something keeps breaking, the thing you should start thinking is 'There has to be be a better way!' and start searching for it.

No comments:

Post a Comment