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

model: convert checked exceptions to runtime exceptions #896

Merged
merged 11 commits into from
Aug 9, 2018

Conversation

pyokagan
Copy link
Contributor

@pyokagan pyokagan commented Aug 7, 2018

DuplicatePersonException and PersonNotFoundException are checked
exceptions. They are thrown by methods to signify that their relevant
preconditions were violated.

Using checked exceptions for such a purpose is slightly useful in that
it will force callers to handle the case where the above preconditions
are not met, such as when the methods are called with invalid user
input.

However, it imposes a HUGE cost on callers where the preconditions are
already known to be met (e.g. in test code, or when the user input has
already been validated before hand). In such a case, callers are forced
to add try-catch blocks around the method call even if they know that
the exception will never be thrown, bloating up the code. It is also
impossible to test the catch blocks as well since correct code will
ensure that the precondition holds and thus the exception will never be
thrown, leading to reduced code coverage.

Checked exceptions also don't work very well with the Java Streams API,
since the API doesn't accept lambdas which could throw checked
exceptions.

In AB-4, the amount of code which benefits from DuplicatePersonException
and PersonNotFoundException being checked exceptions is much smaller
than the amount of code which is negatively impacted.

As such, let's make the tradeoff in the other direction, by making
DuplicatePersonException and PersonNotFoundException runtime exceptions.
New callers _could_ forget to check that the preconditions hold before
calling the methods in question (although test cases should catch that),
but this is balanced out by the huge benefit of having more concise and
testable code.

@CanIHasReview-bot
Copy link

Click here to submit a new iteration when this PR is ready for review.

See this repository's contribution guide for more information.

@pyokagan pyokagan force-pushed the no-checked-exceptions branch 3 times, most recently from 42def33 to c2e22a1 Compare August 8, 2018 08:33
@se-edu se-edu deleted a comment Aug 8, 2018
@pyokagan pyokagan force-pushed the no-checked-exceptions branch 3 times, most recently from 5939b6c to 1185ff5 Compare August 8, 2018 10:27
@CanIHasReview-bot
Copy link

v1

@pyokagan submitted v1 for review.

(📚 Archive)

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

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

@pyokagan pyokagan requested a review from a team August 8, 2018 10:35
Copy link
Member

@eugenepeh eugenepeh left a comment

Choose a reason for hiding this comment

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

[10/10] Since UniquePersonList is in the package seedu.address.model.person;, UniquePersonListTest should be correspondingly the same.

@@ -110,13 +110,12 @@ public ReadOnlyAddressBook getAddressBook() {
}

