Code Quality

Introduction

Basic

๐Ÿ† Can explain the importance of code quality

Always code as if the person who ends up maintaining your code will be a violent psychopath who knows where you live. -- Martin Golding

Production code needs to be of high quality. Given how the world is becoming increasingly dependent of software, poor quality code is something we cannot afford to tolerate.

Code being used in an actual product with actual users

Guideline: Maximise Readability

Introduction

๐Ÿ† Can explain the importance of readability

Programs should be written and polished until they acquire publication quality. --Niklaus Wirth

Among various dimensions of code quality, such as run-time efficiency, security, and robustness, one of the most important is understandability. This is because in any non-trivial software project, code needs to be read, understood, and modified by other developers later on. Even if we do not intend to pass the code to someone else, code quality is still important because we all become 'strangers' to our own code someday.

Example: The two code samples given below achieve the same functionality, but one is easier to read.

int subsidy() {
    int subsidy;
    if (!age) {
        if (!sub) {
            if (!notFullTime) {
                subsidy = 500;
            } else {
                subsidy = 250;
            }
        } else {
            subsidy = 250;
        }
    } else {
        subsidy = -1;
    }
    return subsidy;
}

int calculateSubsidy() {
    int subsidy;
    if (isSenior) {
        subsidy = REJECT_SENIOR;
    } else if (isAlreadySubsidised) {
        subsidy = SUBSIDISED_SUBSIDY;
    } else if (isPartTime) {
        subsidy = FULLTIME_SUBSIDY * RATIO;
    } else {
        subsidy = FULLTIME_SUBSIDY;
    }
    return subsidy;
}

Basic

๐Ÿ† Can follow basic guidelines for improving readability

Be wary when a method is longer than the computer screen, and take corrective action when it goes beyond 30 LOC (lines of code). The bigger the haystack, the harder it is to find a needle.

If you need more than 3 levels of indentation, you're screwed anyway, and should fix your program. --Linux 1.3.53 CodingStyle

In particular, avoid arrowhead style code.

Example:

Avoid complicated expressions, especially those having many negations and nested parentheses. If you must evaluate complicated expressions, have it done in steps (i.e. calculate some intermediate values first and use them to calculate the final value).

Example:

return ((length < MAX_LENGTH) || (previousSize != length)) && (typeCode == URGENT);


boolean isWithinSizeLimit = length < MAX_LENGTH;
boolean isSameSize = previousSize != length;
boolean isValidCode = isWithinSizeLimit || isSameSize;

boolean isUrgent = typeCode == URGENT;

return isValidCode && isUrgent;

The competent programmer is fully aware of the strictly limited size of his own skull; therefore he approaches the programming task in full humility, and among other things he avoids clever tricks like the plague. -- Edsger Dijkstra

When the code has a number that does not explain the meaning of the number, we call that a magic number (as in โ€œthe number appears as if by magicโ€).

Example:

return 3.14236; return PI
return 9 return MAX_SIZE-1

Using a named constant makes the code easier to understand because the name tells us more about the meaning of the number. Similarly, we can have โ€˜magicโ€™ values of other data types.

return โ€œError 1432โ€;  // A magic string!

Make the code as explicit as possible, even if the language syntax allows them to be implicit. Here are some examples:

  • Use explicit type conversion instead of implicit type conversion.
  • Use parentheses/braces to show grouping even when they can be skipped.
  • Use enumerations when a certain variable can take only a small number of finite values. For example, instead of declaring the variable 'state' as an integer and using values 0,1,2 to denote the states 'starting', 'enabled', and 'disabled' respectively, declare 'state' as type SystemState and define an enumeration SystemState that has values 'STARTING', 'ENABLED', and 'DISABLED'.
  • When statements should follow a particular order, try to make it obvious (with appropriate naming, or at least comments). For example, if you name two functions 'taskA()' and 'taskB()', it is not obvious which one should be called first. Contrast this with naming the functions 'phaseOne()' and 'phaseTwo()' instead. This is especially important when statements in one function must be called before the other one.

