Week 10 [Mon, Oct 21st] - Topics

Detailed Table of Contents



Guidance for the item(s) below:

Let us learn a UML diagram type that can be used to model a behavioral aspect of software entities; in contrast, class/object diagrams that you learned earlier model structural aspects.

[W10.1] Sequence Diagrams: Basics

W10.1a

Design → Modelling → Modelling Behaviors → Sequence diagrams - basic

Video

Can draw basic sequence diagrams

Sequence diagrams model the interactions between various entities in a system, in a specific scenario. Modelling such scenarios is useful, for example, to verify the design of the internal interactions is able to provide the expected outcomes.

Some examples where a sequence diagram can be used:

To model how components of a system interact with each other to respond to a user action.

To model how objects inside a component interact with each other to respond to a method call it received from another component.

Contents related to UML diagrams in the panels given below belong to a different chapter (i.e., the chapter dedicated to UML); they have been embedded here for convenience.

UML Sequence Diagrams → Introduction

Loading...

UML Sequence Diagrams → Basic Notation

Loading...

UML Sequence Diagrams → Loops

Loading...

UML Sequence Diagrams → Object Creation

Loading...

UML Sequence Diagrams → Minimal Notation

Loading...


Exercises:

Explain Sequence Diagram about Machine


Draw a Sequence Diagram for the code (PersonList, Person, Tag)


Find notation errors in Sequence Diagram




Follow up notes for the item(s) above:

Now that you know the basic notations for the sequence diagrams, here is a worked example of drawing sequence diagrams (basic notation) to match a code snippet.

Video Sequence diagrams (basic) - Item creation

Guidance for the item(s) below:

Next, let's learn about some guidelines that can apply to improve the quality of your code.

[W10.2] Code Quality: Guidelines


Intro

W10.2a

Implementation → Code Quality → Introduction → What

Video

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 on software, poor quality code is something no one can afford to tolerate.



Coding Standards

W10.2b

Implementation → Code Quality → Style → Introduction

Video

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 usually follow a strict coding standard (aka style guide).

The aim of a coding standard is to make the entire codebase look like it was written by one person. A coding standard is usually specific to a programming language and specifies guidelines such as the locations 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 uses the same coding standard and that the standard is generally not inconsistent with typical industry practices. If a company's coding standard is very different from what is typically used 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.


Exercises:

What is the recommended approach regarding coding standards?



W10.2c

Implementation → Code Quality → Style → What

Can follow simple mechanical style rules

Go through the Java coding standard at @SE-EDU and learn the basic style rules.


Exercises:

Find basic coding standard violations



W10.2d : OPTIONAL

Implementation → Code Quality → Style → Intermediate



Readability

Video

W10.2e

Implementation → Code Quality → 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 readability (aka 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 you do not intend to pass the code to someone else, code quality is still important because you will become a 'stranger' to your own code someday.


W10.2f

Implementation → Code Quality → Readability → Basic → Avoid long methods

Can improve code quality using technique: avoid long methods

Avoid long methods as they often contain more information than what the reader can process at a time. Consider if shortening is possible when a method goes beyond 30 . The bigger the haystack, the harder it is to find a needle.


W10.2g

Implementation → Code Quality → Readability → Basic → Avoid deep nesting

Can improve code quality using technique: avoid deep nesting

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

Avoid deep nesting -- the deeper the nesting, the harder it is for the reader to keep track of the logic.

In particular, avoid arrowhead style code.

A real code example:

Bad

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

Good

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

Bad

def calculate_subs():
    if not age:
        if not sub:
            if not not_fulltime:
                subsidy = 500
            else:
                subsidy = 250
        else:
            subsidy = 250
    else:
        subsidy = -1
    return subsidy
  

Good

def calculate_subsidy():
    if is_senior:
        return REJECT_SENIOR
    elif is_already_subsidized:
        return SUBSIDIZED_SUBSIDY
    elif is_parttime:
        return FULLTIME_SUBSIDY * RATIO
    else:
        return FULLTIME_SUBSIDY

W10.2h

Implementation → Code Quality → Readability → Basic → Avoid complicated expressions

Can improve code quality using technique: avoid complicated expressions

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).

Bad

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

Good

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

boolean isUrgent = typeCode == URGENT;

return isValidCode && isUrgent;

Bad

return ((length < MAX_LENGTH) or (previous_size != length)) and (type_code == URGENT)

Good

is_within_size_limit = length < MAX_LENGTH
is_same_size = previous_size != length
is_valid_code = is_within_size_limit or is_same_size

is_urgent = type_code == URGENT

return is_valid_code and is_urgent

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


W10.2i

