alex
alex

Reputation: 166

define error codes in java with inheritance

I want to model some error codes. The classic enum approach

public enum FileError implement FormattedError {
    _10 ("some error with parameters [{0}] and [{1}]"),
    _20 ("some other error");

    private final String description;

    private Error(String description) {
        this.description = description;
    }

    public String getDescription(Object... parameters) {
        return // logic to format message 
    }

    ...
}

it is not good for me because I have many modules, each with it's error codes and I don't want to copy and paste the boilerplate (constructors, getters, logic..) in all these enums.

So I went for a "manual" enum implemented like this

public class FileError extends BaseError {

    public final static FileError _10  = new FileError (10, "some message with parameters [{0}] and [{1}]");
    public final static FileError _20  = new FileError (20, "some other message");

}

where I can define my logic in BaseError and reuse it.

but it is still bad because there is no way to link the variable name to the number (_10 to 10) and people copy pasting might reuse the same number without noticing. I could add a test to check that via reflection but then how do I enforce people to use that test for their implementations.

so do you guys have a better idea about how I could achieve this ?

[edit] please keep in mind that I don't want to put error codes in properties files because I want the ide to link error codes in the code with their message.

Upvotes: 3

Views: 1449

Answers (4)

pavle
pavle

Reputation: 939

Hope you'll get some idea with this:

public enum FileError {
    SOME_ERROR1("0", "Error something1"),
    SOME_ERROR2("1", "Error something2"),
    SOME_ERROR3("2", "Error something3"),

    private final String code;
    private final String message;

    FileError(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String get() {
        return new CustomException(code, message).toString();
    }
}

And you're CustomException class

public class CustomException {

    ...

    @Override
    public String toString() {
        return String.format(Locale.getDefault(), "%s, %s", code, message);
    }
}

Upvotes: 0

OldCurmudgeon
OldCurmudgeon

Reputation: 65841

It is necessary to use some boilerplate code but you can keep it at a minimum by making the enum implement an interface and put much of the functionality statically in the interface - assuming you are using Java-7+ of course.

interface Error {

    /**
     * Keeps track of error ranges - for sanity check when errors are registered.
     */
    static final Map<ErrorRange, Set<? extends Error>> errors = new HashMap<>();
    /**
     * Lookup range.
     */
    static final Map<Error, ErrorRange> range = new HashMap<>();

    public static <E extends Enum<E> & Error> void register(ErrorRange errorRange, Class<E> theClass) {
        // Keep track of all errors - TODO - Make sure each is registered only once.
        errors.put(errorRange, EnumSet.allOf(theClass));
        // We need the range.
        for (Error e : theClass.getEnumConstants()) {
            range.put(e, errorRange);
        }
    }

    /**
     * Get a formatted string for the error with the provided parameters.
     */
    static <E extends Enum<E> & Error> String format(E error, Object... parameters) {
        // The error number comes from it's range + its ordinal.
        int errNo = range.get(error).range + error.ordinal();
        // The string comes from the formatted description.
        return errNo + "\t" + String.format(error.getDescription(), parameters);
    }

    // All Errors must have a description.
    public String getDescription();

}

/**
 * Register of all error ranges.
 */
enum ErrorRange {

    // All File errors start at 10,000
    FileError(10_000);

    final int range;

    private ErrorRange(int range) {
        this.range = range;
    }

}

public enum FileError implements Error {

    ParameterError("some error with parameters [{0}] and [{1}]"),
    OtherError("some other error");

    //<editor-fold defaultstate="collapsed" desc="Description">
    // Start boilerplate
    private final String description;

    private FileError(String description) {
        this.description = description;
    }

    @Override
    public String getDescription() {
        return description;
    }
    // End boilerplate
    //</editor-fold>

}

static {
    // Statically register me with the Error object.
    Error.register(ErrorRange.FileError, FileError.class);
}

Upvotes: 0

rinde
rinde

Reputation: 1263

A combination of both your approaches may be what you are looking for:

enum ErrorCode {
  _10(new FileError(10, "some message with parameters [{0}] and [{1}]")),
  _20(new FileError(20, "some other message"));

  private final FileError error;

  private ErrorCode(FileError err) {
    error = err;
  }

  public FileError getError() {
    return error;
  }
}

With this code there is an explicit link between the error code and the variable. To avoid other people using the same error code you may prevent them from creating their own FileError instances entirely by making the constructor package private. If that is not an option, you may create an additional subclass as follows:

public class UserDefinedFileError extends FileError {
  public UserDefinedFileError(int code, String msg){
    super(checkCode(code),msg);
  }

  static int checkCode(int code){
    if(code <= 100){ // or check if it exists in a set of used codes
      throw new IllegalArgumentException("Error codes lower than 100 are reserved.");
    }
  }
}

Upvotes: 0

Lionel Parreaux
Lionel Parreaux

Reputation: 1225

To answer your question of how to check for reused numbers, you can do that simply by using a static set of all numbers registered so far, and check that when a new one is registered it does not yet exist:

public class BaseError {
    // ...

    private static Set<Integer> registeredNums = new HashSet<>();

    public BaseError(int N, String msg) {
        synchronized(registeredNums) {
            assert(!registeredNums.contains(N)) : "Duplicated error code";
            registeredNums.add(N);
        }

        // ...
    }
}

The users will need to have assertions enabled. If you want the check to always happen, you could throw an AssertionError manually.

Upvotes: 4

Related Questions