Intermediate

๐Ÿ† Can follow intermediate guidelines for improving readability

Lay out the code so that it adheres to the logical structure. The code should read like a story. Just like we use section breaks, chapters and paragraphs to organize 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.

Avoid things that would make the reader go โ€˜huh?โ€™, such as,

  • unused parameters in the method signature
  • similar things look different
  • different things that look similar
  • multiple statements in the same line
  • data flow anomalies such as, pre-assigning values to variables and modifying it without any use of the pre-assigned value

As the old adage goes, "keep it simple, stupidโ€ (KISS). Do not try to write โ€˜cleverโ€™ code. For example, do not dismiss the brute-force yet simple solution in favor of a complicated one because of some โ€˜supposed benefitsโ€™ such as 'better reusability' unless you have a strong justification.

Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. --Brian W. Kernighan

Programs must be written for people to read, and only incidentally for machines to execute. --Abelson and Sussman

Optimizing code prematurely has several drawbacks:

  • We may not know which parts are the real performance bottlenecks. This is especially the case when the code undergoes transformations (e.g. compiling, minifying, transpiling, etc.) before it becomes an executable. Ideally, you should use a profiler tool to identify the actual bottlenecks of the code first, and optimize only those parts.
  • Optimizing can complicate the code, affecting correctness and understandability
  • Hand-optimized code can be harder for the compiler to optimize (the simpler the code, the easier for the compiler to optimize it). In many cases a compiler can do a better job of optimizing the runtime code if you don't get in the way by trying to hand-optimize the source code.

A popular saying in the industry is make it work, make it right, make it fast which means in most cases getting the code to perform correctly should take priority over optimizing it. If the code doesn't work correctly, it has no value on matter how fast/efficient it it.

Premature optimization is the root of all evil in programming. --Donald Knuth

Note that there are cases where optimizing takes priority over other things e.g. when writing code for resource-constrained environments. This guideline simply a caution that you should optimize only when it is really needed.

Avoid varying the level of abstraction within a code fragment. Note: The Productive Programmer (by Neal Ford) calls this the SLAP principle i.e. Single Level of Abstraction Per method.

Example:

readData();
salary = basic*rise+1000;
tax = (taxable?salary*0.07:0);
displayResult();

readData();
processData();
displayResult();

Design โ†’ Design Fundamentals โ†’ Abstraction โ†’

What

Abstraction is a technique for dealing with complexity. It works by establishing a level of complexity (or an aspect) we are interested in, and suppressing the more complex details below that level (or irrelevant to that aspect).

Most programs are written to solve complex problems involving large amounts of intricate details. It is impossible to deal with all these details at the same time. The guiding principle of abstraction stipulates that we capture only details that are relevant to the current perspective or the task at hand.

Ignoring lower level data items and thinking in terms of bigger entities is called data abstraction.

๐Ÿ“ฆ Within a certain software component, we might deal with a user data type, while ignoring the details contained in the user data item such as name, and date of birth. These details have been โ€˜abstracted awayโ€™ as they do not affect the task of that software component.

Control abstraction abstracts away details of the actual control flow to focus on tasks at a simplified level.

๐Ÿ“ฆ print(โ€œHelloโ€) is an abstraction of the actual output mechanism within the computer.

For example,

Abstraction can be applied repeatedly to obtain progressively higher levels of abstractions.

๐Ÿ“ฆ An example of different levels of data abstraction: a File is a data item that is at a higher level than an array and an array is at a higher level than a bit.

๐Ÿ“ฆ An example of different levels of control abstraction: execute(Game) is at a higher level than print(Char) which is at a higher than an Assembly language instruction MOV.

Putting all details in one place can create lengthy methods, but it is preferred over creating many small methods because it makes the code easier to understand.

