-
Notifications
You must be signed in to change notification settings - Fork 1.7k
ArgumentTokenizer: ensure tokenizing before getting values #312 #320
ArgumentTokenizer: ensure tokenizing before getting values #312 #320
Conversation
@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. |
v1@PierceAndy submitted v1 for review.
Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/1/head:BRANCHNAME where |
There was a problem hiding this 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.
@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.
|
@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. |
@damithc Right, that would be a simple solution while reducing constructor complexity. |
9a53ec0
to
8e39586
Compare
v2@PierceAndy submitted v2 for review. (📚 Archive) (📈 Interdiff between v1 and v2) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/2/head:BRANCHNAME where |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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(...)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No further comments
c43ab13
to
a91e196
Compare
a91e196
to
c3f8027
Compare
c5cb5e5
to
0c63ccf
Compare
v3@PierceAndy submitted v3 for review. (📚 Archive) (📈 Interdiff between v2 and v3) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/3/head:BRANCHNAME where |
eaae7e3
to
9c87cb5
Compare
v22@PierceAndy submitted v22 for review.
(📚 Archive) (📈 Interdiff between v21 and v22) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/22/head:BRANCHNAME where |
v22
|
There was a problem hiding this 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<>(); | ||
} |
There was a problem hiding this comment.
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(""); |
There was a problem hiding this comment.
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?
9c87cb5
to
77aa2e6
Compare
v23@PierceAndy submitted v23 for review.
(📚 Archive) (📈 Interdiff between v22 and v23) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/23/head:BRANCHNAME where |
v23
|
@MightyCupcakes A couple of minor changes since your last review. |
77aa2e6
to
31411b9
Compare
v24@PierceAndy submitted v24 for review.
(📚 Archive) (📈 Interdiff between v23 and v24) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/24/head:BRANCHNAME where |
v24
|
There was a problem hiding this 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<>(); |
There was a problem hiding this comment.
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.
31411b9
to
b3baf26
Compare
v25@PierceAndy submitted v25 for review.
(📚 Archive) (📈 Interdiff between v24 and v25) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/320/25/head:BRANCHNAME where |
v25
|
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)
, andgetPreamble()
, don't give a warning when they are called beforeArgumentTokenizer#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: