Last week, you learned intermediate-level class diagram notation. But the quiz only focused on interpreting such diagrams. This week, we cover the aspect of drawing intermediate-level class diagrams to match code. Here are what you can do:
1. Do Part 1 of this week's quiz (which covers the same area).
2. Watch the worked examples in the following videos to learn the process of drawing intermediate-level class diagrams to match code.
Action
, Task
, History
Person
, Inbox
, Message
Person
, Project
, Task
Detailed Table of Contents
Guidance for the item(s) below:
This week, we learn a few design principles that you can try to apply in your project.
These principles build on top of the design fundamentals you learned earlier (i.e., abstraction, coupling, cohesion).
Can explain separation of concerns principle
Separation of concerns principle (SoC): To achieve better modularity, separate the code into distinct sections, such that each section addresses a separate concern. -- Proposed by Edsger W. Dijkstra
A concern in this context is a set of information that affects the code of a computer program.
Examples for concerns:
add employee
featurepersistence
or security
Employee
entityApplying reduces functional overlaps among code sections and also limits the ripple effect when changes are introduced to a specific part of the system.
If the code related to persistence is separated from the code related to security, a change to how the data are persisted will not need changes to how the security is implemented.
This principle can be applied at the class level, as well as at higher levels.
The n-tier architecture utilizes this principle. Each layer in the architecture has a well-defined functionality that has no functional overlap with each other.
This principle should lead to higher cohesion and lower coupling.
Can explain single responsibility principle
Single responsibility principle (SRP): A class should have one, and only one, reason to change. -- Robert C. Martin
If a class has only one responsibility, it needs to change only when there is a change to that responsibility.
Consider a TextUi
class that does parsing of the user commands as well as interacting with the user. That class needs to change when the formatting of the UI changes as well as when the syntax of the user command changes. Hence, such a class does not follow the SRP.
Gather together the things that change for the same reasons. Separate those things that change for different reasons. ―- Agile Software Development, Principles, Patterns, and Practices by Robert C. Martin
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:
As you know, adding comments to the code is a good practice. Let's learn about a specific type of comments that you can use in Java code that can do even more than just explain the code.
Can explain JavaDoc
JavaDoc is a tool for generating API documentation in HTML format from comments in the source code. In addition, modern IDEs use JavaDoc comments to generate explanatory tooltips.
An example method header comment in JavaDoc format:
/**
* Returns an Image object that can then be painted on the screen.
* The url argument must specify an absolute {@link URL}. The name
* argument is a specifier that is relative to the url argument.
* <p>
* This method always returns immediately, whether or not the
* image exists. When this applet attempts to draw the image on
* the screen, the data will be loaded. The graphics primitives
* that draw the image will incrementally paint on the screen.
*
* @param url An absolute URL giving the base location of the image.
* @param name The location of the image, relative to the url argument.
* @return The Image at the specified URL.
* @see Image
*/
public Image getImage(URL url, String name) {
try {
return getImage(new URL(url, name));
} catch (MalformedURLException e) {
return null;
}
}
Generated HTML documentation:
Tooltip generated by IntelliJ IDE:
Can write JavaDoc comments
In the absence of more extensive guidelines (e.g., given in a coding standard adopted by your project), you can follow the two examples below in your code.
A minimal JavaDoc comment example for methods:
/**
* Returns lateral location of the specified position.
* If the position is unset, NaN is returned.
*
* @param x X coordinate of position.
* @param y Y coordinate of position.
* @param zone Zone of position.
* @return Lateral location.
* @throws IllegalArgumentException If zone is <= 0.
*/
public double computeLocation(double x, double y, int zone)
throws IllegalArgumentException {
// ...
}
A minimal JavaDoc comment example for classes:
package ...
import ...
/**
* Represents a location in a 2D space. A <code>Point</code> object corresponds to
* a coordinate represented by two integers e.g., <code>3,6</code>
*/
public class Point {
// ...
}
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 sometimes results in a new commit (called the merge commit) being created, to represent the cumulative changes done in the receiving branch as a result of the merge.
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 which moves forward automatically as you add more commits to that branch. The HEAD
label indicates which branch you are on.
Git creates a branch named master
(or main
) 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.
Note that how the revision graph appears (colors, positioning, orientation etc.) in your Git GUI varies based on the GUI you use, and might not match the exact diagrams given above.
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 labels point 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 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 (because that's the branch that received the merge).feature1
branch to the commit highlighted (in yellow) 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.
Windows: Tick the box shown below when you merge a branch:
Mac:
Settings
.Git
section.Do not fast-forward when merging, always create commit
.Use the --no-ff
switch (short for no fast forward):
$ git merge --no-ff add-countries
Can use Git to resolve merge conflicts
Merge conflicts happen when you try to combine two incompatible versions (e.g., merging a branch to another but each branch changed the same part of the code in a different way).
Here are the steps to simulate a merge conflict and use it to learn how to resolve merge conflicts.
0. Create an empty repo or clone an existing repo, to be used for this activity.
1. Start a branch named fix1
in the repo. Create a commit that adds a line with some text to one of the files.
2. Switch back to master
branch. Create a commit with a conflicting change i.e. it adds a line with some different text in the exact location the previous line was added.
3. Try to merge the fix1
branch onto the master
branch. Git will pause mid-way during the merge and report a merge conflict. If you open the conflicted file, you will see something like this:
COLORS
------
blue
<<<<<< HEAD
black
=======
green
>>>>>> fix1
red
white
4. Observe how the conflicted part is marked between a line starting with <<<<<<
and a line starting with >>>>>>
, separated by another line starting with =======
.
Highlighted below is the conflicting part that is coming from the master
branch:
blue
<<<<<< HEAD
black
=======
green
>>>>>> fix1
red
This is the conflicting part that is coming from the fix1
branch:
blue
<<<<<< HEAD
black
=======
green
>>>>>> fix1
red
5. Resolve the conflict by editing the file. Let us assume you want to keep both lines in the merged version. You can modify the file to be like this:
COLORS
------
blue
black
green
red
white
6. Stage the changes, and commit. You have now successfully resolved the merge conflict.
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).
[C1] 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.
[C2] 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.
Guidance for the item(s) below:
Given below are some very basic tools and techniques that are often used in planning, scheduling, and tracking projects.
Can explain milestones
A milestone is the end of a stage which indicates significant progress. You should take into account dependencies and priorities when deciding on the features to be delivered at a certain milestone.
Each intermediate product release is a milestone.
In some projects, it is not practical to have a very detailed plan for the whole project due to the uncertainty and unavailability of required information. In such cases, you can use a high-level plan for the whole project and a detailed plan for the next few milestones.
Milestones for the Minesweeper project, iteration 1
Day | Milestones |
---|---|
Day 1 | Architecture skeleton completed |
Day 3 | ‘new game’ feature implemented |
Day 4 | ‘new game’ feature tested |
Can explain buffers
A buffer is time set aside to absorb any unforeseen delays. It is very important to include buffers in a software project schedule because effort/time estimations for software development are notoriously hard. However, do not inflate task estimates to create hidden buffers; have explicit buffers instead. Reason: With explicit buffers, it is easier to detect incorrect effort estimates which can serve as feedback to improve future effort estimates.
Can explain issue trackers
Keeping track of project tasks (who is doing what, which tasks are ongoing, which tasks are done etc.) is an essential part of project management. In small projects, it may be possible to keep track of tasks using simple tools such as online spreadsheets or general-purpose/light-weight task tracking tools such as Trello. Bigger projects need more sophisticated task tracking tools.
Issue trackers (sometimes called bug trackers) are commonly used to track task assignment and progress. Most online project management software such as GitHub, SourceForge, and BitBucket come with an integrated issue tracker.
A screenshot from the Jira Issue tracker software (Jira is part of the BitBucket project management tool suite):
Can explain work breakdown structures
A Work Breakdown Structure (WBS) depicts information about tasks and their details in terms of subtasks. When managing projects, it is useful to divide the total work into smaller, well-defined units. Relatively complex tasks can be further split into subtasks. In complex projects, a WBS can also include prerequisite tasks and effort estimates for each task.
The high level tasks for a single iteration of a small project could look like the following:
Task ID | Task | Estimated Effort | Prerequisite Task |
---|---|---|---|
A | Analysis | 1 man day | - |
B | Design | 2 man day | A |
C | Implementation | 4.5 man day | B |
D | Testing | 1 man day | C |
E | Planning for next version | 1 man day | D |
The effort is traditionally measured in man hour/day/month i.e. work that can be done by one person in one hour/day/month. The Task ID is a label for easy reference to a task. Simple labeling is suitable for a small project, while a more informative labeling system can be adopted for bigger projects.
An example WBS for a game development project.
Task ID | Task | Estimated Effort | Prerequisite Task |
---|---|---|---|
A | High level design | 1 man day | - |
B |
Detail design
|
2 man day
| A |
C |
Implementation
|
4.5 man day
|
|
D | System Testing | 1 man day | C |
E | Planning for next version | 1 man day | D |
All tasks should be well-defined. In particular, it should be clear as to when the task will be considered done.
Some examples of ill-defined tasks and their better-defined counterparts:
Bad | Better |
---|---|
more coding | implement component X |
do research on UI testing | find a suitable tool for testing the UI |
Last week, you learned intermediate-level class diagram notation. But the quiz only focused on interpreting such diagrams. This week, we cover the aspect of drawing intermediate-level class diagrams to match code. Here are what you can do:
1. Do Part 1 of this week's quiz (which covers the same area).
2. Watch the worked examples in the following videos to learn the process of drawing intermediate-level class diagrams to match code.
Action
, Task
, History
Person
, Inbox
, Message
Person
, Project
, Task