False

Explanation: If you are using abstraction properly, you DONโ€™T need to see all details to understand something. The whole point of using abstraction is to be able to understand things without knowing as little details as possible. This is why we recommend single level of abstraction per method and top-down coding.

What are the drawbacks of trying to optimize code too soon?

  • a. We may not know which parts are the real performance bottleneck
  • b. When we optimize code manually, it becomes harder for the compiler to optimize
  • c. Optimizing can complicate code
  • d. Optimizing can lead to more error-prone code

a, b, c, d

This is a common saying among programmers

b

Advanced

๐Ÿ† Can follow advanced guidelines for improving readability

The happy path (i.e. the execution path taken when everything goes well) should be clear and prominent in your code. Restructure the code to make the happy path unindented as much as possible. It is the โ€˜unusualโ€™ cases that should be indented. Someone reading the code should not get distracted by alternative paths taken when error conditions happen. One technique that could help in this regard is the use of guard clauses.

Example:

if (!isUnusualCase) {  //detecting an unusual condition
    if (!isErrorCase) {
        start();    //main path
        process();
        cleanup();
        exit();
    } else {
        handleError();
    }
} else {
    handleUnusualCase(); //handling that unusual condition
}

In the code above,

  • Unusual condition detection is separated from their handling.
  • Main path is nested deeply.

if (isUnusualCase) { //Guard Clause
    handleUnusualCase();
    return;
}

if (isErrorCase) { //Guard Clause
    handleError();
    return;
}

start();
process();
cleanup();
exit();

In contrast, the above code

  • deals with unusual conditions as soon as they are detected so that the reader doesn't have to remember them for long.
  • keeps the main path un-indented.

Guideline: Follow a Standard

Introduction

๐Ÿ† Can explain the need for following a standard

One essential way to improve code quality is to follow a consistent style. That is why software engineers follow a strict coding standard (aka style guide).

The aim of a coding standard is to make the entire code base look like it was written by one person. A coding standard is usually specific to a programming language and specifies guidelines such as the location of opening and closing braces, indentation styles and naming styles (e.g. whether to use Hungarian style, Pascal casing, Camel casing, etc.). It is important that the whole team/company use the same coding standard and that standard is not generally inconsistent with typical industry practices. If a company's coding standards is very different from what is used typically in the industry, new recruits will take longer to get used to the company's coding style.

IDEs can help to enforce some parts of a coding standard e.g. indentation rules.

What is the recommended approach regarding coding standards?

c

What is the aim of using a coding standard? How does it help?

Basic

๐Ÿ† Can follow simple mechanical style rules

Go through the provided Java coding standard and learn the basic style rules.

Intermediate

๐Ÿ† Can follow intermediate style rules

Go through the provided Java coding standard and learn the intermediate style rules.

According to the given Java coding standard, which one of these is not a good name?

b

Explanation: checkWeight is an action. Naming variables as actions makes the code harder to follow. isWeightValid may be a better name.

Guideline: Name Well

Introduction

๐Ÿ† Can explain the need for good names in code

Proper naming improves the readability. It also reduces bugs caused by ambiguities regarding the intent of a variable or a method.

There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton

Basic

๐Ÿ† Can follow basic guidelines for naming

Use nouns for classes/variables and verbs for methods/functions.

Examples:

Name for a
Class CheckLimit LimitChecker
method result() calculate()

Distinguish clearly between single valued and multivalued variables.

Examples:

Person student
ArrayList<Person> students

Use correct spelling in names. Avoid 'texting-style' spelling. Avoid foreign language words, slang, and names that are only meaningful within specific contexts/times e.g. terms from private jokes, a TV show currently popular in your country

Intermediate

๐Ÿ† Can follow intermediate guidelines for naming

A name is not just for differentiation; it should explain the named entity to the reader accurately and at a sufficient level of detail.

Examples:

