Many of the code problems come from airiness during writing. We add features to the game not exactly in the best place with the thought that we’ll improve it later (yeah, sure…). And after a while, we don’t know which class does what. Maybe some classes do a bit of everything. We don’t know where to add another feature, so we pick a random class that already has the most of the needed references. Classes become just files with loosely connected methods and fields. Soon enough you’ll find yourself in a place, where you’ll have to change the current functionality. And that’s when everything fucks up. If this scenario sounds somehow familiar, read on.
Abstraction and the unicorn
ADT (abstract data type) is a set of data and methods that operate on it and that represents a single entity. Nowadays, in object oriented programming, an ADT and a class may be considered equivalent concepts. More or less. An ADT is something that you can find a ‘real’ object. A player, an enemy soldier, a unicorn mount – these are ADTs. However, sometimes you can see a class that is called a
Unicorn, but it also has got public methods like
ReadUnicornPositionFromServer() or public fields like
Color unicornColors. How often do you see a unicorn that connects to the server or manages arrays? Exactly. That is, because the unicorn simply doesn’t do that, or it is very efficient in hiding its secrets. You should get rid of these unicorn inadequate behaviors or change the class’ name to
ServerUnicornArrayManager. Despite the ugliness of that name, it also implies that the class may be doing too much.
Okay, but why should you bother about this that much? Writing classes is simple, right? Right. But writing coherent classes, that are ADTs and that help you solving dilemmas mentioned in the title is not given for every class once and for eternity. It requires discipline and attention during implementation and making changes. Why do you write classes? The main reason is to use a class in other parts of the game without thinking what happens inside. You implement ADT using classes to operate on a higher level. To use a unicorn as a unicorn, not pile of bones, skin, horn etc. But on long terms, it works that way only if the class is coherent and hides its implementation details.
The class is coherent if its data (fields) describes one and only one real entity, its procedures (methods) manage this data and the interface (public methods and data) allows you to do everything that you should be able to do and nothing more. Like the mentioned unicorn. It may be able to connect to the server, but when you’re flying on it (or using interface of the Unicorn class), you cannot see that. You can feed the unicorn, but you cannot see its digestive processes because it doesn’t help you play with it and it’s probably disgusting or hard to understand. And that is exactly like a good class should look like. It should allow you to do every freaking cool stuff it is designed to do, but protect you from the stuff you don’t want to know and you definitely shouldn’t interfere with. So…
ADT is a single entity presented as data and methods that operate on it.
Class is an ADT when it is coherent and hides everything that should be hidden.
Hiding (behind the private element and an abstraction) is called hermetization. It protects the class from incorrect use and the class’ user from details a headache.
Abstraction and hermetization are really good friends. It is impossible to have good abstraction when all the implementation details are known outside the class.
Be a hamster – hide everything you can
You don’t use
player.health directly to heal or hurt the player. If something will change in the future, for example, you’ll add a temporary immortality, you’ll have to find and change all of the field’s uses to take that into account. You provide an interface to manage the player’s health. If you add the immortality, you’ll have to consider it only in the player’s class, not in the entire game. Changes are less invasive. And when you’re using the class, you don’t have to think about details, you operate on a higher level.
Keep an abstraction consistent
Good interface should also be restricted to only one level of abstraction. That means when you’re using
cat.TakeDamage(), you shouldn’t be able to use
cat.livesNumber -= 1. It is just a ticking bomb and makes the reasoning about the class a lot harder.
Difficult problem, simple solution
When you already know all of this, let’s present that easy solution. It is just a simple question that should be asked before making EVERY change to the class. ‘SHOULD it be here?’ Yeah, that’s it. Don’t tell me you feel disappointed! This question triggers special mechanism in your brain, that is being forced to figure out if the new change will keep abstraction coherent and the hermetization on the proper level.
- Should the
healthbe a public field? Am I sure that class’ users (clients) can ensure that the
health, at any point, is over 0 and below or equal to its maximum level? What will I have to do if I change the
healthfrom being a float to being an integer?
- Should a
GetPlayerID()method be in a tank’s interface? Should the tank know about the player? Is the tank the one that uses a player, or is the player the one that uses the tank?
- Should a
DeleteKey()public method? Will the clients know for sure what the key is? Is the key on a proper abstraction level? Maybe the
GunsMenushould operate on a
GunEntryinstead of the mysterious key? Maybe the
GunEntriescollection should be private for the
If you are not able to say what abstraction the class represents, or having its name you can see methods and data that ‘shouldn’t be there’, it is probably a good idea to split that class into two or more separate abstractions.
On the other hand, sometimes you’ll find two classes that very often send the data between each other. Maybe both of them have a reference to each other. Or when you need to add another functionality and you are not sure to which class of these two you should add it, because it seems to fit them both. These may be good premises to combine those classes into one, as they may be two halves of the same apple.
Speaking of objects…
Object-oriented programming is wonderful paradigm with many rules and guidelines. It already has proven its value for the game programming. But by creating objects that are weird inconsistent masses instead of real coherent objects we reject all of these rules. The class is one of the most basic units we use. If you undermine this foundation it will be impossible to construct something magnificent or even stable. Ignore the problem of incoherent abstraction and keep building on that and at one floor the building will fall. Or it will stay under construction forever.
More information on the topic:
Code Complete 2nd by Steve McConnell, Chapter 6 “Working Classes”.
I strongly recommend the whole book (not only me, look on the Internet). It was a big revelation for me.
There you go! At the beginning, I’m trying to create ‘vertical slice’ of the blog. That is, I want to show you the real samples instead of only promises of the content. Thank you for coming by, I hope you found this article valuable. Tell me what you think, I need your feedback. It will also give me the motivation to keep going. I’m looking forward to hearing from you in the comments. And see you in the next post!