Friday, July 13, 2007

Values of a Software Engineer

I originally wrote this as if it were a "best practices" document, but then what are best practices? Who says they're the best? And what the heck do I know and why would anyone listen to me? So, now it's just a post about software engineering practices that I, personally, value.

  • Don't forget to design your code

    It is extremely important to do some upfront design on all but the most simplistic software. It can even be fun, so it's surprising to see how often it's skipped or glossed over. One reason might be that people start prototyping something, they see that it works, and they get very excited. Then, they start feeling like they're almost done, so they begin fleshing out the prototype to bolt on the rest of the features that the software ultimately needs. Before they know it, they've got a big mess of unmaintainable software that clearly had little forethought. This can be OK for simple projects, but for anything beyond your weekend coding frenzy it's not a good idea. Prototyping is a great way to test new ideas and verify existing assumptions, but don't forget that it's just a prototype. 

    Writing software is not like constructing a building, in that you don't need to have rigorous blueprints with every precise measurement listed down to the last excruciating detail. But you do still need to do a lot of design work prior to writing your software. Software is built using abstract ("soft") concepts and objects, and you need to figure out what these concepts and objects are and how they interact and fit together. You need to study the problem domain in which you're working and figure out how it will map to your code. These objects typically make up the domain model of your code, and it's very important that they are well thought out and match reality as much as possible. For example, if you and your team always talk about how "a customer has an account", it should be a red flag to see that your code shows the Account class is the one that "has a" Customer object (or perhaps they're not related at all in the code). This reality-code mismatch is not uncommon and it can harbor a nasty nest of bugs. These domain objects are often used heavily throughout the code, and a poorly designed domain model can quickly infect the rest of your code. 