processInput() (what 'process'?) removeWhiteSpaceFromInput()
flag isValidInput
temp

If the name has multiple words, they should be in a sensible order.

Examples:

bySizeOrder() orderBySize()

Imagine going to the doctor's and saying "My eye1 is swollen"! Donโ€™t use numbers or case to distinguish names.

Examples:

value1, value2 value, Value originalValue, finalValue

While it is preferable not to have lengthy names, names that are 'too short' are even worse. If you must abbreviate or use acronyms, do it consistently. Explain their full meaning at an obvious location.

Related things should be named similarly, while unrelated things should NOT.

Example: Consider these variables

  • colorBlack : hex value for color black
  • colorWhite : hex value for color white
  • colorBlue : number of times blue is used
  • hexForRed : : hex value for color red

This is misleading because colorBlue is named similar to colorWhite and colorBlack but has a different purpose while hexForRed is named differently but has very similar purpose to the first two variables. The following is better:

  • hexForBlack hexForWhite hexForRed
  • blueColorCount

Avoid misleading or ambiguous names (e.g. those with multiple meanings), similar sounding names, hard-to-pronounce ones (e.g. avoid ambiguities like "is that a lowercase L, capital I or number 1?", or "is that number 0 or letter O?"), almost similar names.

Examples:

Reason
phase0 phaseZero Is that zero or letter O?
rwrLgtDirn rowerLegitDirection Hard to pronounce
right left wrong rightDirection leftDirection wrongResponse right is for 'correct' or 'opposite of 'left'?
redBooks readBooks redColorBooks booksRead red and read (past tense) sounds the same
FiletMignon egg If the requirement is just a name of a food, egg is a much easier to type/say choice than FiletMignon

Guideline: Avoid Unsafe Shortcuts

Introduction

๐Ÿ† Can explain the need for avoiding error-prone shortcuts

It is safer to use language constructs in the way they are meant to be used, even if the language allows shortcuts. Some such coding practices are common sources of bugs. Know them and avoid them.

Basic

๐Ÿ† Can follow basic guidelines for avoiding unsafe shortcuts

Always include a default branch in case statements.

Furthermore, use it for the intended default action and not just to execute the last option. If there is no default action, you can use the 'default' branch to detect errors (i.e. if execution reached the default branch, throw an exception). This also applies to the final else of an if-else construct. That is, the final else should mean 'everything else', and not the final option. Do not use else when an if condition can be explicitly specified, unless there is absolutely no other possibility.

if (red) print "red";
else print "blue";

if (red) print "red";
else if (blue) print "blue";
else error("incorrect input");
  • Use one variable for one purpose. Do not reuse a variable for a different purpose other than its intended one, just because the data type is the same.
  • Do not reuse formal parameters as local variables inside the method.

double computeRectangleArea(double length, double width) {
    length = length * width;
    return length;
}

double computeRectangleArea(double length, double width) {
    double area;
    area = length * width;
    return area;
}

Never write an empty catch statement. At least give a comment to explain why the catch block is left empty.

We all feel reluctant to delete code we have painstakingly written, even if we have no use for that code any more ("I spent a lot of time writing that code; what if we need it again?"). Consider all code as baggage you have to carry; get rid of unused code the moment it becomes redundant. If you need that code again, simply recover it from the revision control tool you are using. Deleting code you wrote previously is a sign that you are improving.

Which of these are unsafe coding practices?

  • a. case statements without a default clause

  • b. Omitting braces when a code block has only one statement

    e.g writing this

    if(isProper)
        return PROPER_AMOUNT;
    

    instead of

    if(isProper){
        return PROPER_AMOUNT;
    }
    
  • c. Using a variable just to explain whatโ€™s going on

    e.g.writing this

    boolean isProper = notNull && notEmpty;
    return isProper;
    

    instead of

    return notNull && notEmpty;
    

a, b.

