A bad job of coding is a sure path to a low grade, but a good job of coding does not necessarily mean a good grade. Here are some tips to ease your implementation pains.
Since you are rather inexperienced in programming, no one expects you to write quality code in the first attempt. However, if you decide to live with messy code you produce, it will get messier as time goes on (have you heard about the 'broken windows theory' [http://tinyurl.com/refactoringstory] ?). The remedy is refactoring [http://tinyurl.com/code-refactoring]. Refactor often to improve your code structure.
Refactoring code regularly is as important as washing regularly to keep your privates clean. Neglecting either will result in a big stink. Analogies aside, any time spent on refactoring will not only reduce your debugging time, the quality improvement it adds to your code could even earn you extra credit.
Not recommended |
if (red) print "red"; else print "blue"; |
Better |
if (red) print "red"; else if (blue) print "blue"; else error("incorrect input"); |
When working in a team, it is very important for everyone to follow a consistent coding style. Appoint someone to oversee the code quality and consistency in coding style (i.e. a code quality guru). He can choose a coding standard to adopt. You can simply choose one from many coding standards floating around for any given language. Prune it if it is too lengthy - circulating a 50-page coding standard will not help. But note that a coding standard does not specify everything. You still have to come up with some more conventions for your team e.g. naming conventions, how to organise the source code, which 'error-prone' coding practices to avoid, how to do integration testing, etc.
Conventions work only if the whole team follows them. You can also look for tools that help you enforce coding conventions.
Global variables may be the most convenient way to pass information around, but they do create implicit links between code segments that use the global variable. Avoid them as much as possible.
Avoid indiscriminate use of numbers with special meanings (e.g. 3.1415 to mean the value of mathematical ratio PI) all over the code. Define them as constants, preferably in a central location.
[Bad] return 3.14236;
[Better] return PI;
Along the same vein, make calculation logic behind a number explicit rather than giving the final result.
[Bad] return 9;
[Better] return MAX_SIZE-1;
Imagine going to the doctor's and saying "My nipple1 is swollen"! Minimise the use of numbers to distinguish between related entities such as variables, methods and components.
[Bad] value1, value2
[Better] originalValue, finalValue
A similar logic applies to string literals with special meanings (e.g. "Error 1432").
The Pragmatic Programmer book calls this the DRY (Don't Repeat Yourself) principle. Code duplication, especially when you copy-paste-modify code, often indicates a poor quality implementation. While it is not possible to have zero duplication, always think twice before duplicating code; most often there is a better alternative.
Some students think commenting heavily increases the 'code quality'. This is not so. Avoid writing comments to explain bad code. Try to refactor the code to make it self-explanatory.
Do not use comments to repeat what is already obvious from the code. If the parameter name already clearly indicates what it is, there is no need to repeat the same thing in a comment just for the sake of writing 'well documented' code. However, you might have to write comments occasionally to help someone to help understand the code more easily. Do not write such comments as if they are private notes to self. Instead, write them well enough to be understandable to another programmer. One type of code that is almost always useful is the header comment that you write for a file, class, or an operation to explain its purpose.
When you write comments, use them to explain 'why' aspect of the code rather than the 'how' aspect. The former is not apparent in the code and writing it down adds value to the code. The latter should already be apparent from the code (if not, refactor the code until it is).
Often, simple code runs faster. In addition, simple code is less error-prone and more maintainable. Do not dismiss the brute force yet simple solution too soon and jump into a complicated solution for the sake of performance, unless you have proof that the latter solution has a sufficient performance edge. As the old adage goes, KISS (keep it simple, stupid - don't try to write clever code).
In particular, always assume anyone who reads the code you write is dumber than you (duh). This means you need to make the code understandable to such a person. The smarter you think you are compared to your teammates, the more effort you need to make your code understandable to them. You might ask why we need to care about others reading our code because, in the short span of a class project, you can always be there to handle your own code. Here are some good reasons:
If you think the above reasons are good enough to try and write human-readable code, here are some tips that can help you there:
Lay out the code to follow the logical structure of the code. The code should read like a story. Just like we use section breaks, chapters and paragraphs to organise a story, use classes, methods, indentation and line spacing in your code to group related segments of the code. For example, you can use blank lines to group related statements together.
Sometimes, the correctness of your code does not depend on the order in which you perform certain intermediary steps. Nevertheless, this order may affect the clarity of the story you are trying to tell. Choose the order that makes the story most readable.
A sure way to ruin a good story is to tell details not relevant to the story. Do not vary the level of abstraction too much within a piece of code.
[Bad] readData(); salary = basic*rise+1000; tax = (taxable?salary*0.07:0); displayResult(); |
[Better] readData(); processData(); displayResult(); |
When in doubt about what the specification or the design means, always clarify with the relevant party. Do not simply assume the most likely interpretation and implement that in code. Furthermore, document the clarification or the assumption (if there was no choice but to make one) in the most visible manner possible, for example, using an assertion [http://tinyurl.com/wikipedia-assertion] rather than a comment. You can use assertions to document parameter preconditions, input/output ranges, invariants, resource open/close states, or any other assumptions.
Choose the Design-by-Contract (DbC) [http://tinyurl.com/wikipedia-dbc] approach or the defensive programming [http://tinyurl.com/wikipedia-dp] approach and stick to it throughout the code:
We all get into situations where we need to write some code 'for the time being' that we hope to dispose of or replace with better code later. Mark all such code in some obvious way (e.g. using an embarrassing print message) so that you do not forget about their temporary nature later. It is irresponsible to release such code for others' use. Whatever you release to others should be worthy of the quality standards you live by. If the code is important enough to release, it is important enough to be of production quality.
Instead of switching off compiler warnings, turn it to the highest level. When the compiler gives you a warning, act on it rather than ignore it. A compiler is better at detecting code anomalies than the most of us. It is wise to listen to it when it wants to help us.
The error handling strategy is often too important to leave to individual developers' discretion. Rather, it deserves some pre-planning and standardisation at team level.
Response to an error should match the nature of the error. Handle local errors locally, rather than throwing every error back to the caller. Some systems can benefit from centralised error handling where all errors are passed to a single error handler. This technique is especially useful when errors are to be handled in a standard yet non-trivial manner (e.g. by writing an error message to a network socket).
A sufficiently verbose/interactive application avoids the doubt "is the application stuck or simply busy processing some data?" For example, if your application does an internal calculation that could take more than a couple of seconds, produce some output at short intervals during such a long process (e.g. show the number of records processed at each stage).
Some bugs crops up only occasionally and they are hard to reproduce under test conditions. If such a bug crashes your system, there should be a way to figure out what went wrong. If your application writes internal state information to a log file at different points of operation, you can simply look at that log file for clues. This is the same reason why an aircraft carries a black box (aka Flight Data Recorder).
Printing a welcome message at startup (or a splash screen, if your application has a GUI) - showing some useful data like the version number - is not just good manners; it also makes our application look more professional and helps us catch problems of 'releasing an outdated version'. However, such preliminaries should not take too long and should not annoy the user.
The ability to do a clean shutdown is an important part of a system functionality most students seem to forget, especially, if there are some tasks to be done during the shutdown, such as saving user preferences or closing network connections.
Most students invent their own set of terminology to refer to things in the problem domain. This terminology eventually makes it to the program code, documentation, and the UI. Instead, stick to the terms used in the problem domain. For example, if the specification calls a component the 'projector', do not refer to the same component as 'the visualiser' in your code. While the latter may be a better match for what that component actually does, it could cause unnecessary confusion and much annoyance for the evaluator/supervisor/customer.
A 'throw-away prototype' (not to be confused with an 'evolutionary prototype') [http://tinyurl.com/wikipedia-prototyping] is not meant to be of production quality. It is a quick-and-dirty system you slapped together to verify certain things such as the architectural viability of a proposed product. It is meant to be thrown away and you are supposed to keep only the knowledge you gained from it. Having said that, most of you will find it hard to throw the prototype away after spending so much time on it (because of your inexperience, you probably spent more time on the prototype than it really required). If you really insist on building the real system on top of the prototype (i.e. starting to treat a throw-away prototype as an evolutionary prototype), devote the next iteration to converting the prototype to a production quality system. This will require adding unit tests, error-handling code, documentation, logging code [see tip 11.21], and quite a bit of refactoring [see tip 11.1].
Most bugs are like needles in a haystack. Piling on more hay will not make them easier to find. It is always preferable to find and fix as many bugs as you can before you add more code.
Before you release your own code, check-out the latest code from others, do a clean build, and run all the test cases (having automated test cases will be a great help here). The biggest offense you can do while coding is releasing broken code to the code base.
Even if it is a tiny change and it cannot have possibly broken anything, do all of the above anyway, just to be sure.
You code a little, try it, and it seems to work. You code some more, try it, and it still seems to work. After many rounds of coding like this, the program suddenly stops working, and after hours of trying to fix it, you still cannot figure out why. This approach is what some call 'programming by coincidence' [http://tinyurl.com/prog-by-coin]; your code worked up to now by coincidence, not because it was 100% correct.
Instead, code a little, test it thoroughly, only then you code some more. See chapter 13 for more tips on testing.
Code reviews promote consistent coding styles within the team, give better programmers a chance to mentor weaker ones, and motivate everyone to write better code.
Hold at least one code-review session at an early stage of the project. Use this session to go over each other's code. Discuss the different styles each one has adopted, and choose one for the whole team to follow.
There is no such thing as 'perfect' code. While you should not release sloppy software, do not hold up progress by insisting on yet another minor improvement to an already 'good enough' code.
Having an installer gives you several advantages, one of which is creating a more 'professional' impression of your software. There are several tools available today that can create an installer for your software very easily. But note that one major disadvantage of installers is some potential users might resist installing 'untested' software for the fear of 'slowing down the computer' or 'corrupting the registry'. If your software can do without a installer, then it is best not to have one.
To quote from book The pragmatic programmer: We want to see pride of ownership. "I wrote this, and I stand behind my work."
By making you put your name against the code your wrote (e.g. as a comment at the beginning of the code), we encourage you to write code that you are proud to call your own work. This is also useful during grading the quality of work by individual team members.
To most of us, coding is fun. If it is not fun for you, you are probably not doing it right.
Functional correctness is usually the most important quality of a system. To achieve functional correctness, we should understand the specifications well (we need to know what is the 'correct' behaviour expected in the first place).
In addition, a system is required to have many non-functional qualities (i.e. qualities that are not directly related to its functionality), some explicitly mentioned, some only implied. The nature of the system decides which qualities are more important. Find out the relative importance of each quality, as you might have to sacrifice one to gain another. There are many to choose from such as Analysability, Adaptability, Changeability, Compatibility, Configurability, Conformance, Efficiency, Fault tolerance, Installability/uninstallability, Interoperability Learnability, Localizability, Maintainability, Portability, Performance, Replaceablity, Reliability, Reusability, Security, Scalability, Stability, Supportability, Testability, Understandability, Usability.
Most grading schemes have some marks allocated for certain NF qualities. Note that some NF qualities are not easily visible during a product demo. Be sure to talk about them during the presentation, and in the report. Given next are some common NF qualities and some tips on achieving each.
Can your system withstand incorrect input? Will it gracefully terminate when pushed beyond the operating conditions for which it was designed? Does it warn the user when approaching the breaking point or simply crash without warning? If you can crash your system by providing wrong input values, then it is not a very robust system.
Do not think maintainability is unimportant because this is just a class project or because the instructor did not mention it specifically. The truth is that you will 'maintain' your code from the moment you write it. You will really regret writing unmaintainable code especially during bug-fixing and modifying existing features towards the end of the project.
Keeping the design and eventually, its implementation, simple and easy-to-understand can really boost the maintainability of your system. A comprehensive suite of automated test cases can detect undesirable ripple effects of a change on the rest of the system. Having such tests indirectly helps maintainability.
Maintainability is of utmost importance if you plan to release the product to the public and keep it alive after the course.
Security is a prime concern for most systems, especially for multi-user systems with open access (such as Internet-based applications). If security is a top priority for your system, make sure you have it built into the system from the very beginning. It is much harder to secure a system built based on an unsecure architecture.
Under this aspect, you can address the following questions:
If you have a lot of duplicate code in your system (some code analysers can easily find this out for you), you have not reused code well.
Testability does not come by default; some designs are more testable than others. To give a simple example, methods that return a value are easier to test than those that do not. If you give up testing some parts of your system because it is too difficult to test, you have a testability problem. If you have good testability, it is possible to test each part of your system in isolation and with reasonable ease. During integration, a system with good testability can be tested at each stage of the integration, integrating one component at a time.
How well can your system handle bigger loads (bigger input/output, more concurrent users, etc.)? Does the drop in performance (if any) reasonably reflect the increase in workload?
How easily can you change a certain aspect of your system? For example, can you change the database vendor or the storage medium (say, from a relational database to a file-based system) without affecting the rest of the system? Can you replace a certain algorithm in your system by replacing a component?
How easily can you extend your system (to handle other types of input, do other types of processing, etc.)? For example, can you add functionality by simply plugging in more code, or do you need extensive changes to existing code to add functionality?
Does the usability of your system match the target user base? Is the operation intuitive to the target user base? Have you produced good user manuals? Does your system let users accomplish things with a minimal number of steps? Tip 10.19 can help you further in this aspect.
Any suggestions to improve this book? Any tips you would like to add? Any aspect of your project not covered by the book? Anything in the book that you don't agree with? Noticed any errors/omissions? Please use the link below to provide feedback, or send an email to damith[at]comp.nus.edu.sg
---| This page is from the free online book Practical Tips for Software-Intensive Student Projects V3.0, Jul 2010, Author: Damith C. Rajapakse |---