Avoiding the use of ordinal() in an already-deployed enum
While reviewing a PR, I found an enum being used like this:
class Entity {
public enum STATUS { NEW, SENT, FAILED, OK }
@Column(name = "STATUS") // a NUMBER(1) column in DB
private int status; // + get/set
//...
}
// Inside methods...
Entity e = new Entity();
e.setStatus(STATUS.SENT.ordinal());
EntityDAO.save(e);
Entity e = EntityDAO.findByStatus(STATUS.FAILED.ordinal());
logger.info("Found entity with status: " + STATUS.values()[e.getStatus()]);
Assume the necessary imports etc.
I find this use of ordinal()
to be an antipattern, and consider that it should be avoided.
Instead, an inner field with the equivalent DB representation (e.g. NEW(1)
or NEW("N")
) should be used, in order to decouple the semantically meaningful representation of the enum value in the code (NEW
) from the DB-optimized representation of such value when persisted (1
or 'N'
or whatever).
Now, I don't have this POV for nothing: we've already had bugs stemming from altering the order of enum elements that were being used based on its ordinal()
value (some times because of adding new values in between, some times because someone decided the values looked better in alphabetical order ¯_(ツ)_/¯).
That's the case I made in the PR comments -- but in the end the PR got approved anyway, reasoning that my proposed correction would add too much clutter to the enum for little gain, and that we should all just be careful with enums.
And so the enum made its way to production.
Time has passed, and now I've got the chance to change that enum so its usage is not tied to the ordering of its elements.
However, since this has been in production for quite some time, I have to make it so the current DB values (0, 1, 2...) are kept and still interpreted correctly (as changing the DB values or column type would be too costly now).
This is the first version I came up with:
public enum STATUS {
NEW (0),
SENT (1),
FAILED (2),
OK (3),
;
private final int val;
STATUS (int val) { this.val = val; }
public int val() { return val; }
public static STATUS get(final int val) {
switch (val) {
case 0: return NEW;
case 1: return SENT;
case 2: return FAILED;
case 3: return OK;
default: throw new IllegalArgumentException();
}
}
}
I think this is as concise as it can get.
However, if a dev wanted to add a new enum value, they'd have to modify both the enum list and the switch
inside the get(int)
method. I find this to be inconvenient and, in a way, ammunition for my peers in favor of just using ordinal()
.
So I came up with this second version:
public static enum STATUS {
NEW (0),
SENT (1),
FAILED (2),
OK (3),
;
private static final STATUS values;
static { // like caching values() but sorted by dbValue
STATUS st = values();
values = new STATUS[st.length];
for (STATUS s : st) {
values[s.dbValue] = s;
}
}
public static STATUS fromDB(final int dbValue) {
return values[dbValue];
}
private final int dbValue;
public int toDBValue() {
return dbValue;
}
STATUS(int dbValue) {
this.dbValue = dbValue;
}
}
In this version, adding a new enum element is as simple as adding it to the list, just like a normal enum. And getting an element from its DB value is O(1) and rather efficient (compared to, say, using Streams to filter values()
by dbValue
every time). But maybe it is too complex for an enum? Also, this relies on DB values being consecutive numbers, as adding e.g. CANCELLED(99)
would throw an AIOOB exception upon execution.
So my question is: how can I enhance this enum so adding elements to it is as simple as possible, getting elements by value is as efficient as possible, and it doesn't depend on ordinal()
?
java enum
New contributor
add a comment |
While reviewing a PR, I found an enum being used like this:
class Entity {
public enum STATUS { NEW, SENT, FAILED, OK }
@Column(name = "STATUS") // a NUMBER(1) column in DB
private int status; // + get/set
//...
}
// Inside methods...
Entity e = new Entity();
e.setStatus(STATUS.SENT.ordinal());
EntityDAO.save(e);
Entity e = EntityDAO.findByStatus(STATUS.FAILED.ordinal());
logger.info("Found entity with status: " + STATUS.values()[e.getStatus()]);
Assume the necessary imports etc.
I find this use of ordinal()
to be an antipattern, and consider that it should be avoided.
Instead, an inner field with the equivalent DB representation (e.g. NEW(1)
or NEW("N")
) should be used, in order to decouple the semantically meaningful representation of the enum value in the code (NEW
) from the DB-optimized representation of such value when persisted (1
or 'N'
or whatever).
Now, I don't have this POV for nothing: we've already had bugs stemming from altering the order of enum elements that were being used based on its ordinal()
value (some times because of adding new values in between, some times because someone decided the values looked better in alphabetical order ¯_(ツ)_/¯).
That's the case I made in the PR comments -- but in the end the PR got approved anyway, reasoning that my proposed correction would add too much clutter to the enum for little gain, and that we should all just be careful with enums.
And so the enum made its way to production.
Time has passed, and now I've got the chance to change that enum so its usage is not tied to the ordering of its elements.
However, since this has been in production for quite some time, I have to make it so the current DB values (0, 1, 2...) are kept and still interpreted correctly (as changing the DB values or column type would be too costly now).
This is the first version I came up with:
public enum STATUS {
NEW (0),
SENT (1),
FAILED (2),
OK (3),
;
private final int val;
STATUS (int val) { this.val = val; }
public int val() { return val; }
public static STATUS get(final int val) {
switch (val) {
case 0: return NEW;
case 1: return SENT;
case 2: return FAILED;
case 3: return OK;
default: throw new IllegalArgumentException();
}
}
}
I think this is as concise as it can get.
However, if a dev wanted to add a new enum value, they'd have to modify both the enum list and the switch
inside the get(int)
method. I find this to be inconvenient and, in a way, ammunition for my peers in favor of just using ordinal()
.
So I came up with this second version:
public static enum STATUS {
NEW (0),
SENT (1),
FAILED (2),
OK (3),
;
private static final STATUS values;
static { // like caching values() but sorted by dbValue
STATUS st = values();
values = new STATUS[st.length];
for (STATUS s : st) {
values[s.dbValue] = s;
}
}
public static STATUS fromDB(final int dbValue) {
return values[dbValue];
}
private final int dbValue;
public int toDBValue() {
return dbValue;
}
STATUS(int dbValue) {
this.dbValue = dbValue;
}
}
In this version, adding a new enum element is as simple as adding it to the list, just like a normal enum. And getting an element from its DB value is O(1) and rather efficient (compared to, say, using Streams to filter values()
by dbValue
every time). But maybe it is too complex for an enum? Also, this relies on DB values being consecutive numbers, as adding e.g. CANCELLED(99)
would throw an AIOOB exception upon execution.
So my question is: how can I enhance this enum so adding elements to it is as simple as possible, getting elements by value is as efficient as possible, and it doesn't depend on ordinal()
?
java enum
New contributor
The question is at +1/-1 right now. I don't mind the downvotes but, if anybody feels there's something I could improve, I'd appreciate a comment pointing it out.
– Luis G.
Dec 19 at 16:48
add a comment |
While reviewing a PR, I found an enum being used like this:
class Entity {
public enum STATUS { NEW, SENT, FAILED, OK }
@Column(name = "STATUS") // a NUMBER(1) column in DB
private int status; // + get/set
//...
}
// Inside methods...
Entity e = new Entity();
e.setStatus(STATUS.SENT.ordinal());
EntityDAO.save(e);
Entity e = EntityDAO.findByStatus(STATUS.FAILED.ordinal());
logger.info("Found entity with status: " + STATUS.values()[e.getStatus()]);
Assume the necessary imports etc.
I find this use of ordinal()
to be an antipattern, and consider that it should be avoided.
Instead, an inner field with the equivalent DB representation (e.g. NEW(1)
or NEW("N")
) should be used, in order to decouple the semantically meaningful representation of the enum value in the code (NEW
) from the DB-optimized representation of such value when persisted (1
or 'N'
or whatever).
Now, I don't have this POV for nothing: we've already had bugs stemming from altering the order of enum elements that were being used based on its ordinal()
value (some times because of adding new values in between, some times because someone decided the values looked better in alphabetical order ¯_(ツ)_/¯).
That's the case I made in the PR comments -- but in the end the PR got approved anyway, reasoning that my proposed correction would add too much clutter to the enum for little gain, and that we should all just be careful with enums.
And so the enum made its way to production.
Time has passed, and now I've got the chance to change that enum so its usage is not tied to the ordering of its elements.
However, since this has been in production for quite some time, I have to make it so the current DB values (0, 1, 2...) are kept and still interpreted correctly (as changing the DB values or column type would be too costly now).
This is the first version I came up with:
public enum STATUS {
NEW (0),
SENT (1),
FAILED (2),
OK (3),
;
private final int val;
STATUS (int val) { this.val = val; }
public int val() { return val; }
public static STATUS get(final int val) {
switch (val) {
case 0: return NEW;
case 1: return SENT;
case 2: return FAILED;
case 3: return OK;
default: throw new IllegalArgumentException();
}
}
}
I think this is as concise as it can get.
However, if a dev wanted to add a new enum value, they'd have to modify both the enum list and the switch
inside the get(int)
method. I find this to be inconvenient and, in a way, ammunition for my peers in favor of just using ordinal()
.
So I came up with this second version:
public static enum STATUS {
NEW (0),
SENT (1),
FAILED (2),
OK (3),
;
private static final STATUS values;
static { // like caching values() but sorted by dbValue
STATUS st = values();
values = new STATUS[st.length];
for (STATUS s : st) {
values[s.dbValue] = s;
}
}
public static STATUS fromDB(final int dbValue) {
return values[dbValue];
}
private final int dbValue;
public int toDBValue() {
return dbValue;
}
STATUS(int dbValue) {
this.dbValue = dbValue;
}
}
In this version, adding a new enum element is as simple as adding it to the list, just like a normal enum. And getting an element from its DB value is O(1) and rather efficient (compared to, say, using Streams to filter values()
by dbValue
every time). But maybe it is too complex for an enum? Also, this relies on DB values being consecutive numbers, as adding e.g. CANCELLED(99)
would throw an AIOOB exception upon execution.
So my question is: how can I enhance this enum so adding elements to it is as simple as possible, getting elements by value is as efficient as possible, and it doesn't depend on ordinal()
?
java enum
New contributor
While reviewing a PR, I found an enum being used like this:
class Entity {
public enum STATUS { NEW, SENT, FAILED, OK }
@Column(name = "STATUS") // a NUMBER(1) column in DB
private int status; // + get/set
//...
}
// Inside methods...
Entity e = new Entity();
e.setStatus(STATUS.SENT.ordinal());
EntityDAO.save(e);
Entity e = EntityDAO.findByStatus(STATUS.FAILED.ordinal());
logger.info("Found entity with status: " + STATUS.values()[e.getStatus()]);
Assume the necessary imports etc.
I find this use of ordinal()
to be an antipattern, and consider that it should be avoided.
Instead, an inner field with the equivalent DB representation (e.g. NEW(1)
or NEW("N")
) should be used, in order to decouple the semantically meaningful representation of the enum value in the code (NEW
) from the DB-optimized representation of such value when persisted (1
or 'N'
or whatever).
Now, I don't have this POV for nothing: we've already had bugs stemming from altering the order of enum elements that were being used based on its ordinal()
value (some times because of adding new values in between, some times because someone decided the values looked better in alphabetical order ¯_(ツ)_/¯).
That's the case I made in the PR comments -- but in the end the PR got approved anyway, reasoning that my proposed correction would add too much clutter to the enum for little gain, and that we should all just be careful with enums.
And so the enum made its way to production.
Time has passed, and now I've got the chance to change that enum so its usage is not tied to the ordering of its elements.
However, since this has been in production for quite some time, I have to make it so the current DB values (0, 1, 2...) are kept and still interpreted correctly (as changing the DB values or column type would be too costly now).
This is the first version I came up with:
public enum STATUS {
NEW (0),
SENT (1),
FAILED (2),
OK (3),
;
private final int val;
STATUS (int val) { this.val = val; }
public int val() { return val; }
public static STATUS get(final int val) {
switch (val) {
case 0: return NEW;
case 1: return SENT;
case 2: return FAILED;
case 3: return OK;
default: throw new IllegalArgumentException();
}
}
}
I think this is as concise as it can get.
However, if a dev wanted to add a new enum value, they'd have to modify both the enum list and the switch
inside the get(int)
method. I find this to be inconvenient and, in a way, ammunition for my peers in favor of just using ordinal()
.
So I came up with this second version:
public static enum STATUS {
NEW (0),
SENT (1),
FAILED (2),
OK (3),
;
private static final STATUS values;
static { // like caching values() but sorted by dbValue
STATUS st = values();
values = new STATUS[st.length];
for (STATUS s : st) {
values[s.dbValue] = s;
}
}
public static STATUS fromDB(final int dbValue) {
return values[dbValue];
}
private final int dbValue;
public int toDBValue() {
return dbValue;
}
STATUS(int dbValue) {
this.dbValue = dbValue;
}
}
In this version, adding a new enum element is as simple as adding it to the list, just like a normal enum. And getting an element from its DB value is O(1) and rather efficient (compared to, say, using Streams to filter values()
by dbValue
every time). But maybe it is too complex for an enum? Also, this relies on DB values being consecutive numbers, as adding e.g. CANCELLED(99)
would throw an AIOOB exception upon execution.
So my question is: how can I enhance this enum so adding elements to it is as simple as possible, getting elements by value is as efficient as possible, and it doesn't depend on ordinal()
?
java enum
java enum
New contributor
New contributor
New contributor
asked Dec 19 at 11:52
Luis G.
1093
1093
New contributor
New contributor
The question is at +1/-1 right now. I don't mind the downvotes but, if anybody feels there's something I could improve, I'd appreciate a comment pointing it out.
– Luis G.
Dec 19 at 16:48
add a comment |
The question is at +1/-1 right now. I don't mind the downvotes but, if anybody feels there's something I could improve, I'd appreciate a comment pointing it out.
– Luis G.
Dec 19 at 16:48
The question is at +1/-1 right now. I don't mind the downvotes but, if anybody feels there's something I could improve, I'd appreciate a comment pointing it out.
– Luis G.
Dec 19 at 16:48
The question is at +1/-1 right now. I don't mind the downvotes but, if anybody feels there's something I could improve, I'd appreciate a comment pointing it out.
– Luis G.
Dec 19 at 16:48
add a comment |
1 Answer
1
active
oldest
votes
A negative parameter value or a parameter value >= values.length
passed to fromDB()
will throw an IndexOutOfBoundsException, which is less meaningful than the original IllegalArgumentException since the IndexOutOfBoundsException is complaining about an implementation detail rather than about bad input.
To avoid the IndexOutOfBoundsException, use a Map<Integer, STATUS>
instead of an array.
However, if there are only a few defined values as in the question, a Map is probably not needed either. fromDB()
can simply iterate over values() and return the match when found, otherwise throw IllegalArgumentException. Add the Map when performance profiling indicates fromDB()
is a bottleneck.
Also, consider changing STATUS
to Status
, to follow standard naming conventions.
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Luis G. is a new contributor. Be nice, and check out our Code of Conduct.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f209972%2favoiding-the-use-of-ordinal-in-an-already-deployed-enum%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
A negative parameter value or a parameter value >= values.length
passed to fromDB()
will throw an IndexOutOfBoundsException, which is less meaningful than the original IllegalArgumentException since the IndexOutOfBoundsException is complaining about an implementation detail rather than about bad input.
To avoid the IndexOutOfBoundsException, use a Map<Integer, STATUS>
instead of an array.
However, if there are only a few defined values as in the question, a Map is probably not needed either. fromDB()
can simply iterate over values() and return the match when found, otherwise throw IllegalArgumentException. Add the Map when performance profiling indicates fromDB()
is a bottleneck.
Also, consider changing STATUS
to Status
, to follow standard naming conventions.
add a comment |
A negative parameter value or a parameter value >= values.length
passed to fromDB()
will throw an IndexOutOfBoundsException, which is less meaningful than the original IllegalArgumentException since the IndexOutOfBoundsException is complaining about an implementation detail rather than about bad input.
To avoid the IndexOutOfBoundsException, use a Map<Integer, STATUS>
instead of an array.
However, if there are only a few defined values as in the question, a Map is probably not needed either. fromDB()
can simply iterate over values() and return the match when found, otherwise throw IllegalArgumentException. Add the Map when performance profiling indicates fromDB()
is a bottleneck.
Also, consider changing STATUS
to Status
, to follow standard naming conventions.
add a comment |
A negative parameter value or a parameter value >= values.length
passed to fromDB()
will throw an IndexOutOfBoundsException, which is less meaningful than the original IllegalArgumentException since the IndexOutOfBoundsException is complaining about an implementation detail rather than about bad input.
To avoid the IndexOutOfBoundsException, use a Map<Integer, STATUS>
instead of an array.
However, if there are only a few defined values as in the question, a Map is probably not needed either. fromDB()
can simply iterate over values() and return the match when found, otherwise throw IllegalArgumentException. Add the Map when performance profiling indicates fromDB()
is a bottleneck.
Also, consider changing STATUS
to Status
, to follow standard naming conventions.
A negative parameter value or a parameter value >= values.length
passed to fromDB()
will throw an IndexOutOfBoundsException, which is less meaningful than the original IllegalArgumentException since the IndexOutOfBoundsException is complaining about an implementation detail rather than about bad input.
To avoid the IndexOutOfBoundsException, use a Map<Integer, STATUS>
instead of an array.
However, if there are only a few defined values as in the question, a Map is probably not needed either. fromDB()
can simply iterate over values() and return the match when found, otherwise throw IllegalArgumentException. Add the Map when performance profiling indicates fromDB()
is a bottleneck.
Also, consider changing STATUS
to Status
, to follow standard naming conventions.
answered Dec 19 at 19:25
Andrew S
1311
1311
add a comment |
add a comment |
Luis G. is a new contributor. Be nice, and check out our Code of Conduct.
Luis G. is a new contributor. Be nice, and check out our Code of Conduct.
Luis G. is a new contributor. Be nice, and check out our Code of Conduct.
Luis G. is a new contributor. Be nice, and check out our Code of Conduct.
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f209972%2favoiding-the-use-of-ordinal-in-an-already-deployed-enum%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
The question is at +1/-1 right now. I don't mind the downvotes but, if anybody feels there's something I could improve, I'd appreciate a comment pointing it out.
– Luis G.
Dec 19 at 16:48