Reason why [c] is not a bad practice: The extra variable helps to make the code more understandable because it makes the intent of the formula clearer.

Intermediate

๐Ÿ† Can follow intermediate guidelines for avoiding unsafe shortcuts

Minimize global variables. 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.

Define variables in the least possible scope. For example, if the variable is used only within the if block of the conditional statement, it should be declared inside that if block.

The most powerful technique for minimizing the scope of a local variable is to declare it where it is first used. -- Effective Java, by Joshua Bloch

๐Ÿ“Ž Resources:

Code duplication, especially when you copy-paste-modify code, often indicates a poor quality implementation. While it may not be possible to have zero duplication, always think twice before duplicating code; most often there is a better alternative.

This guideline is closely related to the DRY Principle .

Supplmentary โ†’ Principles โ†’

DRY Principle

DRY (Don't Repeat Yourself) Principle: Every piece of knowledge must have a single, unambiguous, authoritative representation within a system The Pragmatic Programmer, by Andy Hunt and Dave Thomas

This principle guards against duplication of information.

๐Ÿ“ฆ The functionality implemented twice is a violation of the DRY principle even if the two implementations are different.

๐Ÿ“ฆ The value a system-wide timeout being defined in multiple places is a violation of DRY.

Guideline: Comment Minimally, but Sufficiently

Introduction

๐Ÿ† Can explain the need for commenting minimally but sufficiently

Good code is its own best documentation. As youโ€™re about to add a comment, ask yourself, โ€˜How can I improve the code so that this comment isnโ€™t needed?โ€™ Improve the code and then document it to make it even clearer. --Steve McConnell, Author of Clean Code

Some think commenting heavily increases the 'code quality'. This is not so. Avoid writing comments to explain bad code. Improve the code to make it self-explanatory.

Basic

๐Ÿ† Can follow basic guidelines for writing code comments

If the code is self-explanatory, refrain from repeating the description in a comment just for the sake of 'good documentation'.

// increment x
x++;

//trim the input
trimInput();

Do not write comments as if they are private notes to self. Instead, write them well enough to be understood by another programmer. One type of comments that is almost always useful is the header comment that you write for a class or an operation to explain its purpose.

Reason: this comment will only make sense to the person who wrote it

// a quick trim function used to fix bug I detected overnight
void trimInput(){
    ....
}

/** Trims the input of leading and trailing spaces */
void trimInput(){
    ....
}

Intermediate

๐Ÿ† Can follow intermediate guidelines for writing code comments

Comments should explain what and why aspect of the code, rather than the how aspect.

๐Ÿ‘ What : The specification of what the code supposed to do. The reader can compare such comments to the implementation to verify if the implementation is correct

๐Ÿ“ฆ Example: This method is possibly buggy because the implementation does not seem to match the comment. In this case the comment could help the reader to detect the bug.

/** Removes all spaces from the {@code input} */
void compact(String input){
    input.trim();
}

๐Ÿ‘ Why : The rationale for the current implementation.

๐Ÿ“ฆ Example: Without this comment, the reader will not know the reason for calling this method.

// Remove spaces to comply with IE23.5 formatting rules
compact(input);

๐Ÿ‘Ž How : The explanation for how the code works. This should already be apparent from the code, if the code is self-explanatory. Adding comments to explain the same thing is redundant.

๐Ÿ“ฆ Example:

Reason: Comment explains how the code works.

// return true if both left end and right end are correct or the size has not incremented
return (left && right) || (input.size() == size);

Reason: Code refactored to be self-explanatory. Comment no longer needed.


boolean isSameSize = (input.size() == size) ;
return (isLeftEndCorrect && isRightEndCorrect) || isSameSize;

In general, comments should describe,

  • a. WHAT the code does
  • b. WHY the code does something
  • c. HOW the code does something

a, b

Explanation: How the code does something should be apparent from the code itself. However, comments can help the reader in describing WHAT and WHY aspects of the code.