Implementation → Code Quality → Readability → Basic → Avoid magic numbers

Can improve code quality using technique: avoid magic numbers

Avoid magic numbers in your code. When the code has a number that does not explain the meaning of the number, it is called a "magic number" (as in "the number appears as if by magic"). Using a makes the code easier to understand because the name tells us more about the meaning of the number.

Bad

return 3.14236;
...
return 9;
  

Good

static final double PI = 3.14236;
static final int MAX_SIZE = 10;
...
return PI;
...
return MAX_SIZE - 1;

Note: Python does not have a way to make a variable a constant. However, you can use a normal variable with an ALL_CAPS name to simulate a constant.

Bad

return 3.14236
...
return 9
  

Good

PI = 3.14236
MAX_SIZE = 10
...
return PI
...
return MAX_SIZE - 1

Similarly, you can have ‘magic’ values of other data types.

Bad

return "Error 1432"; // A magic string!
return "Error 1432" # A magic string!

Avoid any magic literals in general, not just magic numbers.


W10.2j

Implementation → Code Quality → Readability → Basic → Make the code obvious

Can improve code quality using technique: make the code obvious

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

  • [Java] Use explicit type conversion instead of implicit type conversion.
  • [Java, Python] Use parentheses/braces to show groupings even when they can be skipped.
  • [Java, Python] 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'.

W10.2k

Implementation → Code Quality → Readability → Intermediate → Structure code logically

Can improve code quality using technique: structure code logically

Lay out the code so that it adheres to the logical structure. The code should read like a story. Just like how you 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 separate groups of related statements.

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.

Bad

statement A1
statement A2
statement A3
statement B1
statement C1
statement B2
statement C2
  

Good

statement A1
statement A2
statement A3

statement B1
statement B2

statement C1
statement C2

W10.2l : OPTIONAL

Implementation → Code Quality → Readability → Intermediate → Do not 'Trip Up' reader


W10.2m : OPTIONAL

Implementation → Code Quality → Readability → Intermediate → Practice KISSing


W10.2n : OPTIONAL

Implementation → Code Quality → Readability → Intermediate → Avoid premature optimizations


W10.2o : OPTIONAL

Implementation → Code Quality → Readability → Intermediate → SLAP hard


W10.2p : OPTIONAL

Implementation → Code Quality → Readability → Advanced → Make the happy path prominent



Naming

W10.2q

Implementation → Code Quality → Naming → Introduction

Can explain the need for good names in code

Proper naming improves the readability of code. 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


W10.2r

Implementation → Code Quality → Naming → Basic → Use nouns for things and verbs for actions

Can improve code quality using technique: use nouns for things and verbs for actions

Every system is built from a domain-specific language designed by the programmers to describe that system. Functions are the verbs of that language, and classes are the nouns.
-- Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship

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

Name for a Bad Good
Class CheckLimit LimitChecker
Method result() calculate()

Distinguish clearly between single-valued and multi-valued variables.

Good

Person student;
ArrayList<Person> students;

Good

name = 'Jim'
names = ['Jim', 'Alice']

W10.2s

Implementation → Code Quality → Naming → Basic → Use standard words

Can improve code quality using technique: use standard words

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.


W10.2t : OPTIONAL

Implementation → Code Quality → Naming → Intermediate → Use name to explain


W10.2u : OPTIONAL

Implementation → Code Quality → Naming → Intermediate → Not too long, not too short


W10.2v : OPTIONAL

Implementation → Code Quality → Naming → Intermediate → Avoid misleading names



Code Comments

Video

W10.2w

Implementation → Code Quality → Comments → 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'. That is not so. Avoid writing comments to explain bad code. Improve the code to make it self-explanatory.


W10.2x

Implementation → Code Quality → Comments → Basic → Do not repeat the obvious

Can improve code quality using technique: do not repeat the obvious

Do not repeat in comments information that is already obvious from the code. If the code is self-explanatory, a comment may not be needed.

Bad

//increment x
x++;

//trim the input
trimInput();

Bad

# increment x
x = x + 1

# trim the input
trim_input()

W10.2y

Implementation → Code Quality → Comments → Basic → Write to the reader

Can improve code quality using technique: write to the reader

Write comments targeting other programmers reading the code. Do not write comments as if they are private notes to yourself. Instead, One type of comment that is almost always useful is the header comment that you write for a class or an operation to explain its purpose.

Bad 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() {
    ....
}

Good

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

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

def trim_input():
"""a quick trim function used to fix bug I detected overnight"""
    ...

Good

def trim_input():
"""Trim the input of leading and trailing spaces"""
    ...

W10.2z : OPTIONAL