While designing your code you should also be open to new and potentially wild ideas. Sweeping refactorings during design time have a very low cost; you typically only need to change a few figures in OmniGraffle or crumple and throw away recycle a CRC card. Also, be very pragmatic. Don't immediately dismiss an idea just because of one potential complication. Maybe that complication will never actually arise. Maybe only a small percentage of your users will even care about it. If the idea is otherwise brilliant, it may be worth a small price. Don't expect to please everyone; you can't do it. Be pragmatic. Be pragmatic. Be pragmatic.

    Lastly, expect your design to change as you go, because it will; they always do. But this is OK. Once you're in the trenches writing the code, you may discover a much better way to do something. You shouldn't ignore this. Think about the tradeoffs and go with the best idea.

  • Refactor early and often

    Most software engineers today understand that software development is an iterative process, they know what refactoring is and most say they do it. However, there is often a gray area around when and what to refactor and whether or not it's too late to refactor. One common argument against refactoring is that there's no time for it. But as J. Hank Rainwater said in his book Herding Cats: A Primer for Programmers Who Lead Programmers, "If you don't have time to do the job right, when will you have time to do it again?" Delaying refactoring means delaying reaping the benefits of the well-factored code, and working with well-factored code is much easier, and takes much less time than working with poorly factored code. Delaying refactoring could be a huge cost in development time, bugs, performance, and more. Refactoring does not imply that you'll "slip the schedule." It may take you a couple days upfront to do the refactoring (or minutes, or months; it depends) but a good refactoring can save everyone time in the long run because the code will be easier to write, read, debug, understand, extend, maintain, etc.

    It's also common to ignore the need to refactor by saying "we'll push that off until R2." Sometimes it may be acceptable (although, "R2" often never comes) to delay a refactoring until the next release; my opinion is that it's not OK to push off fixing fundamental, core architectural problems. Problems in core code can have a devastating effect on the rest of the code. For example, consider a simple word processor application that has a Document class that conceptually consists of a list of DocumentPage classes. If Document doesn't have a getDocumentPages() method, then all clients of this code may have to implement their own logic to find all the DocumentPages for a given Document instance. Maybe some clients do it right; maybe some do it wrong and create bugs. The best case you can hope for here is that your code only suffers from the unwanted duplication of code. Perhaps a better alternative would be to put the logic for finding all the DocumentPages in one method inside the Document class itself (and then unit test it!).

    So, when's it time to refactor? Always. Now. What should you refactor? Anything that needs it. When you see code that needs refactoring, fix it then and there if possible. If it requires a larger change, discuss the refactoring with your team and figure out when it can be done. If you find yourself cursing a certain chunk of code daily, it may just need to be refactored—do it. Be vigilant with your code. Take pride in your code. Strive to make your code clean, readable, and
    ... as simple as possible, but no simpler.
    —Albert Einstein.

  • Use design patterns, but don't abuse them

    The use of OO design patterns can be surprisingly controversial. Some folks say they don't know design patterns by name, but that they innately use them anyway because they're just that good at coding. Others swear by patterns; they think the GoF book is the final word on the subject, and they can even get overly patterns-happy writing a Hello World program. I think somewhere in the middle is the best place to be.

    A design pattern is simply a way of documenting a solution to a problem. Each pattern has a name, collectively forming a vocabulary with which engineers can intelligently discuss these solutions. Design patterns are to object-oriented programming what algorithms are to functional programming: they are named solutions to common problems. It's much clearer to say that you sorted some numbers using heapsort, rather than explaining the whole heapsort algorithm in detail. Any worthwhile engineer would know what you're talking about. Similarly, it's clearer to say that NSAttributedString is a Decorator, or that NSNotificationCenter implements the Observer pattern, than it is to actually explain how they're implemented. The patterns vocabulary conveys a lot of information in just a few words; it's succinct.

    As we learned from Spider–Man, with great power comes great responsibility. Just because we know of a pattern doesn't mean we should use it. One of the most misused patterns is the Singleton. The Singleton is probably the easiest of the classic GoF design patterns to understand, which may be why engineers who are new to design patterns abuse it. I already discussed the issues with Singletons in a previous post (The Singleton Smell), so I won't go into it all here. But in a nutshell: Singletons are effectively global variables—avoid them as such, they make for code that's tightly coupled and difficult to test, they limit the reusability of the code, they can cause threading problems, etc. When I see code with lots of Singletons I immediately try to think of ways to refactor the Singletons away. Now, before you flame me saying how much you love and need your Singletons, let me just say that they *can* actually serve a purpose. Although, really, I only know of a few classes that actually should be Singletons.

  • Unit test your code

    Some engineers think of unit testing as an annoying administrative task much like adding a new cover sheet to your TPS report. But the reality is that unit tests are a great benefit to the engineer. They give you confidence that your code is correct. Good code must be semantically correct and do what it claims to do. Unit tests allow you to verify this by formalizing a test case in code that can—and should—be run often. If you have a method that claims to accept NULL as an argument, but you never pass it a NULL, how do you know it works when passed a NULL?

    Unit tests are also useful because they allow you to act as the client of the class and can often expose a clumsy API or code that may need refactoring. Once you realize the class needs to be refactored, the unit test will give you confidence that you didn't break anything while refactoring. If you find that your code is very difficult to unit test, then it probably needs to be refactored. If you can't figure out how to use and test your own class, how can you expect anyone else to either? Classes that are difficult to unit test are often so because they don't have a well-defined role or responsibility; don't build "kitchen sink" classes that have way too much responsibility.

    Unit tests are supposed to test the smallest "unit" of an application. This unit may be a command line program, a function, or the usual for an object-oriented program: a class. Don't try to write a unit test that tests your entire application from end-to-end; that's not a unit test. A class should have a clear role and responsibility and the unit test should ensure this.

Read a lot! Spend time keeping up with current technologies, methodologies, design ideas, etc. What you know today may be irrelevant tomorrow. Don't be that grumpy guy who's learned all he's going to and is content with his current state of knowledge. There are a lot of smart people in this industry; learn from them. When you learn something new and cool, share it with others. I'm sure your teammates would appreciate a new trick that will make their lives easier. And, if you're ever in need of that new trick to show off, read up on ssh port forwarding—that's always a crowd favorite.


Chmeee said...

I couldn't have said it better myself. Up-front design, use but not abuse of design patterns, and unit testing are all things I try to convince my work team to use, but it all seems to fail to register with them, and we're left with clumsy code that's a mess to test, fix, and extend.

Anonymous said...

Really nice article! Guess the hard part is to actually get used to and integrate the presented ideas in your everyday coding practice. Which they would discuss such more general principles in class..

Out of curiosity, how do I understand that very last sentence? :)

Anonymous said...
This comment has been removed by a blog administrator.
chrisjcarter said...

Great post!