Avoiding the use of ordinal() in an already-deployed enum












1














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()?










share|improve this question







New contributor




Luis G. is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.




















  • 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
















1














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()?










share|improve this question







New contributor




Luis G. is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.




















  • 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














1












1








1







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()?










share|improve this question







New contributor




Luis G. is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.











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






share|improve this question







New contributor




Luis G. is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.











share|improve this question







New contributor




Luis G. is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.









share|improve this question




share|improve this question






New contributor




Luis G. is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.









asked Dec 19 at 11:52









Luis G.

1093




1093




New contributor




Luis G. is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.





New contributor





Luis G. is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.






Luis G. is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.












  • 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




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










1 Answer
1






active

oldest

votes


















3














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.






share|improve this answer





















    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.










    draft saved

    draft discarded


















    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









    3














    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.






    share|improve this answer


























      3














      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.






      share|improve this answer
























        3












        3








        3






        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.






        share|improve this answer












        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.







        share|improve this answer












        share|improve this answer



        share|improve this answer










        answered Dec 19 at 19:25









        Andrew S

        1311




        1311






















            Luis G. is a new contributor. Be nice, and check out our Code of Conduct.










            draft saved

            draft discarded


















            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.




            draft saved


            draft discarded














            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





















































            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







            Popular posts from this blog

            Список кардиналов, возведённых папой римским Каликстом III

            Deduzione

            Mysql.sock missing - “Can't connect to local MySQL server through socket”