This site is from a past semester! The current version will be here when the new semester starts.
CS2113/T 2021 Jan-May
  • Full Timeline
  • Week 1 [Mon, Jan 11th]
  • Week 2 [Mon, Jun 21st]
  • Week 3 [Mon, Jun 28th]
  • Week 4 [Mon, Jul 5th]
  • Week 5 [Mon, Jul 12th]
  • Week 6 [Mon, Jul 19th]
  • Week 7 [Mon, Jul 26th]
  • Week 8 [Mon, Aug 9th]
  • Week 9 [Mon, Aug 16th]
  • Week 10 [Mon, Aug 23rd]
  • Week 11 [Mon, Aug 30th]
  • Week 12 [Mon, Sep 6th]
  • Week 13 [Mon, Sep 13th]
  • Textbook
  • Admin Info
  • Dashboards
  •  Individual Project (iP):
  • Individual Project Info
  • iP Upstream Repo
  • iP Code Dashboard
  • iP Progress Dashboard

  •  Team Project (tP):
  • Team Project Info
  • Reference AB3
  • Team List
  • tP Code Dashboard
  • tP Progress Dashboard
  • Java exercises
  • Report Bugs
  • Forum
  • Gitter (Chat)
  • Instructors
  • Announcements
  • Files (handouts, submissions etc.)
  • Tutorial Schedule
  • Java Coding Standard
  • Git Conventions
  • Forum Activities Dashboard
  • Participation Dashboard
  • Week 4 [Mon, Jul 5th] - Topics

    • [W4.1] OOP + Java: Inheritance
    • [W4.1a] Paradigms → OOP → Inheritance → What

    • [W4.1b] Paradigms → OOP → Inheritance → Overloading

    • [W4.1c] Paradigms → OOP → Inheritance → Overriding

    • [W4.1d] C++ to Java → Inheritance → Inheritance (Basics)

    • [W4.1e] C++ to Java → Inheritance → The Object class

    • [W4.2] OOP + Java: Polymorphism
    • [W4.2a] Paradigms → OOP → Polymorphism → What

    • [W4.2b] Paradigms → OOP → Inheritance → Substitutability

    • [W4.2c] Paradigms → OOP → Inheritance → Dynamic and static binding

    • [W4.2d] Paradigms → OOP → Polymorphism → How

    • [W4.2e] C++ to Java → Inheritance → Polymorphism

    • [W4.3] Java: Constants
    • [W4.3a] C++ to Java → Miscellaneous Topics → Constants
    • [W4.4] OOP + Java: Enumerations
    • [W4.4a] Paradigms → OOP → Classes → Enumerations

    • [W4.4b] C++ to Java → Miscellaneous Topics → Enumerations

    • [W4.5] Automated Testing of Text UIs
    • [W4.5a] Quality Assurance → Testing → Introduction → What

    • [W4.5b] Quality Assurance → Testing → Regression Testing → What

    • [W4.5c] Quality Assurance → Testing → Test Automation → What

    • [W4.5d] Quality Assurance → Testing → Test Automation → Automated testing of CLI applications

    • [W4.6] Code Quality: Readability
    • [W4.6a] Implementation → Code Quality → Readability → Introduction

    • [W4.6b] Implementation → Code Quality → Readability → Basic → Avoid long methods

    • [W4.6c] Implementation → Code Quality → Readability → Basic → Avoid deep nesting

    • [W4.6d] Implementation → Code Quality → Readability → Basic → Avoid complicated expressions

    • [W4.6e] Implementation → Code Quality → Readability → Basic → Avoid magic numbers

    • [W4.6f] Implementation → Code Quality → Readability → Basic → Make the code obvious

    • [W4.6g] Implementation → Code Quality → Readability → Intermediate → Structure code logically

    • [W4.6h] Implementation → Code Quality → Readability → Intermediate → Do not 'Trip Up' reader

    • [W4.6i] Implementation → Code Quality → Readability → Intermediate → Practice KISSing

    • [W4.6j] Implementation → Code Quality → Readability → Intermediate → Avoid premature optimizations

    • [W4.6k] Implementation → Code Quality → Readability → Intermediate → SLAP hard

    • [W4.6l] Implementation → Code Quality → Readability → Advanced → Make the happy path prominent

    • [W4.7] Code Quality: Refactoring
    • [W4.7a] Implementation → Refactoring → What

    • [W4.7b] Tools → IntelliJ IDEA → Refactoring : OPTIONAL

    • [W4.7c] Implementation → Refactoring → How

    • [W4.7d] Implementation → Refactoring → When : OPTIONAL

    • [W4.8] RCS: Creating Pull Requests
    • [W4.8a] Tools → Git and GitHub → Creating PRs

    • [W4.8b] Tools → Git and GitHub → Reviewing PRs


    Guidance for the item(s) below:

    Now that you know the basics about classes and objects, let's move to the next level. The sections below explain the third core concept of OOP (called inheritance) and how to use that in Java.

    1. abstraction
    2. encapsulation
    3. inheritance
    4. ...

    [W4.1] OOP + Java: Inheritance

    W4.1a

    Paradigms → OOP → Inheritance → What

    Can explain the meaning of inheritance

    The OOP concept Inheritance allows you to define a new class based on an existing class.

    For example, you can use inheritance to define an EvaluationReport class based on an existing Report class so that the EvaluationReport class does not have to duplicate data/behaviors that are already implemented in the Report class. The EvaluationReport can inherit the wordCount attribute and the print() method from the base class Report.

    • Other names for Base class: Parent class, Superclass
    • Other names for Derived class: Child class, Subclass, Extended class

    A superclass is said to be more general than the subclass. Conversely, a subclass is said to be more specialized than the superclass.

    Applying inheritance on a group of similar classes can result in the common parts among classes being extracted into more general classes.

    Man and Woman behave the same way for certain things. However, the two classes cannot be simply replaced with a more general class Person because of the need to distinguish between Man and Woman for certain other things. A solution is to add the Person class as a superclass (to contain the code common to men and women) and let Man and Woman inherit from Person class.

    Inheritance implies the derived class can be considered as a sub-type of the base class (and the base class is a super-type of the derived class), resulting in an is a relationship.

    Inheritance does not necessarily mean a sub-type relationship exists. However, the two often go hand-in-hand. For simplicity, at this point let us assume inheritance implies a sub-type relationship.

    To continue the previous example,

    • Woman is a Person
    • Man is a Person

    Inheritance relationships through a chain of classes can result in inheritance hierarchies (aka inheritance trees).

    Two inheritance hierarchies/trees are given below. Note that the triangle points to the parent class. Observe how the Parrot is a Bird as well as it is an Animal.

    Multiple Inheritance is when a class inherits directly from multiple classes. Multiple inheritance among classes is allowed in some languages (e.g., Python, C++) but not in other languages (e.g., Java, C#).

    The Honey class inherits from the Food class and the Medicine class because honey can be consumed as a food as well as a medicine (in some oriental medicine practices). Similarly, a Car is a Vehicle, an Asset and a Liability.

    Which of these are correct?

    • a. Superclass is more general than the subclass.
    • b. Child class is more specialized than the parent class.
    • c. A class can inherit behavior from its ancestor classes (ancestor classes = classes above it in the inheritance hierarchy).
    • d. Code reuse can be one benefit of inheritance.
    • e. A change to the superclass will not affect its subclasses.

    (a) (b) (c) (d)

    Explanation: (e) is incorrect. Because subclasses inherit behavior from the superclass, any changes to the superclass could affect subclasses.

    W4.1b

    Paradigms → OOP → Inheritance → Overloading

    Can explain method overloading

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Example:

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int).

    • calculate(String): void
    • calculate(int): void

    W4.1c

    Paradigms → OOP → Inheritance → Overriding

    Can explain method overriding

    Method overriding is when a sub-class changes the behavior inherited from the parent class by re-implementing the method. Overridden methods have the same name, same type signature, and same return type.

    Consider the following case of EvaluationReport class inheriting the Report class:

    Report methods EvaluationReport methods Overrides?
    print() print() Yes
    write(String) write(String) Yes
    read():String read(int):String No. Reason: the two methods have different signatures; This is a case of overloading (rather than overriding).

    Paradigms → OOP → Inheritance →

    Overloading

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Example:

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int).

    • calculate(String): void
    • calculate(int): void

    Which of these methods override another method? A is the parent class. B inherits A.

    • a
    • b
    • c
    • d
    • e

    d

    Explanation: Method overriding requires a method in a child class to use the same method name and same parameter sequence used by one of its ancestors.

    W4.1d

    C++ to Java → Inheritance → Inheritance (Basics)

    Can use basic inheritance

    Given below is an extract from the -- Java Tutorial, with slight adaptations.

    A class that is derived from another class is called a subclass (also a derived class, extended class, or child class). The class from which the subclass is derived is called a superclass (also a base class or a parent class).

    A subclass inherits all the members (fields, methods, and nested classes) from its superclass. Constructors are not members, so they are not inherited by subclasses, but the constructor of the superclass can be invoked from the subclass.

    Every class has one and only one direct superclass (single inheritance), except the Object class, which has no superclass, . In the absence of any other explicit superclass, every class is implicitly a subclass of Object. Classes can be derived from classes that are derived from classes that are derived from classes, and so on, and ultimately derived from the topmost class, Object. Such a class is said to be descended from all the classes in the inheritance chain stretching back to Object. Java does not support multiple inheritance among classes.

    The java.lang.Object class defines and implements behavior common to all classes—including the ones that you write. In the Java platform, many classes derive directly from Object, other classes derive from some of those classes, and so on, forming a single hierarchy of classes.

    The keyword extends indicates one class inheriting from another.

    Here is the sample code for a possible implementation of a Bicycle class and a MountainBike class that is a subclass of the Bicycle:

    public class Bicycle {
    
        public int gear;
        public int speed;
    
        public Bicycle(int startSpeed, int startGear) {
            gear = startGear;
            speed = startSpeed;
        }
    
        public void setGear(int newValue) {
            gear = newValue;
        }
    
        public void applyBrake(int decrement) {
            speed -= decrement;
        }
    
        public void speedUp(int increment) {
            speed += increment;
        }
    
    }
    
    public class MountainBike extends Bicycle {
    
        // the MountainBike subclass adds one field
        public int seatHeight;
    
        // the MountainBike subclass has one constructor
        public MountainBike(int startHeight, int startSpeed, int startGear) {
            super(startSpeed, startGear);
            seatHeight = startHeight;
        }
    
        // the MountainBike subclass adds one method
        public void setHeight(int newValue) {
            seatHeight = newValue;
        }
    }
    

    A subclass inherits all the fields and methods of the superclass. In the example above, MountainBike inherits all the fields and methods of Bicycle and adds the field seatHeight and a method to set it.

    Accessing superclass members

    If your method overrides one of its superclass's methods, you can invoke the overridden method through the use of the keyword super. You can also use super to refer to a when both the superclass and the subclass use the same variable name, the superclass variables is said to be hidden/shadowed by the subclass variablehidden field (although hiding fields is discouraged).

    Consider this class, Superclass and a subclass, called Subclass, that overrides printMethod():

    public class Superclass {
    
        public void printMethod() {
            System.out.println("Printed in Superclass.");
        }
    }
    
    public class Subclass extends Superclass {
    
        // overrides printMethod in Superclass
        public void printMethod() {
            super.printMethod();
            System.out.println("Printed in Subclass");
        }
        public static void main(String[] args) {
            Subclass s = new Subclass();
            s.printMethod();
        }
    }
    

    Printed in Superclass.
    Printed in Subclass
    

    Within Subclass, the simple name printMethod() refers to the one declared in Subclass, which overrides the one in Superclass. So, to refer to printMethod() inherited from Superclass, Subclass must use a qualified name, using super as shown.

    Subclass constructors

    A subclass constructor can invoke the superclass constructor. Invocation of a superclass constructor must be the first line in the subclass constructor. The syntax for calling a superclass constructor is super() (which invokes the no-argument constructor of the superclass) or super(parameters) (to invoke the superclass constructor with a matching parameter list).

    The following example illustrates how to use the super keyword to invoke a superclass's constructor. Recall from the Bicycle example that MountainBike is a subclass of Bicycle. Here is the MountainBike (subclass) constructor that calls the superclass constructor and then adds some initialization code of its own (i.e., seatHeight = startHeight;):

    public MountainBike(int startHeight, int startSpeed, int startGear) {
        super(startSpeed, startGear);
        seatHeight = startHeight;
    }
    

    Note: If a constructor does not explicitly invoke a superclass constructor, the Java compiler automatically inserts a call to the no-argument constructor of the superclass. If the superclass does not have a no-argument constructor, you will get a compile-time error. Object does have such a constructor, so if Object is the only superclass, there is no problem.

    Access modifiers (simplified)

    Access level modifiers determine whether other classes can use a particular field or invoke a particular method. Given below is a simplified version of Java access modifiers, assuming you have not yet started placing your classes in different packages i.e., all classes are placed in the root level. A full explanation of access modifiers is given in a later topic.

    There are two levels of access control:

    1. At the class level:

      • public: the class is visible to all other classes
      • no modifier: same as public

    2. At the member level:

      • public : the member is visible to all other classes
      • no modifier: same as public
      • protected: the member is visible to sub classes only
      • private: the member is not visible to other classes (but can be accessed in its own class)

    Background: Suppose we are creating a software to manage various tasks a person has to do. Two types of such tasks are,

    • Todos: i.e., things that needs to be done some day e.g., 'Read the book Lord of the Rings'
    • Deadlines: i.e., things to be done by a specific date/time e.g., 'Read the text book by Nov 25th'

    The Task class is given below:

    public class Task {
        protected String description;
    
        public Task(String description) {
            this.description = description;
        }
    
        public String getDescription() {
            return description;
        }
    }
    
    1. Write a Todo class that inherits from the Task class.
      • It should have an additional boolean field isDone to indicate whether the todo is done or not done.
      • It should have a isDone() method to access the isDone field and a setDone(boolean) method to set the isDone field.
    2. Write a Deadline class that inherits from the Todo class that you implemented in the previous step. It should have,
      • an additional String field by to store the details of when the task to be done e.g., Jan 25th 5pm
      • a getBy() method to access the value of the by field, and a corresponding setBy(String) method.
      • a constructor of the form Deadline(String description, String by)

    The expected behavior of the two classes is as follows:

    public class Main {
        public static void main(String[] args) {
            // create a todo task and print details
            Todo t = new Todo("Read a good book");
            System.out.println(t.getDescription());
            System.out.println(t.isDone());
    
            // change todo fields and print again
            t.setDone(true);
            System.out.println(t.isDone());
    
            // create a deadline task and print details
            Deadline d = new Deadline("Read textbook", "Nov 16");
            System.out.println(d.getDescription());
            System.out.println(d.isDone());
            System.out.println(d.getBy());
    
            // change deadline details and print again
            d.setDone(true);
            d.setBy("Postponed to Nov 18th");
            System.out.println(d.isDone());
            System.out.println(d.getBy());
        }
    }
    

    Read a good book
    false
    true
    Read textbook
    false
    Nov 16
    true
    Postponed to Nov 18th
    

    Todo class is given below. You can follow a similar approach for the Deadline class.

    public class Todo extends Task {
        protected boolean isDone;
    
        public Todo(String description) {
            super(description);
            isDone = false;
        }
    }
    

    W4.1e

    C++ to Java → Inheritance → The Object class

    Can use Object class

    As you know, all Java objects inherit from the Object class. Let us look at some of the useful methods in the Object class that can be used by other classes.

    The toString method

    Every class inherits a toString method from the Object class that is used by Java to get a string representation of the object e.g., for printing. By default, it simply returns the type of the object and its address (in hexadecimal).

    Suppose you defined a class called Time, to represent a moment in time. If you create a Time object and display it with println:

    class Time {
        int hours;
        int minutes;
        int seconds;
    
        Time(int hours, int minutes, int seconds) {
            this.hours = hours;
            this.minutes = minutes;
            this.seconds = seconds;
        }
    }
    
     Time t = new Time(5, 20, 13);
     System.out.println(t);
    

    Time@80cc7c0 (the address part can vary)

    You can override the toString method in your classes to provide a more meaningful string representation of the objects of that class.

    Here's an example of overriding the toString method of the Time class:

    class Time{
    
       //...
    
       @Override
       public String toString() {
           return String.format("%02d:%02d:%02d\n", this.hours, this.minutes, this.seconds);
       }
    }
    
     Time t = new Time(5, 20, 13);
     System.out.println(t);
    

    05:20:13

    @Override is an optional annotation you can use to indicate that the method is overriding a method from the parent class.

    The equals method

    There are two ways to check whether values are equal: the == operator and the equals method. With objects you can use either one, but they are not the same.

    • The == operator checks whether objects are identical; that is, whether they are the same object.
    • The equals method checks whether they are equivalent; that is, whether they have the same value.

    The definition of identity is always the same, so the == operator always does the same thing.

    Consider the following variables:

    Time time1 = new Time(9, 30, 0);
    Time time2 = time1;
    Time time3 = new Time(9, 30, 0);
    
    • The assignment operator copies references, so time1 and time2 refer to the same object. Because they are identical, time1 == time2 is true.
    • But time1 and time3 refer to different objects. Because they are not identical, time1 == time3 is false.

    By default, the equals method inherited from the Object class does the same thing as ==. As the definition of equivalence is different for different classes, you can override the equals method to define your own criteria for equivalence of objects of your class.

    Here's how you can override the equals method of the Time class to provide an equals method that considers two Time objects equivalent as long as they represent the same time of the day:

    public class Time {
        int hours;
        int minutes;
        int seconds;
    
        // ...
    
        @Override
        public boolean equals(Object o) {
            Time other = (Time) o;
            return this.hours == other.hours
                    && this.minutes == other.minutes
                    && this.seconds == other.seconds;
        }
    }
    
    Time t1 = new Time(5, 20, 13);
    Time t2 = new Time(5, 20, 13);
    System.out.println(t1 == t2);
    System.out.println(t1.equals(t2));
    

    false
    true
    

    Note that a proper equals method implementation is more complex than the example above. See the article How to Implement Java’s equals Method Correctly by Nicolai Parlog for a detailed explanation before you implement your own equals method.

    Suppose you have the following classes Task, Todo, Deadline:

    public class Task {
        protected String description;
    
        public Task(String description) {
            this.description = description;
        }
    
        public String getDescription() {
            return description;
        }
    }
    
    public class Todo extends Task {
        protected boolean isDone;
    
        public Todo(String description) {
            super(description);
            isDone = false;
        }
    
        public void setDone(boolean done) {
            isDone = done;
        }
    
        public boolean isDone() {
            return isDone;
        }
    }
    
    public class Deadline extends Todo {
    
        protected String by;
    
        public Deadline(String description, String by) {
            super(description);
            this.by = by;
        }
    
        public void setBy(String by) {
            this.by = by;
        }
    
        public String getBy() {
            return by;
        }
    }
    

    Override the toString method of the three classes to produce the following behavior.

    public class Main {
        public static void main(String[] args) {
            // create a todo task and print it
            Todo t = new Todo("Read a good book");
            System.out.println(t);
    
            // change todo fields and print again
            t.setDone(true);
            System.out.println(t);
    
            // create a deadline task and print it
            Deadline d = new Deadline("Read textbook", "Nov 16");
            System.out.println(d);
    
            // change deadline details and print again
            d.setDone(true);
            d.setBy("Postponed to Nov 18th");
            System.out.println(d);
        }
    }
    

    description: Read a good book
    is done? No
    description: Read a good book
    is done? Yes
    description: Read textbook
    is done? No
    do by: Nov 16
    description: Read textbook
    is done? Yes
    do by: Postponed to Nov 18th
    

    You can use the super.toString from the subclass to invoke the behavior of the method you are overriding. This is useful here because the overriding method is simply adding onto the behavior of the overridden method.

    toString() method of Task class and Todo class are given below. You can follow a similar approach for the Deadline class.

    public class Task {
        // ...
    
        @Override
        public String toString() {
            return "description: " + description;
        }
    }
    
    public class Todo extends Task {
        // ...
    
        @Override
        public String toString() {
            String status = null;
            if (isDone){
                status = "Yes";
            } else {
                status = "No";
            }
            return super.toString() + System.lineSeparator() + "is done? " + status;
        }
    }
    

    Guidance for the item(s) below:

    Inheritance is even more powerful when combined with polymorphism (which also happens to be the fourth core concept of OOP), explained in the sections below.

    1. abstraction
    2. encapsulation
    3. inheritance
    4. polymorphism

    [W4.2] OOP + Java: Polymorphism

    W4.2a

    Paradigms → OOP → Polymorphism → What

    Can explain OOP polymorphism

    Polymorphism:

    The ability of different objects to respond, each in its own way, to identical messages is called polymorphism. -- Object-Oriented Programming with Objective-C, Apple

    Polymorphism allows you to write code targeting superclass objects, use that code on subclass objects, and achieve possibly different results based on the actual class of the object.

    Assume classes Cat and Dog are both subclasses of the Animal class. You can write code targeting Animal objects and use that code on Cat and Dog objects, achieving possibly different results based on whether it is a Cat object or a Dog object. Some examples:

    • Declare an array of type Animal and still be able to store Dog and Cat objects in it.
    • Define a method that takes an Animal object as a parameter and yet be able to pass Dog and Cat objects to it.
    • Call a method on a Dog or a Cat object as if it is an Animal object (i.e., without knowing whether it is a Dog object or a Cat object) and get a different response from it based on its actual class e.g., call the Animal class's method speak() on object a and get a "Meow" as the return value if a is a Cat object and "Woof" if it is a Dog object.

    Polymorphism literally means "ability to take many forms".

    W4.2b

    Paradigms → OOP → Inheritance → Substitutability

    Can explain substitutability

    Every instance of a subclass is an instance of the superclass, but not vice-versa. As a result, inheritance allows substitutability: the ability to substitute a child class object where a parent class object is expected.

    An AcademicStaff is an instance of a Staff, but a Staff is not necessarily an instance of an AcademicStaff. i.e. wherever an object of the superclass is expected, it can be substituted by an object of any of its subclasses.

    The following code is valid because an AcademicStaff object is substitutable as a Staff object.

    Staff staff = new AcademicStaff(); // OK
    

    But the following code is not valid because staff is declared as a Staff type and therefore its value may or may not be of type AcademicStaff, which is the type expected by variable academicStaff.

    Staff staff;
    ...
    AcademicStaff academicStaff = staff; // Not OK
    

    W4.2c

    Paradigms → OOP → Inheritance → Dynamic and static binding

    Can explain dynamic and static binding

    Dynamic binding (There are subtle differences between late binding and dynamic binding, but they are beyond the scope of this documentaka late binding): a mechanism where method calls in code are deciding which method implementation to executeresolved at at the moment the method call is executedruntime, rather than at compile time.

    Overridden methods are resolved using dynamic binding, and therefore resolves to the implementation in the actual type of the object.

    Paradigms → OOP → Inheritance →

    Overriding

    Method overriding is when a sub-class changes the behavior inherited from the parent class by re-implementing the method. Overridden methods have the same name, same type signature, and same return type.

    Consider the following case of EvaluationReport class inheriting the Report class:

    Report methods EvaluationReport methods Overrides?
    print() print() Yes
    write(String) write(String) Yes
    read():String read(int):String No. Reason: the two methods have different signatures; This is a case of overloading (rather than overriding).

    Paradigms → OOP → Inheritance →

    Overloading

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Example:

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int).

    • calculate(String): void
    • calculate(int): void

    Which of these methods override another method? A is the parent class. B inherits A.

    • a
    • b
    • c
    • d
    • e

    d

    Explanation: Method overriding requires a method in a child class to use the same method name and same parameter sequence used by one of its ancestors.

    Consider the code below. The declared type of s is Staff and it appears as if the adjustSalary(int) operation of the Staff class is invoked.

    void adjustSalary(int byPercent) {
        for (Staff s: staff) {
            s.adjustSalary(byPercent);
        }
    }
    

    However, at runtime s can receive an object of any subclass of Staff. That means the adjustSalary(int) operation of the actual subclass object will be called. If the subclass does not override that operation, the operation defined in the superclass (in this case, Staff class) will be called.

    Static binding (aka early binding): When a method call is resolved at compile time.

    In contrast, overloaded methods are resolved using static binding.

    Paradigms → OOP → Inheritance →

    Overloading

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Example:

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int).

    • calculate(String): void
    • calculate(int): void

    Note how the constructor is overloaded in the class below. The method call new Account() is bound to the first constructor at compile time.

    class Account {
    
        Account() {
            // Signature: ()
            ...
        }
    
        Account(String name, String number, double balance) {
            // Signature: (String, String, double)
            ...
        }
    }
    

    Similarly, the calculateGrade method is overloaded in the code below and a method call calculateGrade("A1213232") is bound to the second implementation, at compile time.

    void calculateGrade(int[] averages) { ... }
    void calculateGrade(String matric) { ... }
    

    W4.2d

    Paradigms → OOP → Polymorphism → How

    Can explain how substitutability operation overriding, and dynamic binding relates to polymorphism

    Three concepts combine to achieve polymorphism: substitutability, operation overriding, and dynamic binding.

    • Substitutability: Because of substitutability, you can write code that expects objects of a parent class and yet use that code with objects of child classes. That is how polymorphism is able to treat objects of different types as one type.
    • Overriding: To get polymorphic behavior from an operation, the operation in the superclass needs to be overridden in each of the subclasses. That is how overriding allows objects of different subclasses to display different behaviors in response to the same method call.
    • Dynamic binding: Calls to overridden methods are bound to the implementation of the actual object's class dynamically during the runtime. That is how the polymorphic code can call the method of the parent class and yet execute the implementation of the child class.

    Which one of these is least related to how OO programs achieve polymorphism?

    (c)

    Explanation: Operation overriding is the one that is related, not operation overloading.

    W4.2e

    C++ to Java → Inheritance → Polymorphism

    Can use polymorphism in Java

    Java is a strongly-typed language which means the code works with only the object types that it targets.

    The following code PetShelter keeps a list of Cat objects and make them speak. The code will not work with any other type, for example, Dog objects.

    public class PetShelter {
        private static Cat[] cats = new Cat[]{
                new Cat("Mittens"),
                new Cat("Snowball")};
    
        public static void main(String[] args) {
            for (Cat c: cats){
                System.out.println(c.speak());
            }
        }
    }
    

    Mittens: Meow
    Snowball: Meow
    
    public class Cat {
        private String name;
    
        public Cat(String name) {
            this.name = name;
        }
    
        public String speak() {
            return name + ": Meow";
        }
    }
    

    This strong-typing can lead to unnecessary verbosity caused by repetitive similar code that do similar things with different object types.

    If the PetShelter is to keep both cats and dogs, you'll need two arrays and two loops:

    public class PetShelter {
        private static Cat[] cats = new Cat[]{
                new Cat("Mittens"),
                new Cat("Snowball")};
        private static Dog[] dogs = new Dog[]{
                new Dog("Spot")};
    
        public static void main(String[] args) {
            for (Cat c: cats){
                System.out.println(c.speak());
            }
            for(Dog d: dogs){
                System.out.println(d.speak());
            }
        }
    }
    

    Mittens: Meow
    Snowball: Meow
    Spot: Woof
    
    public class Dog {
        private String name;
    
        public Dog(String name) {
            this.name = name;
        }
    
        public String speak() {
            return name + ": Woof";
        }
    }
    

    A better way is to take advantage of polymorphism to write code that targets a superclass so that it works with any subclass objects.

    The PetShelter2 uses one data structure to keep both types of animals and one loop to make them speak. The code targets the Animal superclass (assuming Cat and Dog inherits from the Animal class) instead of repeating the code for each animal type.

    public class PetShelter2 {
        private static Animal[] animals = new Animal[]{
                new Cat("Mittens"),
                new Cat("Snowball"),
                new Dog("Spot")};
    
        public static void main(String[] args) {
            for (Animal a: animals){
                System.out.println(a.speak());
            }
        }
    }
    

    Mittens: Meow
    Snowball: Meow
    Spot: Woof
    
    public class Animal {
    
        protected String name;
    
        public Animal(String name){
            this.name = name;
        }
        public String speak(){
            return name;
        }
    }
    
    public class Cat extends Animal {
        public Cat(String name) {
            super(name);
        }
    
        @Override
        public String speak() {
            return name + ": Meow";
        }
    }
    
    public class Dog extends Animal {
        public Dog(String name) {
            super(name);
        }
    
        @Override
        public String speak() {
            return name + ": Woof";
        }
    }
    

    Explanation: Because Java supports polymorphism, you can store both Cat and Dog objects in an array of Animal objects. Similarly, you can call the speak method on any Animal object (as done in the loop) and yet get different behavior from Cat objects and Dog objects.

    Suggestion: try to add an Animal object (e.g., new Animal("Unnamed")) to the animals array and see what happens.

    Polymorphic code is better in several ways:

    • It is shorter.
    • It is simpler.
    • It is more flexible (in the above example, the main method will work even if we add more animal types).

    The Main class below keeps a list of Circle and Rectangle objects and prints the area (as an int value) of each shape when requested.

    Add the missing variables/methods to the code below so that it produces the output given.

    public class Main {
        //TODO add your methods here
    
        public static void main(String[] args) {
            addShape(new Circle(5));
            addShape(new Rectangle(3, 4));
            addShape(new Circle(10));
            printAreas();
            addShape(new Rectangle(4, 4));
            printAreas();
        }
    }
    

    78
    12
    314
    78
    12
    314
    16
    

    Circle class and Rectangle class is given below but you'll need to add a parent class Shape:

    public class Circle {
    
        private int radius;
    
        public Circle(int radius) {
            this.radius = radius;
        }
    
        public int area() {
            return (int)(Math.PI * radius * radius);
        }
    }
    
    public class Rectangle {
        private int height;
        private int width;
    
        public Rectangle(int height, int width){
            this.height = height;
            this.width = width;
        }
    
        public int area() {
            return height * width;
        }
    }
    

    You may use an array of size 100 to store the shapes.

    public class Main {
        private static Shape[] shapes = new Shape[100];
        private static int shapeCount = 0;
    
        public static void addShape(Shape s){
            shapes[shapeCount] = s;
            shapeCount++;
        }
    
        // ...
    
    }
    

    Guidance for the item(s) below:

    You already know how to define variables in Java code; do you know how to define constants?

    [W4.3] Java: Constants

    W4.3a

    C++ to Java → Miscellaneous Topics → Constants

    Can use Java constants

    Java does not directly support constants. The convention is to use a static final variable where a constant is needed. The static modifier causes the variable to be available without instantiating an object. The final modifier causes the variable to be unchangeable. Java constants are normally declared in ALL CAPS separated by underscores.

    Here is an example of a constant named MAX_BALANCE which can be accessed as Account.MAX_BALANCE.

    public class Account{
    
      public static final double MAX_BALANCE = 1000000.0;
    
    }
    

    Math.PI is an example constant that comes with Java.

    Guidance for the item(s) below:

    An enumeration is something in between a primitive (e.g., int) and a class (e.g., Person). Let's learn about enumerations next.

    [W4.4] OOP + Java: Enumerations

    W4.4a

    Paradigms → OOP → Classes → Enumerations

    Can explain the meaning of enumerations

    An Enumeration is a fixed set of values that can be considered as a data type. An enumeration is often useful when using a regular data type such as int or String would allow invalid values to be assigned to a variable.

    Suppose you want a variable called priority to store the priority of something. There are only three priority levels: high, medium, and low. You can declare the variable priority as of type int and use only values 2, 1, and 0 to indicate the three priority levels. However, this opens the possibility of an invalid value such as 9 being assigned to it. But if you define an enumeration type called Priority that has three values HIGH, MEDIUM and LOW only, a variable of type Priority will never be assigned an invalid value because the compiler is able to catch such an error.

    Priority: HIGH, MEDIUM, LOW

    W4.4b

    C++ to Java → Miscellaneous Topics → Enumerations

    Can use Java enumerations

    You can define an enum type by using the enum keyword. Because they are constants, the names of an enum type's fields are in uppercase letters e.g., FLAG_SUCCESS by convention.

    Defining an enumeration to represent days of a week (code to be put in the Day.java file):

    public enum Day {
        SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
        THURSDAY, FRIDAY, SATURDAY
    }
    

    Some examples of using the Day enumeration defined above:

    Day today = Day.MONDAY;
    Day[] holidays = new Day[]{Day.SATURDAY, Day.SUNDAY};
    
    switch (today) {
    case SATURDAY:
    case SUNDAY:
        System.out.println("It's the weekend");
        break;
    default:
        System.out.println("It's a week day");
    }
    

    Note that while enumerations are usually a simple set of fixed values, Java enumerations can have behaviors too, as explained in this tutorial from -- Java Tutorial

    Define an enumeration named Priority. Add the missing describe method to the code below so that it produces the output given.

    public class Main {
    
        // Add your method here
    
        public static void main(String[] args) {
            describe("Red", Priority.HIGH);
            describe("Orange", Priority.MEDIUM);
            describe("Blue", Priority.MEDIUM);
            describe("Green", Priority.LOW);
        }
    }
    

    Red indicates high priority
    Orange indicates medium priority
    Blue indicates medium priority
    Green indicates low priority
    

    Use a switch statement to select between possible values for Priority.

        public static void describe(String color, Priority p) {
            switch (p) {
                case LOW:
                    System.out.println(color + " indicates low priority");
                    break;
                // ...
            }
        }
    

    Code for the enumeration is given below:

    public enum Priority {
        HIGH, MEDIUM, LOW
    }
    

    Guidance for the item(s) below:

    As you start adding features to your project iteratively, you'll need a way to detect if the new code breaks the existing code. Next, let's learn a rather simple way to do that using a certain type of testing (we'll be learning more sophisticated methods in later weeks).

    [W4.5] Automated Testing of Text UIs

    W4.5a

    Quality Assurance → Testing → Introduction → What

    Can explain testing

    Testing: Operating a system or component under specified conditions, observing or recording the results, and making an evaluation of some aspect of the system or component. –- source: IEEE

    When testing, you execute a set of test cases. A test case specifies how to perform a test. At a minimum, it specifies the input to the software under test (SUT) and the expected behavior.

    Example: A minimal test case for testing a browser:

    • Input – Start the browser using a blank page (vertical scrollbar disabled). Then, load longfile.html located in the test data folder.
    • Expected behavior – The scrollbar should be automatically enabled upon loading longfile.html.

    Test cases can be determined based on the specification, reviewing similar existing systems, or comparing to the past behavior of the SUT.

    Other details a test case can contain extra A more elaborate test case can have other details such as those given below.
    • A unique identifier : e.g. TC0034-a
    • A descriptive name: e.g. vertical scrollbar activation for long web pages
    • Objectives: e.g. to check whether the vertical scrollbar is correctly activated when a long web page is loaded to the browser
    • Classification information: e.g. priority - medium, category - UI features
    • Cleanup, if any: e.g. empty the browser cache.

    For each test case you should do the following:

    1. Feed the input to the SUT
    2. Observe the actual output
    3. Compare actual output with the expected output

    A test case failure is a mismatch between the expected behavior and the actual behavior. A failure indicates a potential defect (or a bug), unless the error is in the test case itself.

    Example: In the browser example above, a test case failure is implied if the scrollbar remains disabled after loading longfile.html. The defect/bug causing that failure could be an uninitialized variable.

    A deeper look at the definition of testing extra

    Here is another definition of testing:

    Software testing consists of the dynamic verification that a program provides expected behaviors on a finite set of test cases, suitably selected from the usually infinite execution domain. -– source: Software Engineering Book of Knowledge V3

    Some things to note (indicated by keywords in the above definition):

    • Dynamic: Testing involves executing the software. It is not by examining the code statically.
    • Finite: In most non-trivial cases there are potentially infinite test scenarios but resource constraints dictate that we can test only a finite number of scenarios.
    • Selected: In most cases it is not possible to test all scenarios. That means we need to select what scenarios to test.
    • Expected: Testing requires some knowledge of how the software is expected to behave.

    Explain how the concepts of testing, test cases, test failure, and defects are related to each other.

    W4.5b

    Quality Assurance → Testing → Regression Testing → What

    Can explain regression testing

    When you modify a system, the modification may result in some unintended and undesirable effects on the system. Such an effect is called a regression.

    Regression testing is the re-testing of the software to detect regressions. Note that to detect regressions, you need to retest all related components, even if they had been tested before.

    Regression testing is more effective when it is done frequently, after each small change. However, doing so can be prohibitively expensive if testing is done manually. Hence, regression testing is more practical when it is automated.

    Regression testing is the automated re-testing of a software after it has been modified.

    c.

    Explanation: Regression testing need not be automated but automation is highly recommended.

    Explain why and when you would do regression testing in a software project.

    W4.5c

    Quality Assurance → Testing → Test Automation → What

    Can explain test automation

    An automated test case can be run programmatically and the result of the test case (pass or fail) is determined programmatically. Compared to manual testing, automated testing reduces the effort required to run tests repeatedly and increases precision of testing (because manual testing is susceptible to human errors).

    W4.5d

    Quality Assurance → Testing → Test Automation → Automated testing of CLI applications

    Can semi-automate testing of CLIs

    A simple way to semi-automate testing of a CLI (Command Line Interface) app is by using input/output re-direction.

    • First, you feed the app with a sequence of test inputs that is stored in a file while redirecting the output to another file.
    • Next, you compare the actual output file with another file containing the expected output.

    Let's assume you are testing a CLI app called AddressBook. Here are the detailed steps:

    1. Store the test input in the text file input.txt.

      add Valid Name p/12345 valid@email.butNoPrefix
      add Valid Name 12345 e/valid@email.butPhonePrefixMissing
      
    2. Store the output you expect from the SUT in another text file expected.txt.

      Command: || [add Valid Name p/12345 valid@email.butNoPrefix]
      Invalid command format: add 
      
      Command: || [add Valid Name 12345 e/valid@email.butPhonePrefixMissing]
      Invalid command format: add 
      
    3. Run the program as given below, which will redirect the text in input.txt as the input to AddressBook and similarly, will redirect the output of AddressBook to a text file output.txt. Note that this does not require any code changes to AddressBook.

      java AddressBook < input.txt > output.txt
      
      • The way to run a CLI program differs based on the language.
        e.g., In Python, assuming the code is in AddressBook.py file, use the command
        python AddressBook.py < input.txt > output.txt

      • If you are using Windows, use a normal command window to run the app, not a PowerShell window.

      More on the > operator and the < operator extra

      A CLI program takes input from the keyboard and outputs to the console. That is because those two are the default input and output streams, respectively. But you can change that behavior using < and > operators. For example, if you run AddressBook in a command window, the output will be shown in the console, but if you run it like this,

      java AddressBook > output.txt 
      

      the Operating System then creates a file output.txt and stores the output in that file instead of displaying it in the console. No file I/O coding is required. Similarly, adding < input.txt (or any other filename) makes the OS redirect the contents of the file as input to the program, as if the user typed the content of the file one line at a time.

      Resources:

    4. Next, you compare output.txt with the expected.txt. This can be done using a utility such as Windows' FC (i.e. File Compare) command, Unix's diff command, or a GUI tool such as WinMerge.

      FC output.txt expected.txt
      

    Note that the above technique is only suitable when testing CLI apps, and only if the exact output can be predetermined. If the output varies from one run to the other (e.g. it contains a time stamp), this technique will not work. In those cases, you need more sophisticated ways of automating tests.

    CLI application: An application that has a Command Line Interface. i.e. user interacts with the app by typing in commands.

    Guidance for the item(s) below:

    Continuing our theme on code quality, given below is another aspect of it: readability

    [W4.6] Code Quality: Readability

    W4.6a

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

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

    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
    

    W4.6b

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

    Can improve code quality using technique: avoid long methods

    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.

    W4.6c

    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

    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
    

    W4.6d

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

    Example:

    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;
    

    Example:

    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

    W4.6e

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

    Can improve code quality using technique: avoid magic numbers

    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 e.g., PInamed constant makes the code easier to understand because the name tells us more about the meaning of the number.

    Example:

    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!
    

    In general, try to avoid any magic literals.

    W4.6f

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

    W4.6g

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

    W4.6h

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

    Can improve code quality using technique: do not 'trip up' reader

    Avoid things that would make the reader go ‘huh?’, such as,

    • unused parameters in the method signature
    • similar things that 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

    W4.6i

    Implementation → Code Quality → Readability → Intermediate → Practice KISSing

    Can improve code quality using technique: practice KISSing

    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

    W4.6j

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

    Can improve code quality using technique: avoid premature optimizations

    Optimizing code prematurely has several drawbacks:

    • You 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 it is for the compiler to optimize). 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 no matter how fast/efficient it is.

    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 is simply a caution that you should optimize only when it is really needed.

    W4.6k

    Implementation → Code Quality → Readability → Intermediate → SLAP hard

    Can improve code quality using technique: SLAP hard

    Avoid varying the level of abstraction within a code fragment. Note: The book The Productive Programmer (by Neal Ford) calls this the Single Level of Abstraction Principle (SLAP) while the book Clean Code (by Robert C. Martin) calls this One Level of Abstraction per Function.

    Example:

    Bad (readData(); and salary = basic * rise + 1000; are at different levels of abstraction)

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

    Good (all statements are at the same level of abstraction)

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

    Design → Design Fundamentals → Abstraction →

    What

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

    The guiding principle of abstraction is that only details that are relevant to the current perspective or the task at hand need to be considered. As 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. That is where abstraction can help.

    Data abstraction: abstracting away the lower level data items and thinking in terms of bigger entities

    Within a certain software component, you 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: abstracting away details of the actual control flow to focus on tasks at a higher level

    print(“Hello”) is an abstraction of the actual output mechanism within the computer.

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

    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 level than an Assembly language instruction MOV.

    Abstraction is a general concept that is not limited to just data or control abstractions.

    Some more general examples of abstraction:

    • An OOP class is an abstraction over related data and behaviors.
    • An architecture is a higher-level abstraction of the design of a software.
    • Models (e.g., UML models) are abstractions of some aspect of reality.

    W4.6l

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

    Can improve code quality using technique: make the happy path prominent

    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:

    Bad

    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 detections are separated from their handling.
    • the main path is nested deeply.

    Good

    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.

    Guidance for the item(s) below:

    Knowing code-quality guidelines is useful for sure, but how do we improve the code quality of existing code in a systematic and safe way? That's where the next topic comes in.

    [W4.7] Code Quality: Refactoring

    W4.7a

    Implementation → Refactoring → What

    Can explain refactoring

    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. This process of improving a program's internal structure in small steps without modifying its external behavior is called refactoring.

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

    Improving code structure 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.

    Choose the correct statements.

    • a. Refactoring can improve understandability
    • b. Refactoring can uncover bugs
    • c. Refactoring can result in better performance
    • d. Refactoring can change the number of methods/classes

    a, b, c, d

    Explanation:

    • (a, b, c) Although the primary aim of refactoring is to improve the internal code structure, there are other secondary benefits.
    • (d) Some refactorings result in adding/removing methods/classes.

    Do you agree with the following statement? Justify your answer.

    Statement: Whenever you refactor code to fix bugs, you need not do regression testing if the bug fix was minor.

    There are two flaws in the given statement.

    DISAGREE.

    1. Even a minor change can have major repercussions on the system. You MUST do regression testing after each change, no matter how minor it is.
    2. Fixing bugs is technically not refactoring.

    Explain what refactoring is and why it is not the same as rewriting, bug fixing, or adding features.

    W4.7b : OPTIONAL

    Tools → IntelliJ IDEA → Refactoring

    Can use automated refactoring features of the IDE

    This video explains how to automate the 'Extract parameter' 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 change a method signature as part of refactoring.

    W4.7c

    Implementation → Refactoring → How

    Can apply some basic refactoring

    Given below are some more commonly used refactorings. A more comprehensive list is available at refactoring-catalog.

    1. Consolidate Conditional Expression
    2. Decompose Conditional
    3. Inline Method
    4. Remove Double Negative
    5. Replace Magic Literal
    6. Replace Nested Conditional with Guard Clauses
    7. Replace Parameter with Explicit Methods
    8. Reverse Conditional
    9. Split Loop
    10. Split Temporary Variable

    W4.7d : OPTIONAL

    Implementation → Refactoring → When

    Can decide when to apply a given refactoring

    You know that it is important to refactor frequently so as to avoid the accumulation of ‘messy’ code which might get out of control. But how much refactoring is too much refactoring? It is too much refactoring when the benefits no longer justify the cost. The costs and the benefits depend on the context. That is why some refactorings are ‘opposites’ of each other (e.g. extract method vs inline method).

    ‘Extract method’ and ‘Inline method’ refactorings

    a

    Guidance for the item(s) below:

    Let's learn how to create a pull request (PRs) on GitHub; you need to create one for your project this week.

    Follow that with learning how to review PRs as you'll be doing that too later in this week.

    [W4.8] RCS: Creating Pull Requests

    W4.8a

    Tools → Git and GitHub → Creating PRs

    Can create PRs on GitHub

    Suppose you want to propose some changes to a GitHub repo (e.g., samplerepo-pr-practice) as a pull request (PR). Here is a scenario you can try in order to learn how to create PRs:

    A pull request (PR for short) is a mechanism for contributing code to a remote repo, i.e., "I'm requesting you to pull my proposed changes to your repo". For this to work, the two repos must have a shared history. The most common case is sending PRs from a fork to its upstream repo is a repo you forked fromupstream repo.

    1. Fork the repo onto your GitHub account.

    2. Clone it onto your computer.

    3. Commit your changes e.g., add a new file with some contents and commit it.

    • Option A - Commit changes to the master branch
    • Option B - Commit to a new branch e.g., create a branch named add-intro (remember to switch to the master branch before creating a new branch) and add your commit to it.

    4. Push the branch you updated (i.e., master branch or the new branch) to your fork, as explained here.

    Pushing a branch to a remote repo

    Here's how to push a branch to a remote repo:

    Here's how to push a branch named add-intro to your own fork of a repo named samplerepo-pr-practice:

    Normally: git push {remote repository} {branch}. Examples:

    • git push origin master pushes the master branch to the repo named origin (i.e., the repo you cloned from)
    • git push upstream-repo add-intro pushes the add-intro branch to the repo named upstream-repo

    If pushing a branch you created locally to the remote for the first time, add the -u flag to get the local branch to track the new upstream branch:
    e.g., git push -u origin add-intro

    See git-scm.com/docs/git-push for details of the push command.

    5. Initiate the PR creation:

    1. Go to your fork.

    2. Click on the Pull requests tab followed by the New pull request button. This will bring you to the 'Comparing changes' page.

    3. Set the appropriate target repo and the branch that should receive your PR, using the base repository and base dropdowns. e.g.,
      base repository: se-edu/samplerepo-pr-practice base: master

      Normally, the default value shown in the dropdown is what you want but in case your fork has e.g., the repo you forked from is also a fork of a another repo, which means both of those are considered upstream repos of your forkmultiple upstream repos, the default may not be what you want.

    4. Indicate which repo:branch contains your proposed code, using the head repository and compare dropdowns. e.g.,
      head repository: myrepo/samplerepo-pr-practice compare: master

    6. Verify the proposed code: Verify that the diff view in the page shows the exact change you intend to propose. If it doesn't, commit the new code and push to the branchupdate the branch as necessary.

    7. Submit the PR:

    1. Click the Create pull request button.

    2. Fill in the PR name and description e.g.,
      Name: Add an introduction to the README.md
      Description:

      Add some paragraph to the README.md to explain ...
      Also add a heading ...
      
    3. If you want to indicate that the PR you are about to create is 'still work in progress, not yet ready', click on the dropdown arrow in the Create pull request button and choose Create draft pull request option.

    4. Click the Create pull request button to create the PR.

    5. Go to the receiving repo to verify that your PR appears there in the Pull requests tab.

    The next step of the PR life cycle is the PR review. The members of the repo that received your PR can now review your proposed changes.

    • If they like the changes, they can merge the changes to their repo, which also closes the PR automatically.
    • If they don't like it at all, they can simply close the PR too i.e., they reject your proposed change.
    • In most cases, they will add comments to the PR to suggest further changes. When that happens, GitHub will notify you.

    You can update the PR along the way too. Suppose PR reviewers suggested a certain improvement to your proposed code. To update your PR as per the suggestion, you can simply modify the code in your local repo, commit the updated code to the same master branch, and push to your fork as you did earlier. The PR will auto-update accordingly.

    Sending PRs using the master branch is less common than sending PRs using separate branches. For example, suppose you wanted to propose two bug fixes that are not related to each other. In that case, it is more appropriate to send two separate PRs so that each fix can be reviewed, refined, and merged independently. But if you send PRs using the master branch only, both fixes (and any other change you do in the master branch) will appear in the PRs you create from it.

    To create another PR while the current PR is still under review, create a new branch (remember to switch back to the master branch first), add your new proposed change in that branch, and create a new PR following the steps given above.

    It is possible to create PRs within the same repo e.g., you can create a PR from branch feature-x to the master branch, within the same repo. Doing so will allow the code to be reviewed by other developers (using PR review mechanism) before it is merged.

    W4.8b

    Tools → Git and GitHub → Reviewing PRs

    Can review PRs on GitHub

    The PR review stage is a dialog between the PR author and members of the repo that received the PR, in order to refine and eventually merge the PR.

    Given below are some steps you can follow when reviewing a PR.

    1. Locate the PR:

    1. Go to the GitHub page of the repo.
    2. Click on the Pull requests tab.
    3. Click on the PR you want to review.

    2. Read the PR description. It might contain information relevant to reviewing the PR.

    3. Click on the Files changed tab to see the diff view.

    4. Add review comments:

    1. Hover over the line you want to comment on and click on the icon that appears on the left margin. That should create a text box for you to enter your comment.
      To mark multiple lines, click-and-drag the icon.
    2. Enter your comment.
      This page @SE-EDU/guides has some best practices PR reviewers can follow.
    3. After typing in the comment, click on the Start a review button (not the Add single comment button. This way, your comment is saved but not visible to others yet. It will be visible to others only when you have finished the entire review.

    4. Repeat the above steps to add more comments.

    5. Submit the review:

    1. When there are no more comments to add, click on the Review changes button (on the top right of the diff page).
    2. Type in an overall comment about the PR, if any. e.g.,
      Overall, I found your code easy to read for the most part except a few places
      where the nesting was too deep. I noted a few minor coding standard violations
      too. Some of the classes are getting quite long. Consider splitting into smaller
      classes if that makes sense.
      
      LGTM is often used in such overall comments, to indicate Looks good to merge.
      nit is another such term, used to indicate minor flaws e.g., LGTM, almost. Just a few nits to fix..
    3. Choose Approve, Comment, or Request changes option as appropriate and click on the Submit review button.