Detailed Table of Contents
Guidance for the item(s) below:
As you have learned the Java basic topics already, it is time to move to intermediate level topics. This week, we cover several such topics, starting with the one given below.
Can use Java casting
Casting is the action of converting from one type to another. You can use the (newType)
syntax to cast a value to a type named newType
.
When you cast a primitive value to another type, there may be a loss of precision.
The code below casts a double
value to an int
value and casts it back to a double
. Note the loss of precision.
double d = 5.3;
System.out.println("Before casting to an int: " + d);
int i = (int)d; // cast d to an int
System.out.println("After casting to an int: " + i);
d = (double)i; // cast i back to a double
System.out.println("After casting back a double: " + d);
Before casting to an int: 5.3
After casting to an int: 5
After casting back a double: 5.0
Downcasting is when you cast an object reference from a superclass to a subclass.
Assume the following class hierarchy:
class Animal{
void speak(){
System.out.println("I'm an animal");
}
}
class Cat extends Animal{
@Override
void speak() {
System.out.println("I'm a Cat");
}
}
class DomesticCat extends Cat{
@Override
void speak() {
System.out.println("I'm a DomesticCat");
}
void catchMice(){
// ...
}
}
The foo
method below downcasts an Animal
object to its subclasses.
public static void foo(Animal a){
a.speak();
Cat c = (Cat)a; // downcast a to a Cat
c.speak();
DomesticCat dc = (DomesticCat)a; // downcast a to a DomesticCat
dc.speak();
dc.catchMice();
}
Note that the dc.catchMice()
line will not compile if a
is not downcast to a DomesticCat
object first. Reason: the catchMice
method is specific to the DomesticCat
class not not present in the Animal
or the Cat
classes.
Furthermore, the foo
method will fail at runtime if the argument a
is not a DomesticCat
object. Reason: an object cannot be cast to another class unless the object is of that class to begin with e.g., you cannot cast a Dog
object into a Cat
object.
Upcasting is when you cast an object reference from a subclass to a superclass. However, upcasting is done automatically by the compiler even if you do not specify it explicitly.
This example upcasts a Cat
object to its superclass Animal
:
Cat c = new Cat();
Animal a1 = (Animal)c; //upcasting c to the Animal class
Animal a2 = c; //upcasting is implicit
Note that due to polymorphism, the behavior of the object will reflect the actual type of the object irrespective of the type of the variable holding a reference to it.
The call to the speak()
method in the code below always executes the speak()
method of the DomesticCat
class because the actual type of the object remains DomesticCat
although the reference to it is being downcast/upcast to various other types.
Animal a = new DomesticCat(); //implicit upcast
a.speak();
Cat c = (Cat)a; //downcast
c.speak();
DomesticCat dc = (DomesticCat)a; //downcast
dc.speak();
I'm a DomesticCat
I'm a DomesticCat
I'm a DomesticCat
Casting to an incompatible type can result in a ClassCastException
at runtime.
This code will cause a ClassCastException
:
Object o = new Animal();
Integer x = (Integer)o;
Exception in thread "main" java.lang.ClassCastException: misc.casting.Animal cannot be
cast to java.lang.Integer at misc.casting.CastingExamples.main(CastingExamples.java:14)
You can use the instanceof
operator to check if a cast is safe to perform.
This code checks if the object a
is an instance of the Cat
class before casting it to a Cat
.
Cat c;
if (a instanceof Cat){
c = (Cat)a;
}
Guidance for the item(s) below:
It is time to move on to some intermediate level OOP concepts. Next, let's learn about abstract classes and how they are implemented in Java.
Can implement abstract classes
Abstract class: A class declared as an abstract class cannot be instantiated, but it can be subclassed.
You can declare a class as abstract when a class is merely a representation of commonalities among its subclasses in which case it does not make sense to instantiate objects of that class.
The Animal
class that exists as a generalization of its subclasses Cat
, Dog
, Horse
, Tiger
etc. can be declared as abstract because it does not make sense to instantiate an Animal
object.
Abstract method: An abstract method is a method signature without a method implementation.
The move
method of the Animal
class is likely to be an abstract method as it is not possible to implement a move
method at the Animal
class level to fit all subclasses because each animal type can move in a different way.
A class that has an abstract method becomes an abstract class because the class definition is incomplete (due to the missing method body) and it is not possible to create objects using an incomplete class definition.
Can use abstract classes and methods
In Java, an abstract method is declared with the keyword abstract
and given without an implementation. If a class includes abstract methods, then the class itself must be declared abstract.
The speak
method in this Animal
class is abstract
. Note how the method signature ends with a semicolon and there is no method body. This makes sense as the implementation of the speak
method depends on the type of the animal and it is meaningless to provide a common implementation for all animal types.
public abstract class Animal {
protected String name;
public Animal(String name){
this.name = name;
}
public abstract String speak();
}
As one method of the class is abstract
, the class itself is abstract
.
An abstract class is declared with the keyword abstract
. Abstract classes can be used as reference type but cannot be instantiated.
This Account
class has been declared as abstract although it does not have any abstract methods. Attempting to instantiate Account
objects will result in a compile error.
public abstract class Account {
int number;
void close(){
//...
}
}
Account a;
OK to use as a type
a = new Account();
Compile error!
In Java, even a class that does not have any abstract methods can be declared as an abstract class.
When an abstract class is subclassed, the subclass should provide implementations for all of the abstract methods in its superclass or else the subclass must also be declared abstract.
The Feline
class below inherits from the abstract class Animal
but it does not provide an implementation for the abstract method speak
. As a result, the Feline
class needs to be abstract too.
public abstract class Feline extends Animal {
public Feline(String name) {
super(name);
}
}
The DomesticCat
class inherits the abstract Feline
class and provides the implementation for the abstract method speak
. As a result, it need not be (but can be) declared as abstract.
public class DomesticCat extends Feline {
public DomesticCat(String name) {
super(name);
}
@Override
public String speak() {
return "Meow";
}
}
Animal a = new Feline("Mittens");
Feline
is abstract.Animal a = new DomesticCat("Mittens");
DomesticCat
can be instantiated and assigned to a variable of Animal
type (the assignment is allowed by polymorphism).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.
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++;
}
public static void printAreas(){
for (int i = 0; i < shapeCount; i++){
shapes[i].print();
}
}
public static void main(String[] args) {
addShape(new Circle(5));
addShape(new Rectangle(3, 4));
addShape(new Circle(10));
addShape(new Rectangle(4, 4));
printAreas();
}
}
Circle of area 78
Rectangle of area 12
Circle of area 314
Rectangle of area 16
Circle
class and Rectangle
class is given below:
public class Circle extends Shape {
private int radius;
public Circle(int radius) {
this.radius = radius;
}
@Override
public int area() {
return (int)(Math.PI * radius * radius);
}
@Override
public void print() {
System.out.println("Circle of area " + area());
}
}
public class Rectangle extends Shape {
private int height;
private int width;
public Rectangle(int height, int width){
this.height = height;
this.width = width;
}
@Override
public int area() {
return height * width;
}
@Override
public void print() {
System.out.println("Rectangle of area " + area());
}
}
Add the missing Shape
class as an abstract class with two abstract methods.
Partial solution
Statements about abstract classes
Guidance for the item(s) below:
From abstract classes, we move to another related OOP concept interfaces, and how they are implemented in Java.
Can explain interfaces
An interface is a behavior specification i.e. a collection of . If a class , it means the class is able to support the behaviors specified by the said interface.
There are a number of situations in software engineering when it is important for disparate groups of programmers to agree to a "contract" that spells out how their software interacts. Each group should be able to write their code without any knowledge of how the other group's code is written. Generally speaking, interfaces are such contracts. --Oracle Docs on Java
Suppose SalariedStaff
is an interface that contains two methods setSalary(int)
and getSalary()
. AcademicStaff
can declare itself as implementing the SalariedStaff
interface, which means the AcademicStaff
class must implement all the methods specified by the SalariedStaff
interface i.e., setSalary(int)
and getSalary()
.
A class implementing an interface results in an is-a relationship, just like in class inheritance.
In the example above, AcademicStaff
is a SalariedStaff
. An AcademicStaff
object can be used anywhere a SalariedStaff
object is expected e.g. SalariedStaff ss = new AcademicStaff()
.
Can use interfaces in Java
The text given in this section borrows some explanations and code examples from the -- Java Tutorial.
In Java, an interface is a reference type, similar to a class, mainly containing method signatures. Defining an interface is similar to creating a new class except it uses the keyword interface
in place of class
.
Here is an interface named DrivableVehicle
that defines methods needed to drive a vehicle.
public interface DrivableVehicle {
void turn(Direction direction);
void changeLanes(Direction direction);
void signalTurn(Direction direction, boolean signalOn);
// more method signatures
}
Note that the method signatures have no braces ({ }
) and are terminated with a semicolon.
Interfaces cannot be instantiated—they can only be implemented by classes. When an instantiable class implements an interface, indicated by the keyword implements
, it provides a method body for each of the methods declared in the interface.
Here is how a class CarModelX
can implement the DrivableVehicle
interface.
public class CarModelX implements DrivableVehicle {
@Override
public void turn(Direction direction) {
// implementation
}
// implementation of other methods
}
An interface can be used as a type e.g., DrivableVehicle dv = new CarModelX();
.
Interfaces can inherit from other interfaces using the extends
keyword, similar to a class inheriting another.
Here is an interface named SelfDrivableVehicle
that inherits the DrivableVehicle
interface.
public interface SelfDrivableVehicle extends DrivableVehicle {
void goToAutoPilotMode();
}
Note that the method signatures have no braces and are terminated with a semicolon.
Furthermore, Java allows multiple inheritance among interfaces. A Java interface can inherit multiple other interfaces. A Java class can implement multiple interfaces (and inherit from one class).
The design below is allowed by Java. In case you are not familiar with UML notation used: solid lines indicate normal inheritance; dashed lines indicate interface inheritance; the triangle points to the parent.
Staff
interface inherits (note the solid lines) the interfaces TaxPayer
and Citizen
.TA
class implements both Student
interface and the Staff
interface.TA
class has to implement all methods in the interfaces TaxPayer
and Citizen
.TA
is a Staff
, is a TaxPayer
and is a Citizen
.Interfaces can also contain constants and static methods.
This example adds a constant MAX_SPEED
and a static method isSpeedAllowed
to the interface DrivableVehicle
.
public interface DrivableVehicle {
int MAX_SPEED = 150;
static boolean isSpeedAllowed(int speed){
return speed <= MAX_SPEED;
}
void turn(Direction direction);
void changeLanes(Direction direction);
void signalTurn(Direction direction, boolean signalOn);
// more method signatures
}
Interfaces can contain default method implementations and nested types. They are not covered here.
The Main
class below passes a list of Printable
objects (i.e., objects that implement the Printable
interface) for another method to be printed.
public class Main {
public static void printObjects(Printable[] items) {
for (Printable p : items) {
p.print();
}
}
public static void main(String[] args) {
Printable[] printableItems = new Printable[]{
new Circle(5),
new Rectangle(3, 4),
new Person("James Cook")};
printObjects(printableItems);
}
}
Circle of area 78
Rectangle of area 12
Person of name James Cook
Classes Shape
, Circle
, and Rectangle
are given below:
public abstract class Shape {
public abstract int area();
}
public class Circle extends Shape implements Printable {
private int radius;
public Circle(int radius) {
this.radius = radius;
}
@Override
public int area() {
return (int)(Math.PI * radius * radius);
}
@Override
public void print() {
System.out.println("Circle of area " + area());
}
}
public class Rectangle extends Shape implements Printable {
private int height;
private int width;
public Rectangle(int height, int width){
this.height = height;
this.width = width;
}
@Override
public int area() {
return height * width;
}
@Override
public void print() {
System.out.println("Rectangle of area " + area());
}
}
Add the missing Printable
interface. Add the missing methods of the Person
class given below.
public class Person implements Printable {
private String name;
// todo: add missing methods
}
Partial solution
Guidance for the item(s) below:
As you add more and more Java classes to your project, keeping all those classes in the same directory becomes untenable. The solution is covered in the next topic.
Can use Java packages
You can organize your types (i.e., classes, interfaces, enumerations, etc.) into packages for easier management (among other benefits).
To create a package, you put a package statement at the very top of every source file in that package. The package statement must be the first line in the source file and there can be no more than one package statement in each source file. Furthermore, the package of a type should match the folder path of the source file. Similarly, the compiler will put the .class
files in a folder structure that matches the package names.
The Formatter
class below (in <source folder>/seedu/tojava/util/Formatter.java
file) is in the package seedu.tojava.util
. When it is compiled, the Formatter.class
file will be in the location <compiler output folder>/seedu/tojava/util
:
package seedu.tojava.util;
public class Formatter {
public static final String PREFIX = ">>";
public static String format(String s){
return PREFIX + s;
}
}
Package names are written in all lower case (not camelCase), using the dot as a separator. Packages in the Java language itself begin with java
. or javax
. Companies use their reversed Internet domain name to begin their package names.
For example, com.foobar.doohickey.util
can be the name of a package created by a company with a domain name foobar.com
To use a public from outside its package, you must do one of the following:
The Main
class below has two import statements:
import seedu.tojava.util.StringParser
: imports the class StringParser
in the seedu.tojava.util
packageimport seedu.tojava.frontend.*
: imports all the classes in the seedu.tojava.frontend
packagepackage seedu.tojava;
import seedu.tojava.util.StringParser;
import seedu.tojava.frontend.*;
public class Main {
public static void main(String[] args) {
// Using the fully qualified name to access the Processor class
String status = seedu.tojava.logic.Processor.getStatus();
// Using the StringParser previously imported
StringParser sp = new StringParser();
// Using classes from the tojava.frontend package
Ui ui = new Ui();
Message m = new Message();
}
}
Note how the class can still use the Processor
without importing it first, by using its fully qualified name seedu.tojava.logic.Processor
Importing a package does not import its sub-packages, as packages do not behave as hierarchies despite appearances.
import seedu.tojava.frontend.*
does not import the classes in the sub-package seedu.tojava.frontend.widget
.
If you do not use a package statement, your type doesn't have a package -- a practice not recommended (except for small code examples) as it is not possible for a type in a package to import a type that is not in a package.
Optionally, a static import can be used to import static members of a type so that the imported members can be used without specifying the type name.
The class below uses static imports to import the constant PREFIX
and the method format()
from the seedu.tojava.util.Formatter
class.
import static seedu.tojava.util.Formatter.PREFIX;
import static seedu.tojava.util.Formatter.format;
public class Main {
public static void main(String[] args) {
String formatted = format("Hello");
boolean isFormatted = formatted.startsWith(PREFIX);
System.out.println(formatted);
}
}
Formatter
class
Note how the class can use PREFIX
and format()
(instead of Formatter.PREFIX
and Formatter.format()
).
When using the commandline to compile/run Java, you should take the package into account.
If the seedu.tojava.Main
class is defined in the file Main.java
,
<source folder>
, the command is:javac seedu/tojava/Main.java
<compiler output folder>
, the command is:java seedu.tojava.Main
Guidance for the item(s) below:
As the size of your Java codebase grows, every class being able to access every member of every other class can be problematic. Hence, there should be a way to control the access to our Java classes and their members. The solution is given in the topic below.
Can explain access modifiers
Access level modifiers determine whether other classes can use a particular field or invoke a particular method.
There are two levels of access control:
At the class level:
public
: the class is visible to all classes everywhereAt the member level:
public
or no modifier (package-private): same meaning as when used with top-level classesprivate
: the member can only be accessed in its own classprotected
: the member can only be accessed within its own package (as with package-private) and, in addition, by a subclass of its class in another packageThe following table shows the access to members permitted by each modifier.
Modifier | ||||
---|---|---|---|---|
public | ||||
protected | ||||
no modifier | ||||
private |
Access levels affect you in two ways:
Guidance for the item(s) below:
So far, your iP may have assumed a 'perfect world' e.g., user input is always in the expected format. To make the product ready for the not-so-perfect real world, the code should be able to handle error conditions. Let's learn how to do that.
Can explain error handling
Well-written applications include error-handling code that allows them to recover gracefully from unexpected errors. When an error occurs, the application may need to request user intervention, or it may be able to recover on its own. In extreme cases, the application may log the user off or shut down the system. -- Microsoft
Can explain exceptions
Exceptions are used to deal with 'unusual' but not entirely unexpected situations that the program might encounter at runtime.
Exception:
The term exception is shorthand for the phrase "exceptional event." An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program's instructions. –- Java Tutorial (Oracle Inc.)
Examples:
Can explain how exception handling is done typically
Most languages allow code that encountered an "exceptional" situation to encapsulate details of the situation in an Exception object and throw/raise that object so that another piece of code can catch it and deal with it. This is especially useful when the code that encountered the unusual situation does not know how to deal with it.
The extract below from the -- Java Tutorial (with slight adaptations) explains how exceptions are typically handled.
When an error occurs at some point in the execution, the code being executed creates an exception object and hands it off to the runtime system. The exception object contains information about the error, including its type and the state of the program when the error occurred. Creating an exception object and handing it to the runtime system is called throwing an exception.
After a method throws an exception, the runtime system attempts to find something to handle it in the . The runtime system searches the call stack for a method that contains a block of code that can handle the exception. This block of code is called an exception handler. The search begins with the method in which the error occurred and proceeds through the call stack in the reverse order in which the methods were called. When an appropriate handler is found, the runtime system passes the exception to the handler. An exception handler is considered appropriate if the type of the exception object thrown matches the type that can be handled by the handler.
The exception handler chosen is said to catch the exception. If the runtime system exhaustively searches all the methods on the call stack without finding an appropriate exception handler, the program terminates.
Advantages of exception handling in this way:
Can explain Java Exceptions
Given below is an extract from the -- Java Tutorial, with some adaptations.
There are three basic categories of exceptions In Java:
Error
, RuntimeException
, and their subclasses. Suppose an application prompts a user for an input file name, then opens the file by passing the name to the constructor for java.io.FileReader. Normally, the user provides the name of an existing, readable file, so the construction of the FileReader
object succeeds, and the execution of the application proceeds normally. But sometimes the user supplies the name of a nonexistent file, and the constructor throws java.io.FileNotFoundException
. A well-written program will catch this exception and notify the user of the mistake, possibly prompting for a corrected file name.
Error
and its subclasses. Suppose that an application successfully opens a file for input, but is unable to read the file because of a hardware or system malfunction. The unsuccessful read will throw java.io.IOError
. An application might choose to catch this exception, in order to notify the user of the problem — but it also might make sense for the program to print a stack trace and exit.
RuntimeException
and its subclasses. These usually indicate programming bugs, such as logic errors or improper use of an API. Consider the application described previously that passes a file name to the constructor for FileReader. If a logic error causes a null to be passed to the constructor, the constructor will throw NullPointerException
. The application can catch this exception, but it probably makes more sense to eliminate the bug that caused the exception to occur.
Errors and runtime exceptions are collectively known as unchecked exceptions.
Can use Java Exceptions
A program can catch exceptions by using a combination of the try
, catch
blocks.
try
block identifies a block of code in which an exception can occur.catch
block identifies a block of code, known as an exception handler, that can handle a particular type of exception. The writeList()
method below calls a method process()
that can cause two type of exceptions. It uses a try-catch construct to deal with each exception.
public void writeList() {
print("starting method");
try {
print("starting process");
process();
print("finishing process");
} catch (IndexOutOfBoundsException e) {
print("caught IOOBE");
} catch (IOException e) {
print("caught IOE");
}
print("finishing method");
}
Some possible outputs:
No exceptions | IOException | IndexOutOfBoundsException |
---|---|---|
starting method starting process finishing process finishing method | starting method starting process caught IOE finishing method | starting method starting process caught IOOBE finishing method |
You can use a finally
block to specify code that is guaranteed to execute with or without the exception. This is the right place to close files, recover resources, and otherwise clean up after the code enclosed in the try
block.
The writeList()
method below has a finally
block:
public void writeList() {
print("starting method");
try {
print("starting process");
process();
print("finishing process");
} catch (IndexOutOfBoundsException e) {
print("caught IOOBE");
} catch (IOException e) {
print("caught IOE");
} finally {
// clean up
print("cleaning up");
}
print("finishing method");
}
Some possible outputs:
No exceptions | IOException | IndexOutOfBoundsException |
---|---|---|
starting method starting process finishing process cleaning up finishing method | starting method starting process caught IOE cleaning up finishing method | starting method starting process caught IOOBE cleaning up finishing method |
The try
statement should contain at least one catch
block or a finally block and may have multiple catch
blocks.
The class of the exception object indicates the type of exception thrown. The exception object can contain further information about the error, including an error message.
You can use the throw
statement to throw an exception. The throw statement requires a object as the argument.
Here's an example of a throw
statement.
if (size == 0) {
throw new EmptyStackException();
}
In Java, Checked exceptions are subject to the Catch or Specify Requirement: code that might throw checked exceptions must be enclosed by either of the following:
try
statement that catches the exception. The try
must provide a handler for the exception.throws
clause that lists the exception.Unchecked exceptions are not required to follow to the Catch or Specify Requirement but you can apply the requirement to them too.
Here's an example of a method specifying that it throws certain checked exceptions:
public void writeList() throws IOException, IndexOutOfBoundsException {
print("starting method");
process();
print("finishing method");
}
Some possible outputs:
No exceptions | IOException | IndexOutOfBoundsException |
---|---|---|
starting method finishing method | starting method | starting method |
Java comes with a collection of built-in exception classes that you can use. When they are not enough, it is possible to create your own exception classes.
The Main
class below parses a string descriptor of a rectangle of the format "WIDTHxHEIGHT"
e.g., "3x4"
and prints the area of the rectangle.
public class Main {
public static void printArea(String descriptor){
//TODO: modify the code below
System.out.println(descriptor + "=" + calculateArea(descriptor));
}
private static int calculateArea(String descriptor) {
//TODO: modify the code below
String[] dimensions = descriptor.split("x");
return Integer.parseInt(dimensions[0]) * Integer.parseInt(dimensions[1]);
}
public static void main(String[] args) {
printArea("3x4");
printArea("5x5");
}
}
3x4=12
5x5=25
Update the code of printArea
to print an error message if WIDTH
and/or HEIGHT
are not numbers e.g., "Ax4"
calculateArea
will throw the unchecked exception NumberFormatException
if the code tries to parse a non-number to an integer.
Update the code of printArea
to print an error message if the descriptor is missing WIDTH
and/or HEIGHT
e.g., "x4"
calculateArea
will throw the unchecked exception IndexOutOfBoundsException
if one or both dimensions are missing.
Update the code of calculateArea
to throw the checked exception IllegalShapeException
if there are more than 2 dimensions e.g., "5x4x3"
and update the printArea
to print an error message for those cases. Here is the code for the IllegalShapeException.java
public class IllegalShapeException extends Exception {
//no other code needed
}
Here is the expected behavior after you have done the above changes:
public class Main {
//...
public static void main(String[] args) {
printArea("3x4");
printArea("3xy");
printArea("3x");
printArea("3");
printArea("3x4x5");
}
}
3x4=12
WIDTH or HEIGHT is not a number: 3xy
WIDTH or HEIGHT is missing: 3x
WIDTH or HEIGHT is missing: 3
Too many dimensions: 3x4x5
Partial solution
Guidance for the item(s) below:
This week, you will be dealing with the requirements aspect of the tP. While there isn't time in this week to learn that topic fully, let's learn a couple the techniques you'll be using to deal with requirements of your tP this week: user stories and feature lists.
Can write simple user stories
User story: User stories are short, simple descriptions of a feature told from the perspective of the person who desires the new capability, usually a user or customer of the system. [Mike Cohn]
A common format for writing user stories is:
User story format: As a {user type/role} I can {function} so that {benefit}
Examples (from a Learning Management System):
You can write user stories using a physical medium or a digital tool. For example, you can use index cards or sticky notes, and arrange them on walls or tables. Alternatively, you can use a software (e.g., GitHub Project Boards, Trello, Google Docs, ...) to manage user stories digitally.
User stories in use
With sticky notes
With paper
With software
Can write more detailed user stories
The {benefit}
can be omitted if it is obvious.
As a user, I can login to the system so that I can access my data
It is recommended to confirm there is a concrete benefit even if you omit it from the user story. If not, you could end up adding features that have no real benefit.
You can add more characteristics to the {user role}
to provide more context to the user story.
You can write user stories at various levels. High-level user stories, called epics (or themes) cover bigger functionality. You can then break down these epics to multiple user stories of normal size.
[Epic] As a lecturer, I can monitor student participation levels
You can add conditions of satisfaction to a user story to specify things that need to be true for the user story implementation to be accepted as ‘done’.
As a lecturer, I can view the forum post count of each student so that I can identify the activity level of students in the forum.
Conditions:
Separate post count for each forum should be shown
Total post count of a student should be shown
The list should be sortable by student name and post count
Other useful info that can be added to a user story includes (but not limited to)
Can use user stories to manage requirements of project
User stories capture user requirements in a way that is convenient for , , and .
[User stories] strongly shift the focus from writing about features to discussing them. In fact, these discussions are more important than whatever text is written. [Mike Cohn, MountainGoat Software 🔗]
User stories differ from mainly in the level of detail. User stories should only provide enough details to make a reasonably low risk estimate of how long the user story will take to implement. When the time comes to implement the user story, the developers will meet with the customer face-to-face to work out a more detailed description of the requirements. [more...]
User stories can capture non-functional requirements too because even NFRs must benefit some stakeholder.
An example of an NFR captured as a user story:
As a | I want to | so that |
---|---|---|
impatient user | to be able to experience reasonable response time from the website while up to 1000 concurrent users are using it | I can use the app even when the traffic is at the maximum expected level |
Given their lightweight nature, user stories are quite handy for recording requirements during early stages of requirements gathering.
Given below is a possible recipe you can take when using user stories for early stages of requirement gathering.
Step 0: Clear your mind of preconceived product ideas
Even if you already have some idea of what your product will look/behave like in the end, clear your mind of those ideas. The product is the solution. At this point, we are still at the stage of figuring out the problem (i.e., user requirements). Let's try to get from the problem to the solution in a systematic way, one step at a time.
Step 1: Define the target user as a persona:
Decide your target user's profile (e.g. a student, office worker, programmer, salesperson) and work patterns (e.g. Does he work in groups or alone? Does he share his computer with others?). A clear understanding of the target user will help when deciding the importance of a user story. You can even narrow it down to a persona. Here is an example:
Jean is a university student studying in a non-IT field. She interacts with a lot of people due to her involvement in university clubs/societies. ...
Step 2: Define the problem scope:
Decide the exact problem you are going to solve for the target user. It is also useful to specify what related problems it will not solve so that the exact scope is clear.
ProductX helps Jean keep track of all her school contacts. It does not cover communicating with contacts.
Step 3: List scenarios to form a narrative:
Think of the various scenarios your target user is likely to go through as she uses your app. Following a chronological sequence as if you are telling a story might be helpful.
A. First use:
- Jean gets to know about ProductX. She downloads it and launches it to check out what it can do.
- After playing around with the product for a bit, Jean wants to start using it for real.
- ...
B. Second use: (Jean is still a beginner)
- Jean launches ProductX. She wants to find ...
- ...
C. 10th use: (Jean is a little bit familiar with the app)
- ...
D. 100th use: (Jean is an expert user)
- Jean launches the app and does ... and ... followed by ... as per her usual habit.
- Jean feels some of the data in the app are no longer needed. She wants to get rid of them to reduce clutter.
More examples that might apply to some products:
- Jean uses the app at the start of the day to ...
- Jean uses the app before going to sleep to ...
- Jean hasn't used the app for a while because she was on a three-month training programme. She is now back at work and wants to resume her daily use of the app.
- Jean moves to another company. Some of her clients come with her but some don't.
- Jean starts freelancing in her spare time. She wants to keep her freelancing clients separate from her other clients.
Step 4: List the user stories to support the scenarios:
Based on the scenarios, decide on the user stories you need to support. For example, based on the scenario 'A. First use', you might have user stories such as these:
- As a potential user exploring the app, I can see the app populated with sample data, so that I can easily see how the app will look like when it is in use.
- As a user ready to start using the app, I can purge all current data, so that I can get rid of sample/experimental data I used for exploring the app.
To give another example, based on the scenario 'D. 100th use', you might have user stories such as these:
- As an expert user, I can create shortcuts for tasks, so that I can save time on frequently performed tasks.
- As a long-time user, I can archive/hide unused data, so that I am not distracted by irrelevant data.
Do not 'evaluate' the value of user stories while brainstorming. Reason: an important aspect of brainstorming is not judging the ideas generated.
Other tips:
As a user, I want to see a list of tasks that need my attention most at the present time, so that I pay attention to them first.
While use cases can be recorded on in the initial stages, an online tool is more suitable for longer-term management of user stories, especially if the team is not .
Tool Examples: How to use some example online tools to manage user stories
Can explain feature list
Feature list: A list of features of a product grouped according to some criteria such as aspect, priority, order of delivery, etc.
A sample feature list from a simple Minesweeper game (only a brief description has been provided to save space):
Guidance for the item(s) below:
Let's learn about a few more Git techniques, starting with branching. Although these techniques are not really needed for the iP, we require you to use them in the iP so that you have more time to practice them before they are really needed in the tP.
Can explain branching
Branching is the process of evolving multiple versions of the software in parallel. For example, one team member can create a new branch and add an experimental feature to it while the rest of the team keeps working on another branch. Branches can be given names e.g. master
, release
, dev
.
A branch can be merged into another branch. Merging usually results in a new commit that represents the changes done in the branch being merged.
Merge conflicts happen when you try to merge two branches that had changed the same part of the code and the RCS cannot decide which changes to keep. In those cases, you have to ‘resolve’ the conflicts manually.
Can use Git branching
Git supports branching, which allows you to do multiple parallel changes to the content of a repository.
First, let us learn how the repo looks like as you perform branching operations.
A Git branch is simply a named label pointing to a commit. The HEAD
label indicates which branch you are on. Git creates a branch named master
by default. When you add a commit, it goes into the branch you are currently on, and the branch label (together with the HEAD
label) moves to the new commit.
Given below is an illustration of how branch labels move as branches evolve. Refer to the text below it for explanations of each stage.
There is only one branch (i.e., master
) and there is only one commit on it. The HEAD
label is pointing to the master
branch (as we are currently on that branch).
To learn a bit more about how labels such as master
and HEAD
work, you can refer to this article.
A new commit has been added. The master
and the HEAD
labels have moved to the new commit.
A new branch fix1
has been added. The repo has switched to the new branch too (hence, the HEAD
label is attached to the fix1
branch).
A new commit (c
) has been added. The current branch label fix1
moves to the new commit, together with the HEAD
label.
The repo has switched back to the master
branch. Hence, the HEAD
has moved back to master
branch's .
At this point, the repo's working directory reflects the code at commit b
(not c
).
d
) has been added. The master
and the HEAD
labels have moved to that commit.fix1
branch and added a new commit (e
) to it.master
branch and the fix1
branch has been merged into the master
branch, creating a merge commit f
. The repo is currently on the master
branch.Now that you have some idea how the repo will look like when branches are being used, let's follow the steps below to learn how to perform branching operations using Git. You can use any repo you have on your computer (e.g. a clone of the samplerepo-things) for this.
0. Observe that you are normally in the branch called master
.
$ git status
on branch master
1. Start a branch named feature1
and switch to the new branch.
Click on the Branch
button on the main menu. In the next dialog, enter the branch name and click Create Branch
.
Note how the feature1
is indicated as the current branch (reason: Sourcetree automatically switches to the new branch when you create a new branch).
You can use the branch
command to create a new branch and the checkout
command to switch to a specific branch.
$ git branch feature1
$ git checkout feature1
One-step shortcut to create a branch and switch to it at the same time:
$ git checkout –b feature1
The new switch
command
Git recently introduced a switch
command that you can use instead of the checkout
command given above.
To create a new branch and switch to it:
$ git branch feature1
$ git switch feature1
One-step shortcut:
$ git switch –c feature1
2. Create some commits in the new branch. Just commit as per normal. Commits you add while on a certain branch will become part of that branch.
Note how the master
label and the HEAD
label moves to the new commit (The HEAD
label of the local repo is represented as in Sourcetree, as illustrated in the screenshot below).
3. Switch to the master
branch. Note how the changes you did in the feature1
branch are no longer in the working directory.
Double-click the master
branch.
Revisiting master
vs origin/master
In the screenshot above, you see a master
label and a origin/master
label for the same commit. The former identifies the of the local master
branch while the latter identifies the tip of the master
branch at the remote repo named origin
. The fact that both label points to the same commit means the local master
branch and its remote counterpart are with each other.
Similarly, origin/HEAD
label appearing against the same commit indicates that the of the remote repo is pointing to this commit as well.
$ git checkout master
4. Add a commit to the master branch. Let’s imagine it’s a bug fix.
To keep things simple for the time being, this commit should not involve the same content that you changed in the feature1
branch. To be on the safe side, you can change an entirely different file in this commit.
5. Switch back to the feature1
branch (similar to step 3).
6. Merge the master
branch to the feature1
branch, giving an end-result like the following. Also note how Git has created a merge commit.
Right-click on the master
branch and choose merge master into the current branch
. Click OK
in the next dialog.
$ git merge master
The objective of that merge was to sync the feature1
branch with the master
branch. Observe how the changes you did in the master
branch (i.e. the imaginary bug fix) is now available even when you are in the feature1
branch.
To undo a merge,
In the example below, you merged master
to feature1
.
If you want to undo that merge,
feature1
branch.feature1
branch to the commit highlighted in the screenshot above (because that was the tip of the feature1
branch before you merged the master
branch to it.Instead of merging master
to feature1
, an alternative is to rebase the feature1
branch. However, rebasing is an advanced feature that requires modifying past commits. If you modify past commits that have been pushed to a remote repository, you'll have to force-push the modified commit to the remote repo in order to update the commits in it.
7. Add another commit to the feature1
branch.
8. Switch to the master
branch and add one more commit.
9. Merge feature1
to the master branch, giving and end-result like this:
Right-click on the feature1
branch and choose Merge...
.
$ git merge feature1
10. Create a new branch called add-countries
, switch to it, and add some commits to it (similar to steps 1-2 above). You should have something like this now:
Avoid this rookie mistake!
Always remember to switch back to the master
branch before creating a new branch. If not, your new branch will be created on top of the current branch.
11. Go back to the master
branch and merge the add-countries
branch onto the master
branch (similar to steps 8-9 above). While you might expect to see something like the following,
... you are likely to see something like this instead:
That is because Git does a fast forward merge if possible. Seeing that the master
branch has not changed since you started the add-countries
branch, Git has decided it is simpler to just put the commits of the add-countries
branch in front of the master
branch, without going into the trouble of creating an extra merge commit.
It is possible to force Git to create a merge commit even if fast forwarding is possible.
Tick the box shown below when you merge a branch:
Use the --no-ff
switch (short for no fast forward):
$ git merge --no-ff add-countries
Can work with remote branches
Git branches in a local repo can be linked to a branch in a remote repo so the local branch can 'track' the corresponding remote branch, and revision history contained in the local and the remote branch pair can be synchronized as desired.
[A] Pushing a new branch to a remote repo
Let's see how you can push a branch that you created in your local repo to the remote repo. Note that this branch does not exist in the remote repo yet.
Given below is how to push a branch named add-intro
to your own fork named samplerepo-pr-practice
.
We assume that your local repo already has the remote added to it with the name origin
. If that is not the case, you should first configure your local repo to be able to communicate with the target remote repo.
Push
button, which opens up the Push dialog.add-intro
.Track?
checkbox is ticked for the selected branch(es).Push
.$ git push -u origin add-intro
The -u
(or --set-upstream
) flag tells Git that you wish the local branch to 'track' the remote branch that will be created as a result of this push.
See git-scm.com/docs/git-push for details of the push
command.
[B] Pulling a remote branch for the first time
Here, let's see how to fetch a new branch (i.e., it does not exist in your local repo yet) from a remote repo.
1. Check the list of remote branches by expanding the REMOTES
menu on the left edge of Sourcetree. If the branch you expected to find is missing, you can click the Fetch
button (in the top toolbar) to refresh the information shown under remotes.
2. Double-click the branch name (e.g., tweak-requirements
branch in the myfork
remote), which should open the checkout dialog shown below.
3. Go with the default settings (shown above) should be fine. Once you click OK
, the branch will appear in your local repo. Furthermore, that repo will switch to that branch, and the local branch will the remote branch as well.
1. Fetch details from the remote. e.g., if the remote is named myfork
$ git fetch myfork
2. List the branches to see the name of the branch you want to pull.
$ git branch -a
master
remotes/myfork/master
remotes/myfork/branch1
-a
flag tells Git to list both local and remote branches.
3. Create a matching local branch and switch to it.
$ git switch -c branch1 myfork/branch1
Switched to a new branch 'branch1'
branch 'branch1' set up to track 'myfork/branch1'.
-c
flag tells Git to create a new local branch.
[C] Syncing branches
In this section we assume that you have a local branch that is already tracking a remote branch (e.g., as a result of doing [A] or [B] above).
To push new changes in the local branch to the corresponding remote branch:
Similar to how you pushed a new branch (in [A]):
Similar to [A] above, but omit the -u
flag. e.g.,
$ git push origin add-intro
If you push but the remote branch has new commits that you don't have locally, Git will abort the push and will ask you to pull first.
To pull new changes from a remote branch to the corresponding local branch:
1. Switch to the branch you want to update by double-clicking the branch name. e.g.,
2. Pull the updated in the remote branch to the local branch by right-clicking on the branch name (in the same place as above), and choosing Pull <remote>/<branch> (tracked)
e.g., Pull myfork/add-intro (tracked)
.
1. Switch to the branch you want to update using git checkout <branch>
e.g.,
$ git checkout branch1
2. Pull the updated in the remote branch to the local branch, using git pull <remote> <branch>
e.g.,
$ git pull origin branch1
If you pull but your local branch has new commits the remote branch doesn't have, Git will automatically perform a merge between the local branch and the remote branch.