You know, there is a lot to be said for software that has a lot of features. Every piece of software we use has key features that, if missing, would almost certainly keep us from using the software. For example, one of my favorite pieces of software, Beyond Compare, would be almost completely useless to me if it did not have a “compare folders” feature. This feature for me would be a deal breaker if it were not there.
Sure, you could make file comparison software that did not compare folders, but for me that would almost certainly keep me from using your software. This feature is a common feature among almost all file comparison software, and therefore, if you were to release a file comparison application you would almost certainly want to include it. But that is not always the case. What if the feature that you were considering sent your changed files up to an ftp server? How many people do you really think would find a feature like that useful? Probably a decent number, but should you really put an ftp client into the software? Besides just having more code to write, what other problems could you run into?
Well, there are several problems that you are can run into and most stem from the fact that our universe has a little problem with this thing called entropy. And because our universe suffers from this problem, everything around us suffers from this problem as well, and that includes our software. Entropy is the tendency for any system to deteriorate, and deteriorate they will…quickly. When dealing with software, you will often hear a term called “feature interaction.” Feature interaction is when one feature affects another feature in a way that is generally unexpected. (If you expect it, then most of the time you would just consider it to be part of the feature.) So, feature interaction is a physical manifestation of this entropy that exists in our software application. As we add more and more features, if we do not have checks in place to keep track of entropy in the system, eventually we will reach a point where each feature we add will interact with a large set of additional features, which will result in bugs.
So, how do we keep this entropy in check? Well, this is easier said than done. The one thing that you can do from the start is to test, test, test. Writing unit tests, doing integration testing, using continuous integration, all these things will help, but you will still run into places where feature interactions will bite you. For example, when you are writing unit tests, you are generally trying to isolate a single feature and test it independently of the rest of the system. This is why people put so much work into creating mocks frameworks and generating stubs for their classes. They are trying to isolate independent functions of the application and test them in a vacuum. And this is great, because when you do this you are relying on a process that says, if I can show that part "a" works and part "b" works, then I can reasonably assume that a + b works. The key word is "reasonably", because we aren’t proving anything, and we are looking at the process and making assumptions about how we think this interaction will occur.
The problem is that we are often wrong, and this is where integration testing comes into play. The problem is that many development shops do not do integration testing (at least not on purpose or with any intent). They do unit testing and then when that works they think that the system is well tested. Integration testing can also be automated, just like unit testing. You can come up with code that exercises many different components in the application in unison, which tests their interactions. In fact, many people already do this in their unit tests and they mistakenly consider those to also be unit tests. These tests should ideally be isolated from unit tests since they are testing many different parts of the application. These tests will also often rely on parts of the system like databases, network connections, etc… which unit tests should not generally rely on. But there is one problem with integration testing that is often the elephant in the room…integration testing an application with tons of features can be almost impossible if there is a lot of interaction between features.
So, since we are not here today to talk about integration testing, let’s begin by stating why we *are* here today. We are here to discuss how we can keep from getting to the point where integration testing because a horrible, unmanageable nightmare. And please keep in mind that what we are going to talk about is not easy answers. Nothing of any importance is ever easy.
1) Keep features in check. Sure, we all know the 80/20 rule, and we also all know it is B.S. As Joel Spolsky put it:
A lot of software developers are seduced by the old "80/20" rule. It seems to make a lot of sense: 80% of the people use 20% of the features. So you convince yourself that you only need to implement 20% of the features, and you can still sell 80% as many copies. Unfortunately, it's never the same 20%. Everybody uses a different set of features.
And of course that is true, if everyone used the same features then it would make our jobs pretty easy, right? But that doesn't mean that you have to include everything and the kitchen sink. Don't believe me? Check out the Chandler Project and the book that was written on it.
2) Try and isolate objects and features. Follow the Law of Demeter (also known as the principle of least knowledge). This law basically states that an object can only call its own methods and those on objects that it directly holds and instance of. In particular you should not call methods on objects that are properties of other objects or that are returned from another objects methods. This basically makes sure that one object does not depend too much on the internal functioning of another object. Give it a try, you may be surprised how often you do this and you might be surprised how much it helps you decouple objects!
3) Write a lot of unit tests. The immediate benefits are obvious, but what you might not expect is how much it helps you decouple your features. If you find that writing your unit tests is difficult, then you may be suffering from a high level of coupling. You can take this a step further with TDD (Test Driven Development) and this will help you even further in designing features that are inherently decoupled.
4) YAGNI. I am really tired of seeing this acronym, but it is so applicable to so many things. It stands for "You Ain't Gonna Need It" and the idea is that you never program any feature that you don't absolutely need right now. As programmers we often want to make everything "robust" and occasionally this is desirable, but more often we are just introducing bugs in code that we really didn't need in the first place.
5) Carefully consider performance optimization. While you obviously want software that runs quickly, often the only way of improving performance is to take shortcuts in the code which can sometimes complicate the code. While this doesn't directly affect the interactions between features, any time that the code is more complicated it is harder to figure out exactly what it is doing, which makes it harder to figure out what it is going to interact with.
6) Use short methods. Shorter methods allow you to break down a problem easier and allow you to keep them more encapsulated. If a method does more than one thing, then it probably needs to be broken down further. If you cannot keep all of the contents of a method on your screen at one time (assuming that you aren't running at 800×600), then you should have a good reason for this. This leads us into the next number…
7) Name your methods well. Name your methods well so that other developers using your classes know what they do. This will help to keep functionality from being reproduced in other classes since the developer will be able to tell that what they want to accomplish already exists in the class that they are calling. In smaller projects the developer would be able to just modify the class they were calling directly, but in larger projects this may not be possible and therefore the method naming can mean the difference between having nicely separated classes and having tightly woven classes. I have often heard it said that a method name should describe everything that a method does, if the name becomes too long, then it is because your method is doing too much.
8) KISS. This stands for Keep It Simple Stupid. This goes hand in hand with the last few numbers and is pretty much a blanket rule across all aspects of programming. The most simple solution is always the best one. Like Albert Einstein said "Make everything as simple as possible, but not simpler." The more complex a problem becomes the less of it that you will be able to hold in your head at one point, and therefore the problem needs to be broken down so that you can fully grok it. Just remember, if you wrote the code and have a hard time understanding it, then just imagine if you didn't write it.
So there you have it, follow these simple rules and most likely your software will still end up being crap. But that is just life. :-) Follow these rules while at the same time using your brain to adapt to the situation at hand and using your experience to help you adapt these rules to your particular application and you will likely succeed. Please take all of these rules with a grain of salt, I merely want to get you thinking about your application and how to best approach it, not act like I have some silver bullets for you. I honestly believe that one of the biggest problems facing software today is people accepting practices and methods without question, always question and always think.