@Override
public void deletePerson(Person target) throws PersonNotFoundException {
Copy link
Contributor

Choose a reason for hiding this comment

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

[1/10] PersonNotFoundException import not removed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I'll remember to run tests on every commit next time ;_;

Copy link
Contributor

@yamidark yamidark left a comment

Choose a reason for hiding this comment

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

[5/10]
nit in last paragraph:
As such, let's making make the tradeoff in the other direction, by making
DuplicatePersonException a RuntimeException. New callers could forget to...

Copy link
Contributor

@yamidark yamidark left a comment

Choose a reason for hiding this comment

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

[6/10]
same as [5/10]

Copy link
Contributor

@Zhiyuan-Amos Zhiyuan-Amos left a comment

Choose a reason for hiding this comment

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

[3/10]

@@ -181,6 +182,11 @@ public ReadOnlyAddressBook getAddressBook() {
private class ModelStubAcceptingPersonAdded extends ModelStub {
final ArrayList<Person> personsAdded = new ArrayList<>();

@Override
public boolean hasPerson(Person person) {
return personsAdded.stream().anyMatch(addedPerson -> addedPerson.isSamePerson(person));
Copy link
Contributor

Choose a reason for hiding this comment

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

lambda expression can be simplified to : person::isSamePerson

Copy link
Contributor

@Zhiyuan-Amos Zhiyuan-Amos left a comment

Choose a reason for hiding this comment

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

[10/10] Since we are on Java 9, should we use List.of(...) instead of Arrays.asList(...)?

@Test
public void setPerson_editedPersonIsSamePerson_success() {
uniquePersonList.add(ALICE);
uniquePersonList.setPerson(ALICE, ALICE);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we do uniquePersonList.setPerson(ALICE, new PersonBuilder(ALICE).build()); instead?
It's unclear here why we are using the same object references.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If they refer to the same object (for whatever reason), the API should still allow it, right?

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, after some more thought, I think I'll keep it. The "different object reference case" is already tested in setPerson_editedPersonHasSameIdentity_success

@pyokagan
Copy link
Contributor Author

pyokagan commented Aug 8, 2018

@yamidark Sorry, please do concentrate on se-edu/addressbook-level2#388 , I think @damithc would prefer that. Sorry for requesting your review and taking your time, this PR is not so important.

Copy link
Contributor

@Zhiyuan-Amos Zhiyuan-Amos left a comment

Choose a reason for hiding this comment

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

[5/10]

throw new AssertionError("sample data cannot contain duplicate persons", e);
AddressBook sampleAb = new AddressBook();
for (Person samplePerson : getSamplePersons()) {
sampleAb.addPerson(samplePerson);
Copy link
Contributor

Choose a reason for hiding this comment

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

will this PR be simplifying some of the loops such as this one?
Arrays.stream(getSamplePersons()).forEach(sampleAb::addPerson);

Since you mentioned in your commit message that:

Checked exceptions also don't work very well with the Java Streams API,
since the API doesn't accept lambas which could throw checked
exceptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Eh let's keep that for another PR, to avoid delaying this one.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok! Oh I spotted a spelling mistake for your commit msg: lambas :p

@pyokagan
Copy link
Contributor Author

pyokagan commented Aug 9, 2018

@eugenepeh

[10/10] Since UniquePersonList is in the package seedu.address.model.person;, UniquePersonListTest should be correspondingly the same.

fyi, this is not a new file, but I'll do it anyway in this PR since it's easy enough to do.

These redundant throws clauses were identified using IntelliJ's
Analyze->Inspect Code feature, and analyzing the whole project.
In the next few commits, we are going to be converting
DuplicatePersonException and PersonNotFoundException into runtime
exceptions.

To do that, we need to be able to query the model to determine if a
person exists in the address book.

This functionality is already available in
`UniquePersonList#contains(Person)`, but was simply not exposed. Let's
expose it.
In the next few commits, we will convert DuplicatePersonException into a
runtime exception.

In preparation for that, let's teach AddCommand to not depend on
catching DuplicatePersonException for its control flow.

Since DuplicatePersonException is still a checked exception, we need to
add an ugly `throw new AssertionError("should not happen")`, but this
will be removed in later commits once we convert
DuplicatePersonException into a runtime exception.
In the next few commits, we will convert DuplicatePersonException into a
runtime exception.

In preparation for that, let's teach EditCommand to not depend on
catching DuplicatePersonException for its control flow.

Since DuplicatePersonException is still a checked exception, we need to
add an ugly `throw new AssertionError("should not happen")`, but this
will be removed in later commits once we convert
DuplicatePersonException into a runtime exception.
DuplicatePersonException is a checked exception. It is thrown by methods
such as `UniquePersonList#add(Person)`,
`UniquePersonList#setPerson(Person, Person)` etc. to signify that
certain preconditions were not met -- namely that the operation would
cause the "no duplicate persons in address book/UniquePersonList"
constraint to be violated.

Using checked exceptions for such a purpose is slightly useful in that
it will force callers to handle the case where the above preconditions
are not met, such as when the methods are called with invalid user
input.

However, it imposes a HUGE cost on callers where the preconditions are
already known to be met (e.g. in test code, or when the user input has
already been validated before hand). In such a case, callers are forced
to add try-catch blocks around the method call even if they know that
the exception will never be thrown, bloating up the code. It is also
impossible to test the catch blocks as well since correct code will
ensure that the precondition holds and thus the exception will never be
thrown, leading to reduced code coverage.

Checked exceptions also don't work very well with the Java Streams API,
since the API doesn't accept lambdas which could throw checked
exceptions.

In AB-4, the amount of code which benefits from
DuplicatePersonException being a checked exception is much smaller than
the amount of code which is negatively impacted.

As such, let's make the tradeoff in the other direction, by making
DuplicatePersonException a RuntimeException. New callers _could_ forget
to check that the preconditions hold before calling the methods in
question (although test cases should catch that), but this is balanced
out by the huge benefit of having more concise and testable code.
PersonNotFoundException is a checked exception. It is thrown by methods
such as `UniquePersonList#remove(Person)`,
`UniquePersonList#setPerson(Person, Person)` etc. to signify that
certain preconditions were not met -- namely that the person provided to
those methods does not exist in the address book/UniquePersonList.

Using checked exceptions for such a purpose is slightly useful in that
it will force callers to handle the case where the above preconditions
are not met, such as when the methods are called with invalid user
input.

However, it imposes a HUGE cost on callers where the preconditions are
already known to be met (e.g. in test code, or when the user input has
already been validated before hand). In such a case, callers are forced
to add try-catch blocks around the method call even if they know that
the exception will never be thrown, bloating up the code. It is also
impossible to test the catch blocks as well since correct code will
ensure that the precondition holds and thus the exception will never be
thrown, leading to reduced code coverage.

Checked exceptions also don't work very well with the Java Streams API,
since the API doesn't accept lambdas which could throw checked
exceptions.

In AB-4, the amount of code which benefits from PersonNotFoundException
being a checked exception is much smaller than the amount of code which
is negatively impacted.

As such, let's make the tradeoff in the other direction, by making
PersonNotFoundException a RuntimeException. New callers _could_ forget
to check that the preconditions hold before calling the methods in
question (although test cases should catch that), but this is balanced
out by the huge benefit of having more concise and testable code.
The code in the `try` block does not throw any checked exceptions.
Some methods do not explicitly check that their arguments are non-null
with `requireNonNull(...)`.

Let's fix them to be consistent with the rest of the code base.
Since UniquePersonList is in the `seedu.address.model.person` package,
its corresponding test UniquePersonListTest should be in the same
package as well.
@pyokagan
Copy link
Contributor Author

pyokagan commented Aug 9, 2018

@Zhiyuan-Amos

Since we are on Java 9, should we use List.of(...) instead of Arrays.asList(...)?

I considered that, but we don't use List.of(...) at all in our code base, and this PR may not be a good time to "break new ground" in that sense. Putting aside immutability, Arrays.asList(...) is the same as List.of(...) and not that many more characters to type.

I'll wait for #871

@pyokagan
Copy link
Contributor Author

pyokagan commented Aug 9, 2018

[3/11] Also added some requireNonNull()s, missed that out in v1.

Copy link
Contributor

@Zhiyuan-Amos Zhiyuan-Amos left a comment

Choose a reason for hiding this comment

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

Just a minor nit 👍

@Test
public void setPersons_list_replacesOwnListWithProvidedList() {
uniquePersonList.add(ALICE);
List<Person> personList = Arrays.asList(BOB);
Copy link
Contributor

Choose a reason for hiding this comment

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

IntelliJ would prefer you to use Collections.singletonList() here. :p

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Foiled again.

Does it still prefer Collections.singletonList() over List.of(), I wonder.

More test coverage is always nice.

In particular, we want to check that UniquePersonList's methods throw
DuplicatePersonException and PersonNotFoundException where relevant.
These cases were not tested by existing test code.
@pyokagan pyokagan removed the request for review from vivekscl August 9, 2018 09:57
@pyokagan pyokagan merged commit db126b9 into se-edu:master Aug 9, 2018
@pyokagan pyokagan deleted the no-checked-exceptions branch December 4, 2018 18:00
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.

6 participants