The following is a set of principles which have proven to be helpful in developing robust, elegant, and maintainable software. These are not hard and fast rules. However, their use is strongly encouraged as their benefits are great.
The common idea behind almost all of these principles is to tame complexity. In addition, they strive to avoid the common pitfalls associated with distributed teams of developers and keep the software we write “nimble”.
I created this document as a wiki page since I consider it to be a living document. There are other valuable principles not listed here. Feel free to add any you feel are missing.
Again, these are more guiding principles.
- Data should live in ONE location and ONE location only. This is the most important principle so it deserves to be first. Data synchronization should be avoided at (almost) all costs, unless it is absolutely necessary. Usually, the only times is it necessary is if you are caching data from some remote location.The benefits of this principle can’t be overstated. Besides the obvious wastefulness of storing data twice, duplicated data now has to be kept in synchronization. Synchronization is difficult, even when it is a specific functional requirement and there is all sorts of machinery in place to support it. Ambiguity often results when dealing with duplicate data.Synchronization takes computing time and resources. More importantly, it consumes developer time and resources. It creates needless complexity that must be maintained.
- Work happens in ONE location and ONE location only. This one is somewhat related to #1. This means that if you find yourself copying and pasting a segment of code, it is highly likely that the piece of code can be moved into a separate function common to both locations.The obvious benefit of this is that when you need to make a change, either for a bug-fix or an enhancement, you will only need to make the change in one place. You don’t have to remember to change the other copy of the code.
- Minimize the number of things that must be “remembered”. If you are designing or writing code and you find yourself thinking: “I will need to remember this” or “I will need to remember to do this”, stop what you are doing and come up with a better way of achieving whatever you are trying to achieve. You should almost always find a better way where nobody needs to remember, or keep track of, anything.Well designed code should be easy to use. There should be a minimum number of “rules” one must know about and keep in mind when working with a set of code, and that is it. There are already a tremendous number of things we, as software developers, must remember and keep in mind as we code, especially when dealing with complex systems. It is almost impossible to keep them all in mind as it is. Adding to the list doesn’t help.Think about how many times you have made a simple change or fix, only to realize you inadvertently broke something else. More often than not, you wrote the broken code and forgot about it, or somebody else wrote it and didn’t think that others needed to know about it. This happens ALL the time.
Note that documentation only slightly mitigates this problem. You may not even know about the existence of the code you just broke, let alone the documentation. New developers almost certainly haven’t seen the code or read the documentation.
Unit tests also mitigate this problem in that they help catch these things that break. However, the issue of ever increasing complexity, which is what we are trying to avoid, only increases if this principle isn’t held.
- Strive for simplicity and elegance. This one is a little trickier to get right. It often involves finding a subtle balance and getting things to just “feel” right. And by feel right, I’m not talking about the religious stances we take to certain programming paradigms. I am talking about the feeling you get when something is simple, elegant, and well designed.Simplicity is a tough thing. The idea is to make things as simple as possible, but not any simpler (I’m sure you’ve all heard this before). This is where the balancing act comes in… trying to made code that is highly functional, is robust, and easy to use.
- Don’t over design your code. Design patterns are wonderful things, very useful in solving common problems and making code robust and elegant. However, that is only true when they are used in situations where there is a clear benefit to using them. For instance, using the PIMPL paradigm when there is no clear benefit only adds a layer of indirection which must be maintained and followed when debugging and trying to understand code, for almost no gain. The same can be said of things like “The Visitor Pattern”.Design patterns often add a little complexity to tame a larger complex problem, or to enforce some order. Their judicious use often achieves this goal. Their indiscriminate use often adds complexity for no gain.
- Have a target. Imagine driving in your car to go somewhere and not knowing exactly where you are going, hoping you’ll magically find it. Or imagine building a house without a blueprint, hoping it will all just work out. Crazy, isn’t it? This one seems so obvious that it is almost not worth mentioning. Sadly, it is a principle which is ignored too often.Know what you are coding towards. Have a clear idea of what the end result is supposed to be, before you write a single line of code. If you can’t because you don’t have enough information from your manager, stop coding and go talk to him/her.
- Code should be easy to follow and understand. Strive to make your code so simple that somebody walking up to your code could figure out what is going on in under 10 seconds. Ideally, they could figure out what is going on upon first gazing at your code. If after a few months you revisit your code and can’t figure out what is going on, then you have probably violated this principle.In practice following this principle means using descriptive variable/function names, writing short, simple (i.e. non-complex) functions, and not pulling any “magic tricks”. Code should flow logically. Magic tricks can be useful, but shouldn’t be a part of the daily development process. When complex code is deemed necessary for whatever reason (performance, backwards compatibility, etc.), a good paragraph of comments/documentation describing the situation helps tremendously.
- Strive for consistency. Well designed frameworks all tend to have at least one thing in common. Their interface is consistent. Both the .Net framework and Qt libraries are prime examples of this principle in action. Common operations among otherwise disparate classes have identical names. Casing is consistent. Naming style is consistent.The benefit from a consistent set of class interfaces is that you don’t have to go look at the documentation for a class to figure out how to use it. Classes with similar interfaces should obviously be used similarly (unless otherwise stated).As for the code itself, consistency in coding style helps developers follow and understand code more easily since it will immediately be more familiar to them, even if it isn’t code they have encountered before.
- Coding doesn’t end when a simple test case passes. Often, when coding some new feature, we have some simple test data/case which we use to test if our new code works. Once we find that the simple test case works, we say “I’m done”, commit our changes, and pass off to QA. We’re always amazed when QA comes back to us within 2 minutes of testing indicating that things don’t work… at all.Of course, they are likely testing with different data than you, went through different steps to get to the point of testing their data, took different steps to test their data, had settings/environments/machines that were configured differently than you, etc… you get the point.Your simple test case is just that, nothing more. When your simple test case passes, your job is about 20% done. The rest of the 80% of time is going to be spent handling everything else.
- Error conditions. Never assume that your code will be accessed in one way or even in the way that you envisioned it would. If you want to ensure that your code is used a certain way, make damn sure, through the use of exceptions, asserts, rocket flares, whatever, that when your code is accessed, everything is “just right”.
- Edge cases. These aren’t really even edge cases at all… but the way different “normal” users will go about using your beautifully designed code.
- Data more complex than your simple test case.
10. Refactor code as you go. Entropy is natural and inevitable. Things tend to become disordered over time unless energy is put in to restore order. The more time has passed, the more disorder accumulates. Large scale disorder, especially among complex systems, is MUCH harder to “reorder” than slight or moderate disorder.
Refactoring should be an ongoing process, not something that is done in one huge overhaul after a project has become so dismally painful to work with that making even the slightest change is more dreadful than getting a swift kick in your cajones.
Think about it this way. You can either wait until your bedroom is so messy it takes you a whole weekend to clean it up, or you can pick up after yourself as you go along. This way, you have your weekends to do what you want!
11. Keep GUI’s as thin as possible. All business logic should exist in core components. The role of the GUI should be to present core data to the user, and to allow the user to modify the core data using interfaces supplied by the core. You should be able to replace the GUI of an application with minimal effort. Replacing the GUI with a command line interface should be a one week task, at most. If you were to throw away an application’s GUI, the only thing that should be missing is a window and some mouse and keyboard interactions.
12. Write Unit Tests, even when you think you don’t need them. You will thank yourself someday. If you don’t, somebody else will thank you.
Unit tests have a number of benefits.
- If written before writing your class, they force you to think about the interface and usage of your class. This, more often than not, will expose “devilish details” you hadn’t thought of when you first sat down and designed your beautiful framework.
- Unit tests allow you to make changes, sometimes even largescale changes, with confidence. You will know immediately what your changes break, long before code is ever released and some poor user is cursing your name after losing half of their work.
13. Classes should have a minimal interface. You probably heard this in your beginners coding classes, but it is a strong and valid point. You can think of a class as representing some real world object. Usually, real world objects have simple interfaces. A radio has very few elements. It has an on/off switch, volume and channel dials, and a speaker. To operate a radio, you only need to know to turn it on or off, find the station you are interested in, adjust the volume, and listen. You don’t need to have a degree in physics or electrical engineering to be able to use one.
This is how the interfaces to your classes should be designed… with the radio in mind… minimal and simple.
This is one thing that COM got right. Its objects expose interfaces, and nothing else. The interfaces define a small set of behaviors, and that is guaranteed to be true as long as you use the interface. To use a COM object, you don’t need to know about the data members it has, how it does what it does, etc. COM got a lot of things wrong too, but that is a story for another day.
14. Avoid circular dependencies. Don’t go nuts with this one. Sometimes it just makes sense to have things know about each other. But in practice it is a good principle to keep in mind. Circular dependencies can often be avoided by moving the common dependency into its own class/module. Again, it is easy to go overkill on this and have a trillion tiny libraries, which makes code hard to follow and causes link times balloon, and then you end up with a net effect of lost time.
15. Keep up with code changes made by others. Be aware of what is going on outside of the island you happen to be currently inhabiting. It is likely that you will one day need to fix a house built by somebody else on another island across the world.
What does this mean? Listen to what other people are working on during daily standups. Keep track of code changes made by others when you do an SVN update. See what has changed and why.
Besides making it easier for you to work on more parts of the codebase, you will tend to become a “master” of the entire codebase much faster.
16. Don’t start optimizing too early. Often you don’t know what the bottlenecks will be up front when you are designing your modules. You can guess and spend a whole lot of time optimizing something that doesn’t need optimization. Or you can get your module working and optimize the bottlenecks that emerge later. Things that obviously have strong performance consequences should be optimized early on. But don’t optimize things for maximum efficiency if there is no need.
Note that this is not the same as “don’t think about performance”. It is just as easy to die from a thousand cuts as it is from one huge gash. Don’t completely ignore performance. Know, for instance, that trillions of constant memory allocations will bring the application to a grinding halt. Don’t be lazy and forget about this consideration altogether. However, don’t sweat if some segment of code will do lots of allocations but not have any impact on the application as a whole because it is rarely executed and still executes “quick enough”.
17. Agile programming principles. There are some real gems in the “official” Agile programming book. Pick it up and become familiar with them. Here are a few:
- Classes should be open to extension, but closed for modification
- Single responsibility rule
- Principle of least surprise.