Implementation → Code Quality → Comments → Intermediate → Explain WHAT and WHY, not HOW



Unsafe Practices

W10.2A : OPTIONAL

Implementation → Code Quality → Error-Prone Practices → Introduction


W10.2B : OPTIONAL

Implementation → Code Quality → Error-Prone Practices → Basic → Use the default branch


W10.2C : OPTIONAL

Implementation → Code Quality → Error-Prone Practices → Basic → Don't recycle variables or parameters


W10.2D : OPTIONAL

Implementation → Code Quality → Error-Prone Practices → Basic → Avoid empty catch blocks


W10.2E : OPTIONAL

Implementation → Code Quality → Error-Prone Practices → Basic → Delete dead code


W10.2F : OPTIONAL

Implementation → Code Quality → Error-Prone Practices → Intermediate → Minimize scope of variables


W10.2G : OPTIONAL

Implementation → Code Quality → Error-Prone Practices → Intermediate → Minimize code duplication



Guidance for the item(s) below:

The topic of refactoring was covered in an ealier week. We repeat the topic below for your convenience as you'll need to refactor your code when applying the code quality guidelines given above.

[W10.3] Code Quality: Refactoring (Revisited)

Video

W10.3a

Implementation → Refactoring → What

Can explain refactoring

The process of improving a program's internal structure in small steps without modifying its external behavior is called refactoring. Refactoring is needed because the first version of the code you write may not be of production quality. It is OK to first concentrate on making the code work, rather than worry over the quality of the code, as long as you improve the quality later.

  • Refactoring is not rewriting: Discarding poorly-written code entirely and re-writing it from scratch is not refactoring because refactoring needs to be done in small steps.
  • Refactoring is not bug fixing: By definition, refactoring is different from bug fixing or any other modifications that alter the external behavior (e.g. adding a feature) of the component in concern.

Refactoring code can have many secondary benefits e.g.

  • hidden bugs become easier to spot
  • improve performance (sometimes, simpler code runs faster than complex code because simpler code is easier for the compiler to optimize).

Given below are two common refactorings (more).

Refactoring Name: Consolidate Duplicate Conditional Fragments

Situation: The same fragment of code is in all branches of a conditional expression.

Method: Move it outside of the expression.

Example:

if (isSpecialDeal()) {
    total = price * 0.95;
    send();
} else {
    total = price * 0.98;
    send();
}
 → 
if (isSpecialDeal()) {
    total = price * 0.95;
} else {
    total = price * 0.98;
}
send();

if is_special_deal:
    total = price * 0.95
    send()
else:
    total = price * 0.98
    send()
 → 
if is_special_deal:
    total = price * 0.95
else:
    total = price * 0.98

send()

Refactoring Name: Extract Method

Situation: You have a code fragment that can be grouped together.

Method: Turn the fragment into a method whose name explains the purpose of the method.

Example:

void printOwing() {
    printBanner();

    // print details
    System.out.println("name:	" + name);
    System.out.println("amount	" + getOutstanding());
}

void printOwing() {
    printBanner();
    printDetails(getOutstanding());
}

void printDetails(double outstanding) {
    System.out.println("name:	" + name);
    System.out.println("amount	" + outstanding);
}
def print_owing():
    print_banner()

    # print details
    print("name:	" + name)
    print("amount	" + get_outstanding())

def print_owing():
    print_banner()
    print_details(get_outstanding())

def print_details(amount):
    print("name:	" + name)
    print("amount	" + amount)

Some IDEs have builtin support for basic refactorings such as automatically renaming a variable/method/class in all places it has been used.

Refactoring, even if done with the aid of an IDE, may still result in regressions. Therefore, each small refactoring should be followed by regression testing.


Exercises:

Results of Refactoring


Do you agree with the following statement? Refactoring and regression testing


Explain Refactoring



W10.3b

Tools → IntelliJ IDEA → Refactoring

Can use automated refactoring features of the IDE

This video explains how to automate the 'Extract variable' refactoring using IntelliJ IDEA. Most other refactorings available work similarly. i.e. select the code to refactorfind the refactoring in the context menu or use the keyboard shortcut.

Here's another video explaining how to do some more useful refactorings in IntelliJ IDEA.


W10.3c

Implementation → Refactoring → How

W10.3d : OPTIONAL

Implementation → Refactoring → When



Guidance for the item(s) below:

Previously, you learned how to use exceptions to handle errors. Here are two more techniques that can help with the error handling aspect of programming.

[W10.4] Error Handling: Logging and Assertions


Logging

Video

W10.4a

Implementation → Error Handling → Logging → What

Can explain logging

