Ok several things again.
Object Orientation is not a tool to Make Complexity Go Away, as you have already realised. It is a tool to think about code in a certain way. If the problem you are trying to resolve happens to align well with object orientation, then code will be easier to write in an Object-oriented fashion. But not all problems align well with OO (some concepts have this nasty tendency to touch several orthogonal categories).
Other paradigms are different ways to think about problems. If you just need a set of steps, maybe an iterative code might be simple. If your problem can be specified as a set of processes working on what other processes produce, without changing much of state, maybe a functional approach will be better. And so on.
The programming paradigm you use is about the
quality of the problem you solve. You however have a
quantity problem. You have lots of stuff. And you need to manage that.
The bad news is that it's not an easy problem to solve. Books are written on the matter. Careers are built around it. As a result, I can only give you some pointers and rules-of-thumb.
The good news is that these rules can work in parallel with pretty much any programming paradigm that you use.
First:
Be able to test your changes quickly. For example: instead of changing all your Units code to OO in one go, change just one unit to OO, and make the code keep working. You might need to write some scaffolding code in order to make this work (eg. something that says "if this unit is OO then do this else do-old-stuff"). If it allows you to test the game faster, it is totally worth it.
Second:
Save often. This basically means using a version control system, like git. Every time you do one of those little changes I mentioned on the first rule, and you have tested that they work, make a commit. Then make the code prettier, and make another commit. If you get to a point where you can't solve an issue, you can always go back. If you are not sure about a change that you are going to make, create a branch to experiment on it, so you can throw it away. Learn at least the basics of git
Third:
Be coherent. The example that you put before about adding Units to Armies: It doesn't matter what you choose. Well, it in some cases it does. But what matters most is that you are coherent. Pick one.
And then make sure you always use that one. Don't have army:add(unit) in one place and button:insertInto(window) in another. If necessary, document the decisions you take in a text file ("when adding foos to a collection, I shall always do collection:addFoo(foo)")
Fourth:
Name things properly (and coherently!). This isn't so difficult as it seems. Sometimes a good name doesn't come in the first minute. Fine, spend more minutes. Open a thesaurus if you need to. It's time well spent. If you want to read more about naming things, I think the first chapter of
Clean Code is very good.
Fifth:
Make things as local as possible. Certainly your global space should not be polluted with lots of stuff. But you should not stop there.
Every scope should "know" as little as possible. This means that it might not be not enough putting all the player-related functions into a Player class (or a player module). Go deeper. Consider that maybe the player-movement functions and the player-scoring functions should not be in the same place (maybe move the score ones to a different Score module/class?)
Sixth:
Be mindful of the language's boundaries. Boundaries here means "concepts that can be used as separators" when dealing with complexity. In Lua, these are files, functions (with their closures) & tables. If you use Middleclass, you'll also get classes, instances and mixins, which in reality are types of tables, but are logically different. Be "aware" every time that you use one boundary ("why am I creating this function?"). And at the same time, be ready to put a boundary when necessary ("should I split this into a function?"). Read rule 3 again.
Seventh:
Put boundaries between levels of abstraction. You should not be dealing with planets, continents, countries, cities, buildings, rooms, people, body parts and cells in the same piece of code. Ideally every piece of code should deal with 1 level. It's ok to deal with 2 levels when they touch (eg. parent-child relationships) but 3 should raise your eyebrow, and 4 is a no-no.
Eighth:
Use composition. If you have something that is "a-x-and-a-y-and-a-z", try transforming it into something that "has a x, has a y, and has a z". For example, instead of giving your player a mass of 50, a position (x=100,y=200), a score(0), a list of items(empty), and healthpoints(10), along with all the methods and functions that depend/modify those, consider giving your player a body (with healthpoints) which in turn will have a physical_body (where you will put the mass & the position). Also give your player inventory (where the items will be stored). And maybe leave the score inside the player. Then move the correspondent methods out of player and into the classes/modules of body, physical_body and inventory.
Ninth:
Pure function > function with explicit dependencies > the rest of the functions > too little functions. A function with explicit dependencies is a function that only "works" with its parameters. It doesn't use global variables. This means that if you call it with the same parameters, you will get the same result. A pure function is a function with explicit parameters which does not change any of them (probably it returns a calculation). Here are 3 versions of the same code, in ascending order:
Code: Select all
-- level 1: no function, changes/uses 2 globals
bullets[bullets + 1] = {x=player.x, y=player.y, math.atan2(player.x - t.x, player.y - t.y)}
-- level 2: A function, with better names(target, angle) changes/uses 2 globals
local function shootAt(target)
local angle = math.atan2(player.x - target.x, player.y - target.y)
bullets[bullets + 1] = {x=player.x, y=player.y, angle=angle}
end
-- level 3: A function with explicit dependencies, modifies "bullets"
local function shootAt(origin, target, bullets)
local angle = math.atan2(origin.x - target.x, origin.y - target.y)
bullets[bullets + 1] = {x=origin.x, y=origin.y, angle=angle}
end
-- level 4: 1 Pure functions and 1 (smaller) impure function
local function getAngleBetweenPoints(x1,y1,x2,y2)
return math.atan2(x1-x2, y1-y2)
end
local shootAt(origin, target, bullets)
local angle = getAngleBetweenPoints(origin.x, origin.y, target.x, target.y)
bullets[#bullets+1] {x=origin.x, y=origin.y, angle=angle}
end
It is not always possible to make functions completely pure, but it is often possible to make their dependencies explicit ('self' is an explicit dependency in instance methods when using middleclass). Even when it's not possible to make a function pure, it is often possible to make parts of it pure.
Tenth:
Involve others. Put your code in front of other people's eyes as soon as you can. Even if it's "too dirty and inaccessible". The worst thing that can happen is that no one will read it. But if you are lucky someone will. And even give you valuable feedback (there's lots of weird people out there). Put it on github, bitbucket, or whatever you prefer, as soon as possible!
That is all I have for now. Sorry for the wall of text, and good luck!