Categories
Game Development

Loose coupling & tight cohesion

About 3 years ago I read “Code Complete.” One of the programming tips in the book is to make your functions with loose coupling and tight cohesion. Loose coupling means the input and output to your function are of types that are more generic, rather than more specific. For example, an int is more generic […]

About 3 years ago I read “Code Complete.” One of the programming tips in the book is to make your functions with loose coupling and tight cohesion.

Loose coupling means the input and output to your function are of types that are more generic, rather than more specific. For example, an int is more generic than a UINT which is more generic than MyIntClass. A function which takes an XML file is more generic than one that takes your custom proprietary format, but is less generic than a pointer to the data in the XML file.

Tight cohesion means your function does one thing, rather than many things. Joel on Software refers to this as leaky abstractions, although he was talking about API code, it’s the same principle. A function which sends a file between two systems has tight cohesion. A function which sends data between two systems has even tighter cohesion.

Architecture design, which is what loose coupling and tight cohesion is a guiding principle of, is something you learn programming architecture. Most programmers don’t know how to design architecture – not because they are bad programmers but because most programmers don’t get to write architecture and the only way you learn is by doing. You have to be in the position of writing large programs that evolve over time and have complex parts that have interrelations and work together. This is not something you learn by simply writing a large program. It’s something you learn when your large program doesn’t extend well, or you have to rewrite systems that, or your users complain about bugs and 6 months later you can’t figure out what you wrote the first time.

This is why when I read “Code Complete” 3 years ago I understood the words and the reasoning behind loose coupling and tight cohesion but I wasn’t able to put it into practice. Even though I had been programming for years already, including some small games, I had never written a large enough system to practice these skills. RakNet is finally getting large enough (60K lines of code, excluding comments and dependencies) to where I’m learning real architecture and have some comments on this principle.

The first is that it’s easy to see when you do it wrong. When your coupling is too tight, this means your function (or class) can take one type of data, where it should be able to take another type of data that is otherwise perfectly valid. Or it takes several types of data and performs conversions between them. For example, I want to use the compression of bzip2 and the binary deltas of bsdiff. Compression and binary deltas should just take an array of bytes and a length, along with some optional parameters. However, the implementations of these tools assume your data is on a file on disk. Furthermore, they are written such that they are tightly coupled to running from a main() function, rather than implemented as a set of functions that take a structure of data and happen to run from main. So now I have several hours of headache in front of me to figure out how to decouple these tools from main and from assuming they work on disk files. I’ll have to write C++ wrapper to do this and a test-bed to make sure I got it right and so forth. A big waste of time.

An example of poor cohesion was the last autopatcher I wrote, which is part of the reason why I’m rewriting it now. The last autopatcher I wrote did quite a few things:
1. Binary diff on a set of files on one computer
2. Transfer that diff in a message representing a set of diffs for a set of files to another computer
3. Process that set and find the differences on the local computer
4. Generate a list of files based on those differences
5. Compress those files
6. Transfer those files back to the requester
7. Unpack and write those files to disk, creating directories when needed.

It did all these things, but the problem was all this functionality was contained in one class. So later on, when I just wanted to transfer a set of files and write them to disk on another computer, I basically had to do it by hand. Another time, when I wanted to compress a set of files, again I had to do it by hand. Sure, I could have looked at the code I had and copy / pasted to make things faster, but it wasn’t like I had a nice function I could just pull out (the functionality was too tightly coupled).

An architecture with good cohesion, which happens to be the way I’m rewriting it, finds the the set of minimal functional units that comprise an activity. One functional unit is generating a list of files. Another is writing a list of files to disk, taking a list of files as input. Another is transmitting that list of files over the network. Another is compressing that list of files. And so on. So that later on, when I want to use a list of files for a different purpose, I have the code already there and it didn’t matter that I originally wrote it for an autopatcher.

How do you know if you’re writing good architecture? If you didn’t have to spend some serious time thinking about the architecture, coming up with solutions, and discarding those solutions for better solutions, then you probably aren’t. Designing good architecture is hard because you are programming (in your head) in general and solving unknown problems. If you spend an hour changing the functions around for header files you are probably working on architecture. If you are writing code, you are probably not.

A good indicator of when you are using good architecture is that you are able to build up on previous successes and so accomplish a lot with much less work than if you were to write your solution from scratch. A trivial example is where you wanted to display an ordered list of names. One way to do it is to allocate an array, sort this array, and print out the names. Another way to do it is to use a previously written ordered list class that takes a template for your type, then iterate through that list. Another way is to use the same ordered list class, but extend it with a print function, and then just call that.

If you ever encounter the scenario where you have a huge problem to solve and do so with surprising ease because of something you wrote in the past you are benefiting from good architecture.

Design Patterns is a good reference to read about this subject.

Leave a Reply

Your email address will not be published. Required fields are marked *