Logging is the deliberate recording of certain information during a program execution for future reference. Logs are typically written to a log file but it is also possible to log information in other ways e.g. into a database or a remote server.

Logging can be useful for troubleshooting problems. A good logging system records some system information regularly. When bad things happen to a system e.g. an unanticipated failure, their associated log files may provide indications of what went wrong and actions can then be taken to prevent it from happening again.

A log file is like the of an airplane; they don't prevent problems but they can be helpful in understanding what went wrong after the fact.


Exercises:

Logging vs blackbox



W10.4b : OPTIONAL

Implementation → Error Handling → Logging → How



Assertions

W10.4c

Implementation → Error Handling → Assertions → What

Video

Can explain assertions

Assertions are used to define assumptions about the program state so that the runtime can verify them. An assertion failure indicates a possible bug in the code because the code has resulted in a program state that violates an assumption about how the code should behave.

An assertion can be used to express something like when the execution comes to this point, the variable v cannot be null.

If the runtime detects an assertion failure, it typically takes some drastic action such as terminating the execution with an error message. This is because an assertion failure indicates a possible bug and the sooner the execution stops, the safer it is.

In the Java code below, suppose you set an assertion that timeout returned by Config.getTimeout() is greater than 0. Now, if Config.getTimeout() returns -1 in a specific execution of this line, the runtime can detect it as an assertion failure -- i.e. an assumption about the expected behavior of the code turned out to be wrong which could potentially be the result of a bug -- and take some drastic action such as terminating the execution.

int timeout = Config.getTimeout();
// set assertion here ...

W10.4d

Implementation → Error Handling → Assertions → How

Can use assertions

Use the assert keyword to define assertions.

This assertion will fail with the message x should be 0 if x is not 0 at this point.

x = getX();
assert x == 0 : "x should be 0";
...

Assertions can be disabled without modifying the code.

java -enableassertions HelloWorld (or java -ea HelloWorld) will run HelloWorld with assertions enabled while java -disableassertions HelloWorld will run it without verifying assertions.

Java disables assertions by default. This could create a situation where you think all assertions are being verified as true while in fact they are not being verified at all. Therefore, remember to enable assertions when you run the program if you want them to be in effect.

Enable assertions in IntelliJ (how?) and get an assertion to fail temporarily (e.g. insert an assert false into the code temporarily) to confirm assertions are being verified.

Java assert vs JUnit assertions: Both check for a given condition but JUnit assertions are more powerful and customized for testing. In addition, JUnit assertions are not disabled by default. Use JUnit assertions in test code and Java assert in functional code.


Resources:

Tutorials:

Best practices:


W10.4e

Implementation → Error Handling → Assertions → When

Can use assertions optimally

It is recommended that assertions be used liberally in the code. Their impact on performance is low, and worth the additional safety they provide.

Do not use assertions to do work because assertions can be disabled. If not, your program will stop working when assertions are not enabled.

The code below will not invoke the writeFile() method when assertions are disabled. If that method is performing some work that is necessary for your program, your program will not work correctly when assertions are disabled.

...
assert writeFile() : "File writing is supposed to return true";

Assertions are suitable for verifying assumptions about Internal Invariants, Control-Flow Invariants, Preconditions, Postconditions, and Class Invariants. Refer to Programming with Assertions (second half) to learn more.

Exceptions and assertions are two complementary ways of handling errors in software but they serve different purposes. Therefore, both assertions and exceptions should be used in code.

  • The raising of an exception indicates an unusual condition created by the user (e.g. user inputs an unacceptable input) or the environment (e.g., a file needed for the program is missing).
  • An assertion failure indicates the programmer made a mistake in the code (e.g., a null value is returned from a method that is not supposed to return null under any circumstances).

Exercises:

Assertion failure in Calculator


Statement about exceptions and assertions




[W10.5] Design Patterns : OPTIONAL


Introduction

Video

W10.5a : OPTIONAL

Design → Design Patterns → Introduction → What


W10.5b : OPTIONAL

Design → Design Patterns → Introduction → Format



Singleton pattern

Video

W10.5c : OPTIONAL

Design → Design Patterns → Singleton → What


W10.5d : OPTIONAL

Design → Design Patterns → Singleton → Implementation


W10.5e : OPTIONAL

Design → Design Patterns → Singleton → Evaluation



Facade pattern

Video

W10.5f : OPTIONAL

Design → Design Patterns → Facade Pattern → What



Command pattern

W10.5g : OPTIONAL

Design → Design Patterns → Command Pattern → What



Abstraction Occurrence pattern

W10.5h : OPTIONAL

Design → Design Patterns → Abstraction Occurrence Pattern → What