The role of abstractions in programming. How to split chaos into modules?
The code for 4XG (a browser-based, Civilization-paced, MMO 4X game) used to be a tangled mass of functions. Under Eloquent Javascript's influence, I've begun to reorganize the Javascript into modules based on the doesn't care test. How can I make it so one piece of code doesn't care about what another piece of code is doing?
Here's my map for bringing modular order out of chaos:
BRIDGE --> STATIC LISTENERS
|
V
COMMAND --> AJAX WEBSOCKETS
\ \ \ /
\ \------> UPDATE DATA
\ \
\ BUILD ------> UPDATE UI
\ \
\ \ ------> DYNAMIC LISTENERS --> TOOLTIPS
\ \
\-> ANIMATE --------> DRAW MAP
Note: Static listeners are bound only once (on page load) to bridge elements. These listeners act independently of player state. When you click "pane-->quest..." or "grid" it always does the same thing.
Once BRIDGE is done, COMMAND does its thing.
1) COMMAND validates the input and sends it along to both AJAX and ANIMATE.
Note: the error-checking in the COMMAND module isn't perfect. For obscure errors, it's not worth it to check client-side. I just let the server-side error-catching get them.
2) AJAX sends the input to the server then gets the response back.
3) AJAX parses the response and puts it into a format suitable for UPDATE DATA to read. It waits for the animation to finish, then sends the data to UPDATE DATA.
4) UPDATE DATA updates the data
Note: if the input isn't valid, COMMAND skips AJAX and sends a message directly to UPDATE DATA containing a message to display to the player with what went wrong. UPDATE DATA has no idea whether or not it received the message from COMMAND or AJAX or WEBSOCKETS. It doesn't care.
5) UPDATE DATA sends a message to BUILD telling it which data has been updated.
Note: BUILD has no idea how the data was updated. It just knows that (for example) ships were updated.
6) BUILD sends a message to UPDATE UI telling it to update certain elements.
7) BUILD sends a message to DRAW MAP telling it to redraw certain objects.
8) BUILD sends a message to LISTENERS telling it to rebind certain dynamic listeners.
Note: Dynamic listeners are removed from and regenerated on the map again and again as the game state changes.
ANIMATE doesn't care what AJAX is doing. X times per second, it manipulates certain canvas layers then redraws them. (There are currently 4 canvas layers.) X times per second, it manipulates the SVG objects in some way.
All this time spent planning and refactoring equals time NOT spent shipping new features. The game should still look and play much like it does now:
I should say two words about why all this is necessary.
Right now the code is tightly coupled. The interfaces between the different parts of the code are not well-defined. When I change something, it often introduces bugs in a distant, unrelated part of the game.
I'm using modules to introduce more abstraction into my code. In order to think more concretely about abstraction (/cough), I use the doesn't care test. How can I make it so one piece of code doesn't care about what another piece of code is doing?
In my design:
And so on.
One reason the code is so tightly coupled is that I was overly concerned before about the performance cost of calling a new function and creating a new stack. Three things:
I started learning to code in earnest this past May, and started building web apps this past August. There has a been consistent thread over these past few months: I love Python. But Javascript? Not so much.
A key reason: Python and Django force you to use modules. Javascript does not. I've been adding functions and global variables to quickly implement each discrete individual feature. It worked well for awhile. But at this point, especially after seeing the code in Eloquent Javascript, something has to give.
I'm not sure when exactly its time for a novice Javascript programmer to begin thinking in terms of modules. If I hadn't read Eloquent Javascript, I probably would have kept toiling away without refactoring for a few more months. That would have sucked.
But before? Especially in those first few months? I was spending so much mental effort just trying to make my functions actually work: understand the syntax, see the control flow, keep track of scope, keep track of variables passed by reference versus value, etc.
Imagining how the function should interact with the rest of the codebase and where it should go is a whole 'nother league of programming.
I'm super excited to be crossing this bridge now. It feels right.
There's nothing quite like suffering through the wrong way of doing things to really appreciate the right way.
I'm a Pragmatist. I'm suspicious of abstractions that are not rooted in lived experience. In this case the lived experience is writing a lot of code. Now that I've done that, I'll think about those experiences. Then explore useful abstractions to understand and improve the code.
The alternative would be beginning with abstractions and write code that fits inside those abstractions, compelling subsequent lived experience to follow the contours of a theory. Which could save a lot of time if I chose a really great theory. But if not....
(P.S. Marty: all the while recognizing that the process of discovery--or in the case of programming and art, creation--existing abstractions (both conscious and unconscious) in the mind of the discoverer/creator (what utility does that distinction represent anyway?) are inextricably intertwined with the "concrete" data that results from the process of discovery and creation. The existing abstractions in the mind of the discoverer/creator are informed by previous interaction with other abstractions. Many abstractions that seem to "arise" from data--especially the ones that seem to occur organically/spontaneously--are embedded within the "found" data at the moment of its discovery/creation. A large number of the organic abstractions I am "finding" flow from the structure of the language itself, which was a human choice based on human abstractions. That structure channels the code I am writing toward certain abstractions. Which is one of the reasons to let the abstractions flow from the lived experience, rather than the other way round. Working against the basic structure of the programming language, the browser, the network, the server, etc. would suck.)