Noticed any bugs/issues or unclear areas while following this tutorial? Help us improve it by reporting it at our issue tracker.
Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …[Therefore,] making it easy to read makes it easier to write.
— Robert C. Martin Clean Code: A Handbook of Agile Software Craftsmanship
When trying to understand an unfamiliar code base, one common strategy used is to trace some representative execution path through the code base. One easy way to trace an execution path is to use a debugger to step through the code. In this tutorial, you will be using the IntelliJ IDEA’s debugger to trace the execution path of a specific user command to:
(a) learn how to use a debugger to trace code, and
(b) get a preliminary sense of how AB3's code works.
Before we jump into the code, it is useful to get an idea of the overall structure and the high-level behavior of the application. This is provided in the 'Architecture' section of the developer guide. In particular, the architecture diagram (reproduced below), tells us that the App consists of several components.
It also has a sequence diagram (reproduced below) that tells us how a command propagates through the App.
Note how the diagram shows only the execution flows between the main components. That is, it does not show details of the execution path inside each component. By hiding those details, the diagram aims to inform the reader about the overall execution path of a command without overwhelming the reader with too much details. In this tutorial, you aim to find those omitted details so that you get a more in-depth understanding of how the code works.
Before we proceed, ensure that you have done the following:
./gradlew run
-- AB3 app should launch, and you should be able to interact with it.src\main\java\seedu\address\Main.java
) and ensure the app launches.As you know, the first step of debugging is to put in a breakpoint where you want the debugger to pause the execution (e.g., to debug an issue with data saving, you might put a breakpoint in the method that saves data). When using the debugger to trace the code (to learn how it works), we start the same way, by putting a breakpoint in the code where we want to start tracing the execution path.
In our case, it's natural that we start tracing the code at the point where the App start processing user input (i.e., somewhere in the UI component), and then trace through how the execution proceeds through the UI component. That is the part of the UI
component's activation bar indicated by the yellow circle in the component-level sequence diagram below.
However, the execution path through a GUI is often somewhat obscure due to various used by GUI frameworks. Therefore, let us skip that part of the execution path for now, and put the breakpoint where the UI
transfers control to the Logic
component.
According to the sequence diagram, the UI
component yields control to the Logic
component through a method named execute
(also indicated by the yellow circle in the diagram above). So, let's put our breakpoint in the execute
method of the Logic
component.
Searching through the code base for an execute()
method that belongs to the Logic
component yields a promising candidate in seedu.address.logic.Logic
, as shown below.
Intellij Tip: The 'Search Everywhere' feature can be used here. In particular, the 'Find Symbol' ('Symbol' here refers to methods, variables, classes etc.) variant of that feature is quite useful here as we are looking for a method named execute
, not simply the text execute
.
A quick look at the seedu.address.logic.Logic
(an extract given below) confirms that this indeed might be what we’re looking for.
public interface Logic {
/**
* Executes the command and returns the result.
* @param commandText The command as entered by the user.
* @return the result of the command execution.
* @throws CommandException If an error occurs during command execution.
* @throws ParseException If an error occurs during parsing.
*/
CommandResult execute(String commandText) throws CommandException, ParseException;
...
}
But apparently, this is an interface, not a concrete implementation. That should be fine because the Architecture section of the Developer Guide tells us that components interact through interfaces. Here's the relevant diagram:
Next, let's find out which statement(s) in the UI
code is calling this method, thus transferring control from the UI
to the Logic
.
Intellij Tip: The 'Find Usages' feature can find from which parts of the code a class/method/variable is being used.
Bingo! MainWindow#executeCommand()
seems to be exactly what we’re looking for!
Now let’s set the breakpoint. First, double-click the item to reach the corresponding code. Once there, set a breakpoint, as shown below.
Recall from the User Guide that the edit
command has the format: edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…
For this tutorial we will be issuing the command edit 1 n/Alice Yeoh
.
Tip: Over the course of the debugging session, you will encounter every major component in the application. Try to keep track of what happens inside the component and where the execution transfers to another component.
Run
> Debug Main
When the GUI appears, enter edit 1 n/Alice Yeoh
into the command box and press Enter.
The Debugger tool window should show up and show something like this:
CommandResult commandResult = logic.execute(commandText);
, as that is where we put the breakpoint).logic.execute(commandText)
portion of that line so let’s Step in into that method call:We end up in LogicManager#execute()
(not Logic#execute
-- but this is expected because we know the execute()
method in the Logic
interface is actually implemented by the LogicManager
class). Let’s take a look at the body of the method. Given below is the same code, with additional explanatory comments.
LogicManager#execute().
@Override
public CommandResult execute(String commandText)
throws CommandException, ParseException {
//Logging, safe to ignore
logger.info("----------------[USER COMMAND][" + commandText + "]");
CommandResult commandResult;
//Parse user input from String to a Command
Command command = addressBookParser.parseCommand(commandText);
//Executes the Command and stores the result
commandResult = command.execute(model);
try {
//We can deduce that the previous line of code modifies model in some way
// since it's being stored here.
storage.saveAddressBook(model.getAddressBook());
} catch (IOException ioe) {
throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe);
}
return commandResult;
}
LogicManager#execute()
appears to delegate most of the heavy lifting to other components. Let’s take a closer look at each one.
Step over the logging code since it is of no interest to us now.
Step into the line where user input is parsed from a String to a Command, which should bring you to the AddressBookParser#parseCommand()
method (partial code given below):
public Command parseCommand(String userInput) throws ParseException {
...
final String commandWord = matcher.group("commandWord");
final String arguments = matcher.group("arguments");
...
Step over the statements in that method until you reach the switch
statement. The 'Variables' window now shows the value of both commandWord
and arguments
:
We see that the value of commandWord
is now edit
but arguments
is still not processed in any meaningful way.
Stepping through the switch
block, we end up at a call to EditCommandParser().parse()
as expected (because the command we typed is an edit command).
...
case EditCommand.COMMAND_WORD:
return new EditCommandParser().parse(arguments);
...
Let’s see what EditCommandParser#parse()
does by stepping into it. You might have to click the 'step into' button multiple times here because there are two method calls in that statement: EditCommandParser()
and parse()
.
Intellij Tip: Sometimes, you might end up stepping into functions that are not of interest. Simply use the step out
button to get out of them!
Stepping through the method shows that it calls ArgumentTokenizer#tokenize()
and ParserUtil#parseIndex()
to obtain the arguments and index required.
The rest of the method seems to exhaustively check for the existence of each possible parameter of the edit
command and store any possible changes in an EditPersonDescriptor
. Recall that we can verify the contents of editPersonDescriptor
through the 'Variables' window.
As you just traced through some code involved in parsing a command, you can take a look at this class diagram to see where the various parsing-related classes you encountered fit into the design of the Logic
component.
Let’s continue stepping through until we return to LogicManager#execute()
.
The sequence diagram below shows the details of the execution path through the Logic component. Does the execution path you traced in the code so far match the diagram?
Now, step over until you read the statement that calls the execute()
method of the EditCommand
object received, and step into that execute()
method (partial code given below):
EditCommand#execute()
:
@Override
public CommandResult execute(Model model) throws CommandException {
...
Person personToEdit = lastShownList.get(index.getZeroBased());
Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor);
if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) {
throw new CommandException(MESSAGE_DUPLICATE_PERSON);
}
model.setPerson(personToEdit, editedPerson);
model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson));
}
command#execute()
does indeed make changes to the model
object. Specifically,setPerson()
method (defined in the interface Model
and implemented in ModelManager
as per the usual pattern) to update the person data.updateFilteredPersonList
method to ask the Model
to populate the 'filtered list' with all persons.edit
command, we populate it with all the persons so that the user can see the edited person along with all other persons. If this was a find
command, we would be setting that list to contain the search results instead.Model
component. See if you can figure out where the 'filtered list' of persons is being tracked.As you step through the rest of the statements in the EditCommand#execute()
method, you'll see that it creates a CommandResult
object (containing information about the result of the execution) and returns it.
Advancing the debugger by one more step should take you back to the middle of the LogicManager#execute()
method.
Given that you have already seen quite a few classes in the Logic
component in action, see if you can identify in this partial class diagram some of the classes you've encountered so far, and see how they fit into the class structure of the Logic
component:
LogicManager#execute()
method to examine how the control is transferred to the Storage
component and what happens inside that component.If the statement has multiple method calls, how to step into a specific method
As you step through the code inside the Storage
component, you will eventually arrive at the JsonAddressBookStorage#saveAddressBook()
method which calls the JsonSerializableAddressBook
constructor, to create an object that can be serialized (i.e., stored in storage medium) in JSON format. That constructor is given below (with added line breaks for easier readability):
JsonSerializableAddressBook
constructor:
/**
* Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use.
*
* @param source future changes to this will not affect the created
* {@code JsonSerializableAddressBook}.
*/
public JsonSerializableAddressBook(ReadOnlyAddressBook source) {
persons.addAll(
source.getPersonList()
.stream()
.map(JsonAdaptedPerson::new)
.collect(Collectors.toList()));
}
It appears that a JsonAdaptedPerson
is created for each Person
and then added to the JsonSerializableAddressBook
.
This is because regular Java objects need to go through an adaptation for them to be suitable to be saved in JSON format.
While you are stepping through the classes in the Storage
component, here is the component's class diagram to help you understand how those classes fit into the structure of the component.
We can continue to step through until you reach the end of the LogicManager#execute()
method and return to the MainWindow#executeCommand()
method (the place where we put the original breakpoint).
Stepping into resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser());
, we end up in:
ResultDisplay#setFeedbackToUser()
public void setFeedbackToUser(String feedbackToUser) {
requireNonNull(feedbackToUser);
resultDisplay.setText(feedbackToUser);
}
Finally, you can step through until you reach the end ofMainWindow#executeCommand()
.
DG This is a good time to read through the UI component section of the DG
Here are some quick questions you can try to answer based on your execution path tracing. In some cases, you can do further tracing for the given commands to find exactly what happens.
[A] In this tutorial, we traced the "happy path" (i.e., no errors). What do you think will happen if we traced the following commands instead? What exceptions do you think will be thrown (if any), where will the exceptions be thrown and where will they be handled?
[B] What components will you have to modify to perform the following enhancements to the application?
ToC | What's next? Adding a Command
Authors: