2
\$\begingroup\$

Based on this question and this answer, I decided to try the enum approach to do some more conversions I need in my application. This time I required to convert between the different field naming conventions I use in my application:

  1. SQL naming convention (used for my field names in SQL DBs): field_name.
  2. Java naming convention (used for variables, i.e. in beans): fieldName.
  3. TITLE naming convention (used for display purposes, i.e. in GUI): Field Name.

An example of where would I need this type of conversion is when I try to parse some error messages I get from some library, and try to present them in a readable format to the final user. In this case I would need to get the field name referenced in the error message (which uses the SQL naming convention, because this library refers to the fields as they are found in the DB) and present it to the user in TITLE format.

Another example would be a matching I do between file headers in a CSV file (in the headers I accept any of the three naming conventions) and the field structure required for the CSV processing to be successful, which I get from a Java Bean whose fields (named using my JAVA convention) represent the list of headers I need to get on the file (with their expected data types). (This design is the outcome of several comments made in that same question).

Here is my working enum:

public enum FieldNameConvention {
    SQL {
        @Override
        public String getName(String name) {
            return convertName(this, name);
        }
    },

    JAVA {
        @Override
        public String getName(String name) {
            return convertName(this, name);
        }
    },

    TITLE {
        @Override
        public String getName(String name) {
            return convertName(this, name);
        }
    };

    public abstract String getName(String fieldName);

    private static String convertName(FieldNameConvention type, String name){

        StringBuilder convertedName = new StringBuilder();  
        // first character is always upper or lower, depending on the type
        char currentChar = name.charAt(0);
        convertedName.append((type == TITLE) ? toUpper(currentChar) : toLower(currentChar));

        for(int i = 1; i < name.length(); i++){
            currentChar = name.charAt(i);
            if(isSeparator(currentChar)){
                // separate if in order to clean trailing separators
                if(++i < name.length()){
                    currentChar = name.charAt(i);
                    String nextChars = getReplacementChars(type, currentChar);
                    convertedName.append(nextChars);
                }
            }else if(isUpper(currentChar)){
                convertedName.append(getReplacementChars(type, currentChar));
            }else{
                convertedName.append(currentChar);
            }
        }
        return convertedName.toString();
    }

    private static String getReplacementChars(FieldNameConvention type, char nextChar) {
        String replacementChars = "";
        switch(type){
            case SQL:
                replacementChars = "_" + toLower(nextChar);
                break;
            case JAVA: 
                replacementChars = "" + toUpper(nextChar);
                break;
            case TITLE:
                replacementChars = " " + toUpper(nextChar);
        }
        return replacementChars;
    }

    private static boolean isSeparator(char value){
        return value == '_' || value == ' ';
    }

    private static boolean isUpper(char value){
        return Character.isUpperCase(value);
    }

    private static char toUpper(char value){
        return Character.toUpperCase(value);
    }

    private static char toLower(char value){
        return Character.toLowerCase(value);
    }
}

And this is how I use it to get the field name in the required convention:

String fieldNameSQL = FieldNameConvention.SQL.getName(fieldName);
String fieldNameJava = FieldNameConvention.JAVA.getName(fieldName);
String fieldNameTitle = FieldNameConvention.TITLE.getName(fieldName);

My questions are:

  1. I would appreciate feedback regarding my coding style, the decision to use enums for this type of task and the solution I am trying to implement to the naming conversion problem I am facing (do you face this same problem? How do you approach it?).
  2. Do I need to program more defensively (check for several spaces/underscores in malformed names, null/empty names, etc)? Or should my contract just process names properly formed (according to one of the three conventions above) and produce unexpected results when something else is provided to it?
  3. And as a side question, would you use lambdas here? How? How can I improve this code further?

Also, am I getting 'too clever' with these design decisions? My ultimate goal is to become very good at producing simple, understandable and correct code!

\$\endgroup\$
2
\$\begingroup\$

Naming

getName() sounds like a poor fit when what you are doing here is really a convert()-like processing.

Inheritance

Your current approach has room for improvement, starting from the fact that all the enum values have the same implementation for the abstract String getName(String) method:

@Override
public String getName(String name) {
    return convertName(this, name);
}

And then your convertName() does some checks here and there for the current enum value (see: getReplacementChars()) in order to derive the correct conversion.

Assuming your approach in convertName() is largely sound, what you should have implemented in each enum value is the method getReplacementChars() (without the first method parameter, naturally), so that they can tell the common method convertName() how to perform the character replacement when required.

static wrapper methods

Your isUpper()/toUpper()/toLower() methods are just wrapper methods, which means you can do without them as well. If you're concerned with using Character. as the class prefix everywhere, you can do a import static Character.*, replacing the asterisk with the three methods you need.

Reinventing the wheel

Come and think of it, Guava's CaseFormat would be an almost-identical fit for your usage if you'll rather not be . With a helpful dose of Apache common-lang's WordUtils for the "Title Case" formatting, you should then be good to go...

edit:

Lambdas

Your code effectively needs to handle two scenarios:

  1. The first character (what case to use)

    This can be implemented as a Function<Character, Character>, i.e. converting a Character (boxed from a char, which is safe) to another Character.

  2. The conversion when encountering the next character past a separator

    There are two possible approaches here:

    1. A Function<Character, String>: Possibly simpler to understand, at the slight expense of not being 'resource-efficient'. This is a stateless operation.

    2. A BiConsumer<Character, StringBuilder>: Appends the conversion result to a supplied StringBuilder to minimize object instantiation. This is a stateful operation on the StringBuilder.

Here's an illustration for both (simply pick your own between using the converter or appender):

// Assuming the java.lang.Character.* methods are imported static-ally
enum FieldNameConvention {
    SQL(Character::toLowerCase,
            c -> "_" + toLowerCase(c),
            (c, builder) -> builder.append('_').append(toLowerCase(c))),
    JAVA(Character::toLowerCase,
            c -> String.valueOf(toUpperCase(c)),
            (c, builder) -> builder.append(toUpperCase(c))),
    TITLE(Character::toUpperCase,
            c -> " " + toUpperCase(c),
            (c, builder) -> builder.append(' ').append(toUpperCase(c)));

    private final UnaryOperator<Character> firstCaser;
    private final Function<Character, String> converter;
    private final BiConsumer<Character, StringBuilder> appender;

    private FieldNameConvention(
            UnaryOperator<Character> firstCaser,
            Function<Character, String> converter,
            BiConsumer<Character, StringBuilder> appender) {
        this.firstCaser = firstCaser;
        this.converter = converter;
        this.appender = appender;
    }
}

The use of firstCaser should be self-explanatory:

// convertedName.append((type == TITLE) ? toUpper(currentChar) : toLower(currentChar));
convertedName.append(firstCaser.apply(fieldName.charAt(0)));

If you are going with the converter (i.e. approach "2.1") way, you just need to replace your getReplacementChars() call with converter.apply(Character):

// convertedName.append(getReplacementChars(type, currentChar));
convertedName.append(converter.apply(currentChar));

If you are going with the appender (i.e. approach "2.2") way, you will need to call appender.accept(Character, StringBuilder). This reuses the StringBuilder instance throughout the method:

// convertedName.append(getReplacementChars(type, currentChar));
appender.accept(currentChar, convertedName);

Unit-testing

It'll be good to introduce some unit testing to make sure your code works too. :)

\$\endgroup\$
  • \$\begingroup\$ Regarding your Inheritance section, I must admit that I started with three different methods, one for each enum, but they were alarmingly similar. I then decided (doubtingly) to refactor them into just one, and use the two parameters for deciding how to perform the conversion on each case. Would you please care to elaborate (and maybe provide some code) on how would you do it without writing the same code over and over? \$\endgroup\$ – carlossierra May 6 '16 at 4:48
  • \$\begingroup\$ @carlossierra updated my answer. \$\endgroup\$ – h.j.k. May 6 '16 at 7:23
1
\$\begingroup\$

You have to analyse the differences of your constants to create proper enum-values. As far as I can see, the main difference is the separator (or lack of it) and the treatment of the following character (either upper case or lower case). There shouldn't be any switches on enum values in your code, rather than a single method that does the job based on the actual enum value. It could for instance work like the following:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public enum FieldNameConvention2
{
    // separator and case handler
    SQL("_", "toLowerCase"),
    JAVA("", "toUpperCase"),
    TITLE(" ", "toUpperCase");

    private final String replacement;
    private final Method caseHandler;

    private FieldNameConvention2(String replacement, String caseHandler) {
        this.replacement = replacement;
        try {
            this.caseHandler = Character.class.getMethod(caseHandler, Character.TYPE);
        } catch (NoSuchMethodException | SecurityException e) {
            throw new RuntimeException("Unable to get hold of method", e);
        }
    }

    public String getName(String fieldName) {
        // your conversion code here...        
        // e.g. converting the case
        try
        {
            // how to execute the caseHandler
            char whateverCase = (char) this.caseHandler.invoke(this, 'a');            
            System.out.println(whateverCase);
        }
        catch (IllegalAccessException
               | IllegalArgumentException
               | InvocationTargetException e)
        {
            // handle exception
        }        
        return null;
    }

}
\$\endgroup\$
  • \$\begingroup\$ I appreciate the use of Character.class.getMethod(caseHandler, Character.TYPE). I just learned something new. But I am afraid your code code won't work as expected, since the casehandler is not always the same for each enum for the first character and for the subsequent characters. For instance, my JAVA uses toLowerCase for the first char and toUpperCase for the word starting chars in the middle. \$\endgroup\$ – carlossierra May 6 '16 at 4:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.