Skip to content
This repository has been archived by the owner on Mar 5, 2023. It is now read-only.

ArgumentTokenizer: ensure tokenizing before getting values #312 #320

Merged
merged 4 commits into from
Mar 29, 2017

Conversation

PierceAndy
Copy link
Contributor

@PierceAndy PierceAndy commented Mar 6, 2017

Closes #312.

ArgumentTokenizer's tokenized argument getter methods can be accessed before ArgumentTokenizer is given arguments to tokenize through ArgumentTokenizer#tokenize(String).

Furthermore, the getter methods in ArgumentTokenizer, getValue(Prefix), getAllValues(Prefix), and getPreamble(), don't give a warning when they are called before ArgumentTokenizer#tokenize(...) is ever called.

This behaviour is problematic as the getters can be called before ArgumentTokenizer#tokenize(...) without any warnings, and would return empty optionals, lists, and strings that are indistinguishable from normal empty results.

Link to discussion that highlighted this issue

Proposed merge commit message:

ArgumentTokenizer's tokenized arguments getters can be called before
arguments have been tokenized with ArgumentTokenizer#tokenize(String).

This is problematic as the getters would return empty Optionals, lists,
and strings that are indistinguishable from normal empty results.

Let's encapsulate ArgumentTokenizer's getters in ArgumentMultimap such
that we can enforce the sequence of method calls and simplify
ArgumentTokenizer's logic.

@mention-bot
Copy link

@PierceAndy, thanks for your PR! By analyzing the history of the files in this pull request, we identified @ndt93, @MightyCupcakes and @chao1995 to be potential reviewers.

@CanIHasReview-bot
Copy link

v1

@PierceAndy submitted v1 for review.

(📚 Archive)

Checkout this PR version locally
git fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/1/head:BRANCHNAME

where BRANCHNAME is the name of the local branch you want to fetch this PR to.

Copy link
Contributor

@MightyCupcakes MightyCupcakes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this solution is chosen over the step-builder pattern because we create a new tokenizer object whenever we parse a user input (and it seems there are currently no use-cases that requires multiple parsing of similar arg strings except in the test cases)

Anyway, looks okay to me.

@PierceAndy
Copy link
Contributor Author

@MightyCupcakes Yes, the step builder pattern and nested classes work, but I realized that they were unnecessarily complex, for the reason you mentioned. I will improve the first commit's message with what you wrote in the next iteration.

Moving the logic into the constructor is preferred over other potential
solutions such as the step builder pattern or nested classes, as this
solution does not involve the creation of additional objects and is more
efficient. Furthermore, there is no need for a more complex solution that
preserves ArgumentTokenizer#tokenize(...) as a separate method, as a
new ArgumentTokenizer object is created each time we parse a user input,
and there are no cases that require consecutive parsing of arguments
with identical prefixes other than in the test cases.

@damithc
Copy link
Contributor

damithc commented Mar 7, 2017

@PierceAndy what about this alternative?

Arguments arguments = new ArgumentTokenizer().tokenize(...);
String name = agruments.getValue(...)

There is a general consensus that doing work inside a constructor is not good. But as you said, we don't want to complicate things too much too.

@PierceAndy
Copy link
Contributor Author

@damithc Right, that would be a simple solution while reducing constructor complexity.

@CanIHasReview-bot
Copy link

v2

@PierceAndy submitted v2 for review.

(📚 Archive) (📈 Interdiff between v1 and v2)

Checkout this PR version locally
git fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/2/head:BRANCHNAME

where BRANCHNAME is the name of the local branch you want to fetch this PR to.

Copy link
Contributor

@chao1995 chao1995 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[v2 1/1]

I really like this API design. Good API enforces people to do the right thing.

Some changes suggested below.

