![]() | Monkeys at Keyboards: The Javanomicon © Michael James Heron | ||||
| Topic: Java Programming Level: 2 Version: delta | |||||
3 - Hallmarks of Good Code | |||||
| Previous | Table of Contents | Next |
| Forum |
| Chapter Objectives |
By the end of this chapter, the reader will be able to:
|
Programming as a discipline offers a degree of granularity in terms of how code is structured internally. It's possible that a piece of code will do everything it's supposed to do, but still be 'bad code'. In this section of the book we're going to look at the hallmarks of good code and what exactly qualifies a program as being well coded. This is a matter of some subjectivity - certain coding techniques are likely to appeal to developers on the basis of nothing more than personal preference. There is nothing wrong with this, provided that the code checks out on each of the hallmarks:
We can often say that one method of coding is better than another method, but it is difficult to precisely quantify this. Provided any given approach checks out on these standards of quality then it doesn't really matter, beyond personal preference, how a piece of functionality is implemented. As you gain experience, you'll also gain an appreciation of what works, why it works, and why it's worth doing. The unfortunate thing is that often these hallmarks are mutually exclusive. It is possible to write a piece of code that is supremely efficient, but not very readable. It is possible to write code that is extremely maintainable, but not very efficient. True software quality comes from a balance of these principles, except when real world constraints dictate otherwise. We'll discuss this point some more in a later chapter. In the code we develop for this textbook, there are no over-riding functional requirements that favour the emphasis of one aspect over another, so quality is defined as code that blends each of the hallmarks to a satisfactory degree. In the real world, it is often the case that the functional requirements of a piece of software over-ride one or more of the hallmarks. When writing a piece of software for a device with limited memory or processing power (like a mobile phone or a PDA), then efficiency will be an over-riding concern over the readability of the code. Likewise writing a framework for an application designed to grow as its deployment environment changes will favour maintainability over efficiency.
With modern computers, efficiency isn't as much of a fundamental requirement as it once was. With gigahertz processors being the baseline for systems, and hundreds of megabytes of memory being easily available, the old ethos of writing compact, super efficient code has largely died out in favour of the benefits that come from having a heavy framework that allows for powerful and configurable development. Object orientation as a programming principle is an example of this - every object that is created in an application takes up memory and system resources, and these are often much greater than they functionally need to be. The flexibility that comes from object orientation is considered to be a suitable payoff for this loss of efficiency. For the purposes of this textbook, we define efficiency as: The code makes use of system resources in an optimised manner. The scope of variables is consistent with the requirements of the functionality. Loops are terminated at appropriate junctures and there is no unnecessary processing done on data. Note that this definition does not require your code to be as fast as it could possibly be - there are often multiple ways of attempting a task and some of these are guaranteed to be faster than others. It is not required that you choose the fastest method of execution over any others, although an application should be considered inefficient if it has an unnecessary amount of processing required to meet the requirements when a simpler, more compact method is available. For a simple example, let's look at a program that utilises a for loop designed to give one number to the power of another:
This class will work exactly as intended - as in, it meets the requirements of the application. However, it is inefficient. The variables have all been declared with a class wide scope, which means that they exist and take up space in memory as long as the class exists. Even once we've called the returnPower method, num1, num2, and i continue to exist. Instead of this, we should write the class ensuring that variables have their scope restricted to the bare minimum they need to accomplish their task:
This second class does exactly the same thing as the previous class, but does it slightly more efficiently - because the scope of variables is correct it means that they don't exist for longer than they are actually needed. The first class meets the requirements of the application, but it does so inefficiently. It meets the requirement, but is still, to a degree, 'bad code'. The second class meets the requirements of the application, and it does so as efficiently as it can be. For a second example, let us consider a loop that prints out every even number between two values given by the user:
This would seem to be an efficient way of meeting the requirements of the application. However, consider what is actually being done in the code: for every number between the start and end points. Check to see if the number is even. If it is, print it to the screen Considering what we're actually doing and how restricted the problem really is, we can write a slightly different method:
This is actually a more efficient way of doing this. For one thing, we only do the check for an odd number once, at the very start. If it's odd, we then add one to it so that it is even - from this point on, we know every second number will be even. Then in the loop, we move on two places every time around - so we only loop half as many times. There is a method we can use to determine exactly how efficient one piece of code is in comparison to another - we simply take a note of the time at the start of execution and take it again at the end of the execution. Then we print out the end time minus the start time, and this will give us the execution time. Using the particular methods we are going to use, it will give us the difference in nanoseconds (doesn't that make you feel as if you're writing code for the Starship Enterprise?). We will write a method that we can use to evaluate how long a particular piece of code takes to run. To write this method, we make use of a method called nanoTime in the System class
Most of the time, any given piece of code will be executed so quickly that it won't even register - so we put our code to time within a for loop that repeats the code a suitable number of times. This also has the effect of averaging out the effect of unforeseen system events:
For example, we could write a class that incorporates this method to test our very first version of the printEvenNumbers method:
Note that we've taken out the System.out.println call - see how long the method takes to run if you leave it in! The program itself will do much of the averaging out for us, but we may still wish to run it a number of times to see the level of variation between runs. The longer we leave it running, the more accurate the number we get will be, but after a certain point it won't change radically. Running this testing framework gives us the following output:
Note too that if you run this on two different computers you will get two different answers - the figures we get here are only relative indications of how long code takes to execute.
Next, we want to compare this against our second implementation, so we swap out the printEventNumbers method code - everything else remains exactly the same:
This gives us the following output:
Bear in mind here that the numbers are not a straightforward comparison - there is some overhead in making a method call that is factored into the execution time for each of the methods (mainly because it's too difficult to swap out comparative methods otherwise). We cannot simple assume that method one takes over three times as long to execute as method two - but what we can see is that method two is clearly the most efficienct of the two. We even have metrics to back that up!
This is a very simple method, but we can see that careful construction of the appropriate constructs can have very real gains in efficiency. In this particular example, even when the method is executed 10000 times, the efficincy gain is not something that will particularly affect a normal user... but imagine if this was a method in a 3D game engine - particularly a method called many times per second. We will return to the topic of optimisation later in the book - it is a complex subject that carries with it a number of caveats that must be considered before it is attempted. For now, it is sufficient that we have an understanding of the kind of impact coding decisions may have on efficiency of performance.
Robustness means that your code should deal gracefully with contingencies and not simply keel over and die when someone does something unexpected. Consider a simple division method:
This will work as intended, until someone passes in 0 as a second parameter. At this point, the method will throw an error and your application will terminate. You can solve this problem by including a check:
This demonstrates the basic principle of robustness - where possible, your code should deal with errors in a sensible way and ensure that an error somewhere in the code doesn't bring down the rest of the application. This is a secondary concern at this point in the book - when we talk about exceptions in chapter 16 we will see a way for dealing with these kind of problems correctly. For your first few forays into Java, this is not a consideration, but once we discuss the idea of exceptions you will be expected to ensure the robustness of your code if you wish to improve the quality of your applications. The idea of robustness once again introduces the idea of the distinction between good and bad code. More accurately, it introduces a distinction between correct code (which accomplishes the task for which it was written), and robust code (which handles unexpected input in a graceful way). A program can be correct and not robust, and also robust but not correct. The best programs, of course, are both. Ensuring robustness at this stage is difficult, because we haven't covered the techniques we can use to identify problems in code... it's fairly easy to apply an ad hoc approach to identifying faults in code, but this isn't enough to ensure any kind of confidence in the robustness (or indeed, correctness) of a piece of code. There are formal tools for doing this, and we'll touch upon them in chapter 17 and 18, but for now we're pretty much incapable of finding all of the faults in our projects. However, we shouldn't be discouraged by this - where we can fix faults, we should... the fact that we can't yet fix them all does not mean that we shouldn't fix any of them. Consider it to be your task as a developer to ascend to ever higher levels of correctness and robustness, until your program achieves the kind of of perfected ascension normally associated with departed Jedi Knights:
We will call the table above the 'Javanomicon Correctness Metric'. Most programs that compile will begin somewhat above level one for robustness - it's pretty rare that a program, even in its initial stages, will crash with all input. It's possible, of course... but such programs don't last in that stage for long. However, a program cannot be classed as level two until it can be proven (through formal testing) that it will accept all standard input. All programs begin at level one for correctness - until there has been some formal testing, there is no way to determine (or at least, prove) the level of correctness of the program. There is nothing to stop a suitably motivated developer achieving level five in both robustness and correctness - the highest level is not an unachievable ideal, it is something well within the reach of programmers. It just takes the will to get there.
Requirements change all the time when developing code - good developers keep this fact in mind when writing applications and make sure their code is ready to be modified should fundamental aspects of the requirements be altered. At its most basic level, this means ensuring your code is modular - in other words, that your code is broken up into sensible methods (and classes, but we'll get to that later) and that you make use of all the expandable code structures provided for you, such as arrays (we'll talk about these in a later chapter). If a requirement changes and it means that your code has to be rewritten from scratch, it's an indication that you probably need to make your code design more modular. For example, consider an application designed to go through a list of ten numbers and print out the square of each:
If later on the requirements change so that instead of printing out the square of each number we must print out the cube, we must then change every line of code that does this calculation. We could have made this more modular, and thus more maintainable, by using a method:
Now when our requirements change, we need only change the giveAnswer method:
And voila, our code now meets the changed requirements. Much like with the efficiency section, this is a very simple example of the idea - a proof of concept rather than a genuine argument as to why it is necessary to write modular code. The only way to really gain an appreciation for why maintainability is an important issue is to have someone fundamentally change your programming requirements on you at the last minute. Such changes occur quite a lot when developing a real world application - people change their minds, and as the software they see starts to take shape, they begin to have new ideas and request extra or modified functionality. That's the nature of the beast - but provided you have anticipated this, you will find it much easier to cope with the hectic demands that software developing will place on your delicate foreheads.
One of the aspects of good code most overlooked is that of readability - this is a sin that most developers are guilty of, the author being no less sinful than others. The readability of your code has an effect on almost all aspects of development - it eases bug fixing, improves maintainability, and makes it easier for others to expand upon your code or aid in development. Making your code readable is a doubleplusgood thing to get into the habit of. There are simple rules to follow when writing code - make sure you keep them in mind to improve the readability of your programs: Use Meaningful Variable NamesThe names you give your variables within your code should have some meaning as to what they actually do. For example, if you have an integer value that you are using to store the number of times a button has been pressed, then the name should reflect that:
Rather than for example:
When coding purely for one's own pleasure, it is easy to get into the habit of naming variables in an idiosyncratic way. For example, I myself often use the variable names bing, bong, bang, blue, and frog when naming local variables within a method - this is not helpful when others read the code - or indeed, when I read the code in later months! For any work you do where others are going to have to read (and possibly assess) your code, you should ensure that your variable names have some relevance to what they are actually doing. In addition to this, you should adhere to some kind of naming convention when giving your variables names. The naming convention used throughout Java as standard is that the first letter of a variable name is lower case, with upper case letters being used to separate out words. This is only a convention and not enforced by Java itself. It doesn't really matter what convention you use, as long as you are consistent with it: Bad:
Better:
Comment Your CodeAgain, this is something that is widely preached and only occasionally practiced. You should comment your code effectively to improve the readability. When writing comments, there are some thoughts to bear in mind. The main one is - who is going to be reading the comments? It's not your grandmother, your sister, or some guy you meet on a bus. It's going to be someone who has at least some passing familiarity with code, and as such you can assume that there is no need to comment lines of code that are fundamental parts of the language. For example:
This is not a helpful comment since all it does is restate the line of code in English. That is not to say that there is no use in commenting variables:
The difference lies in what the comments tell us. One simply restates the line of code, the other states the intention and the use. Mainly though you need only comment those sections of code that have some complex or semi-complex functionality. You should state the intention of the code, not what it actually does - these may not actually be the same. For example: Bad:
Better:
The second comments are better because they indicate the intention of the code. With the first comments, it's not possible to tell simply from the loop that it is incorrect. With the second it's clear that the loop is not working correctly because it uses a + instead of a *. Format your code consistentlyThis is a dangerous area when discussing readability of code -arguably nothing affects the readability of code more than the way it is laid out. There are often holy wars of formatting that break out amongst developers as to how a piece of code should be indented. The truth is that it really doesn't matter, as long as you are consistent. The two main ways of laying out code are as follows: Method 1:
Method 2:
Either of these methods are fine. The convention for both is that you indent one level for each level of braces - the only difference is where the opening brace goes: Method 1:
Method 2:
As you will have noticed from the example code, Method 1 is the way the programs in this book are formatted. You may be more familiar with Method 2 from other programming text books, but it is important than you understand how to read code formatted using both methods. It is just unimportant with which of the methods you choose to format your own code. Do not use Magic NumbersMagic numbers are those numbers that appear throughout code, usually used as a mathematical modifier for some variable, but have no explanation given as to what they represent. For example, a bank that has an interest rate of 2.15% that is applied to all accounts annually may have a method such as:
This is an example of readability and maintainability - even without saying what the interest rate is it's pretty clear what this method is doing from the name of the method, the name of the variable, and the code itself. But if you encounter a line of code somewhere in a method that looks like:
There is no indication as to what 7.2 actually means and what it represents. Likewise we cannot infer its use from the name of the variable it is being applied to. 7.2 is a magic number. It is very bad form to use magic numbers throughout your code. Instead you should create a meaningful variable name and assign it a particular value:
This has the added value of increasing maintainability - if you use this variable throughout your code, you need only change the variable once to update all of your code with the correct values. Keep It Simple, StupidThere is a natural assumption when learning a programming language that the lebel of complexity of the code is a reflection on the intelligence of the coder... this is usually not the case. It can be fun and satisfying to write 'clever' code to solve a problem, but it has a big impact on the readability of your programs. There should never be a case when you write a piece of code that only you can understand, unless it's the only way implement piece of functionality. Keep your code as simple as possible (but to paraphrase Albert Einstein, not one bit simpler). It makes it easier to read, easier for others to maintain, and doesn't make you look like someone with a compulsive need to impress.
All of these concepts are tied together in a general category called elegance. This is the most subjective of all these categories since the definition of elegance in code is a largely aesthetic exercise. It is possible for code to meet each of the hallmarks above, but still be inelegant. There are several definitions of elegance, but the one we're going to use for the textbook concerns the flow of logic through a program. One of the main sources of inelegance in coding is that of convoluted logic flows:
This is an example of a convoluted logic flow - thinking through this structure is a relatively difficult exercise for humans. One of the principles of elegance is that if you have to write a logic structure like this then there is probably a better way of doing it. The idea of elegance isn't something that will be particularly important when learning the concepts in this textbook - it is something to aim for once you have understanding. Often the elegance of a solution to a problem is tied up in the underlying representation of the data - at the moment we simply don't have the tools in our toolkit to effectively model complex relationships of data. The elegance of a solution is the exactness and conciseness of its data representation and the ease with which it can be manipulated. For reasons of efficiency, readability and maintainability, it is sometimes necessary to sacrifice elegance for more pragmatic considerations. The search for elegance in computation is one of the few areas in which programming is more like an art than a science - it's when the act of programming is no longer about solving a problem (as in, we just want an solution) but expressing an approach to the solution that has at its core an appreciation of the nature of both the problem area and the act of programming itself. Elegance is not something that can be methodically implemented when designing a computer application - it's something that must be built into the application at the start, and it comes from thought, consideration and planning about what is the simplest and most effective way of designing an application. It is impossible for a solution to be elegant if it does not meet the requirements of all the hallmarks above, but a solution that does meet all these hallmarks is not necessarily elegant. Elegant solutions are what we should be aiming for all the time as programmers - it's only appropriate that they are so hard to achieve.
We will have cause to return to this topic at the end of the book, since it is a very deep subject that is worth further exploration. For now, we're interested in these concepts only so far as they impact on quality of our intial forays into programming. We haven't really covered enough of Java to be able to really go into any depth about any of these topics. There are tips and strategies for optimising all of these aspects of a program that are just too complex for us to cover at the moment. This book is not just about learning more tools relating to programming - it's about learning to program better. This is something that comes only with practise - this book can only nudge you in the right direction. It's up to you to make sure that you start walking.
Exercise OneGet together with a friend and exchange copies of programs that you have written for previous modules. Analyse your friend's code for maintainability. What would happen if the requirements changed sometime in the future. Would it be easy to update the code? Analyse your friend's code for robustness. Can you break the program using nothing more than the provided interface? Analyse your friend's code for efficiency. Are there any obvious areas where code is being executed when it doesn't need to be? Analyse your friend's code for readability. Can you understand what the code is doing, and what it is supposed to be doing? These may not be the same thing... Exercise twoAnalyse your own code. Go through all of the programs you wrote in first year and comment them properly. Exercise threeConsider the following code snippet. Identify areas of bad practise:
Exercise fourUsing only the standard Java syntax we have covered in the past three chapters, how would you improve the robustness of the following piece of code?
Further ReadingThe following table details further reading on the topic in this chapter, and also any external resources that you may find useful.
|
| Previous | Table of Contents | Next |
© 2004-2006 Michael James Heron