this.tokenizedArguments.get(prefix).add(value);
private void saveArgument(Map<Prefix, List<String>> tokenizedArguments, Prefix prefix, String value) {
if (tokenizedArguments.containsKey(prefix)) {
tokenizedArguments.get(prefix).add(value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a nice method, getOrDefault, which I think can help to get rid of this method.

Something like

List<String> values = tokenizedArguments.getOrDefault(prefix, new ArrayList<>());
values.add(value);
tokenizedArguments.put(prefix, values);

And probably we can just do this inside extractArugments(...).

private void resetTokenizerState() {
this.tokenizedArguments.clear();
Map<Prefix, List<String>> tokenizedArguments = extractArguments(argsString, positions);
return new Arguments(tokenizedArguments);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better for extractArguments(...) to return Arguments? Sounds more intuitively. Like

Arguments arguments = extractArguments(argsString, positions);
return arguments;

And then only deal with the Map inside extractArguments(...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was debating on which return type to give extractArguments(...).
@MightyCupcakes Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree with @chao1995

*/
private void extractArguments(String argsString, List<PrefixPosition> prefixPositions) {
private Map<Prefix, List<String>> extractArguments(String argsString, List<PrefixPosition> prefixPositions) {
Copy link
Contributor

@chao1995 chao1995 Mar 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So as mentioned, instead of return the map, return Arguments directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: extra space before Map

for (int i = 0; i < prefixPositions.size() - 1; i++) {
String argValue = extractArgumentValue(argsString, prefixPositions.get(i), prefixPositions.get(i + 1));
saveArgument(prefixPositions.get(i).getPrefix(), argValue);
saveArgument(tokenizedArguments, prefixPositions.get(i).getPrefix(), argValue);
Copy link
Contributor

@chao1995 chao1995 Mar 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned below, if use getOrDefault, can replace this method with 3 lines of code.

The reason not to do this maybe is for clarity concern, but since now the Map is a local variable, adding an instance method for just saving something to a local map, doesn't feel quite right.

Copy link
Contributor Author

@PierceAndy PierceAndy Mar 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that saveArgument(...) is following SLAP, and extracting it out into the body of the loop makes the code harder to read and manage. Yes, I would agree that extracting the method back into the loop reduces clarity.

Copy link
Contributor

@chao1995 chao1995 Mar 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I didn't use the SLAP term, but I mean it

The reason not to do this maybe is for clarity concern

My reason is that this instance method needs a reference to a local variable (the Map) from another method, I don't feel right doing that. Plus, in my opinion adding a comment is clear enough for this piece of code (just adding a value to a map).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that makes sense. Because you want to avoid mutating something that you're passing by reference.

Copy link
Contributor

@MightyCupcakes MightyCupcakes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No further comments

@PierceAndy PierceAndy force-pushed the 312-ensure-tokenize-before-get branch 3 times, most recently from c43ab13 to a91e196 Compare March 8, 2017 14:23
@PierceAndy PierceAndy closed this Mar 8, 2017
@PierceAndy PierceAndy force-pushed the 312-ensure-tokenize-before-get branch from a91e196 to c3f8027 Compare March 8, 2017 14:27
@PierceAndy PierceAndy reopened this Mar 8, 2017
@PierceAndy PierceAndy force-pushed the 312-ensure-tokenize-before-get branch from c5cb5e5 to 0c63ccf Compare March 8, 2017 14:41
@CanIHasReview-bot
Copy link

v3

@PierceAndy submitted v3 for review.

(📚 Archive) (📈 Interdiff between v2 and v3)

Checkout this PR version locally
git fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/3/head:BRANCHNAME

where BRANCHNAME is the name of the local branch you want to fetch this PR to.

@PierceAndy PierceAndy force-pushed the 312-ensure-tokenize-before-get branch from eaae7e3 to 9c87cb5 Compare March 26, 2017 08:58
@CanIHasReview-bot
Copy link

v22

@PierceAndy submitted v22 for review.

(📚 Archive) (📈 Interdiff between v21 and v22)

Checkout this PR version locally
git fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/22/head:BRANCHNAME

where BRANCHNAME is the name of the local branch you want to fetch this PR to.

@PierceAndy
Copy link
Contributor Author

v22

  • Rebase with master

Copy link
Contributor

@damithc damithc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nits. Otherwise looks good.

*/
public ArgumentMultimap() {
argMultimap = new HashMap<>();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can/should we avoid this constructor by initializing the map in line 19?

*/
public String getPreamble() {
Optional<String> storedPreamble = getValue(new Prefix(""));
return storedPreamble.orElse("");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can merge these two lines?

@PierceAndy PierceAndy force-pushed the 312-ensure-tokenize-before-get branch from 9c87cb5 to 77aa2e6 Compare March 26, 2017 10:52
@CanIHasReview-bot
Copy link

v23

@PierceAndy submitted v23 for review.

(📚 Archive) (📈 Interdiff between v22 and v23)

Checkout this PR version locally
git fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/23/head:BRANCHNAME

where BRANCHNAME is the name of the local branch you want to fetch this PR to.

@PierceAndy
Copy link
Contributor Author

v23

  • Minor improvements to ArgumentMultimap class

@PierceAndy
Copy link
Contributor Author

@MightyCupcakes A couple of minor changes since your last review.

@PierceAndy PierceAndy force-pushed the 312-ensure-tokenize-before-get branch from 77aa2e6 to 31411b9 Compare March 27, 2017 07:20
@CanIHasReview-bot
Copy link

v24

@PierceAndy submitted v24 for review.

(📚 Archive) (📈 Interdiff between v23 and v24)

Checkout this PR version locally
git fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/24/head:BRANCHNAME

where BRANCHNAME is the name of the local branch you want to fetch this PR to.

@PierceAndy
Copy link
Contributor Author

v24

  • Fix minor typo.

Copy link
Contributor

@MightyCupcakes MightyCupcakes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[v24 3/4] Commit organisation.

public ArgumentMultimap() {
argMultimap = new HashMap<>();
}
private final Map<Prefix, List<String>> argMultimap = new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be in commit 2 instead of this commit

ArgumentTokenizer's tokenized argument getters can be accessed even
before ArgumentTokenizer is given arguments to tokenize through
ArgumentTokenizer#tokenize(String).

This behaviour is problematic as the getters can be called before
ArgumentTokenizer#tokenize(...), and would return empty optionals,
lists, and strings that are indistinguishable from normal empty results.

Let's extract the getters into a new ArgumentMultimap class, and have
ArgumentTokenizer#tokenize(...) return an instance of that class so that
these getters cannot be accessed before tokenization.
The Logic class diagram in the developer guide does not reflect the
updated dependencies between ArgumentTokenizer, ArgumentMultimap,
Prefix, and command parser classes.

Let's update the Logic class diagram.
Command parsers need to create a new ArgumentTokenizer object before
they can tokenize an arguments string. During instantiation, the
ArgumentTokenizer constructor is provided prefixes to be later used when
ArgumentTokenizer#tokenize(String) is called.

ArgumentTokenizer doesn't have a need to be instantiated before
tokenizing can be done. The object is not reused to tokenize different
arguments strings with the same set of prefixes provided during
instantiation.

Let's make ArgumentTokenizer#tokenize(...) static, which also makes it
state-independent and thus easier to test.
@PierceAndy PierceAndy force-pushed the 312-ensure-tokenize-before-get branch from 31411b9 to b3baf26 Compare March 27, 2017 09:26
@CanIHasReview-bot
Copy link

v25

@PierceAndy submitted v25 for review.

(📚 Archive) (📈 Interdiff between v24 and v25)

Checkout this PR version locally
git fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/25/head:BRANCHNAME

where BRANCHNAME is the name of the local branch you want to fetch this PR to.

@PierceAndy
Copy link
Contributor Author

v25

  • Move erroneous changes from [v24 3/4] to [v24 2/4]

@chao1995 chao1995 mentioned this pull request Mar 28, 2017
@damithc damithc changed the title ArgumentTokenizer: tokenizing is not ensured before getting values #312 ArgumentTokenizer: ensure tokenizing before getting values #312 Mar 29, 2017
@damithc damithc merged commit 2d93a18 into se-edu:master Mar 29, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ArgumentTokenizer: ensure tokenizing before getting values
6 participants