Search code examples
javaabstract-class

What is the recommended pattern for two classes with identical behaviours but different class constants?


I have two classes which have identical behaviour, except class SnakeCaseyMapper uses snake_case constant fields and class CamelCaseyMapper uses camelCase constant fields.

Before requiring two of these classes, my logic looked roughly like:

public class Mapper {
    public static final String FIELD = "snake_casey_field";
    // Lots of other constant fields ...

    public Foo map(Bar bar) {
        // Some logic that makes use of the constant FIELDs
    }
}

public class ClassThatsDoingLogic {
    var mapper = new Mapper();
    var result = mapper.map(bar);
}

Now I require this same method, map(Bar bar) but with camelCase constants, as well as the original implementation with snake_case.

My idea was to make use of abstract classes:

public abstract class Mapper {
    public String field; // Not instantiated here
    // Lots of other member variable fields ...

    public Foo map(Bar bar) {
        // Some logic that makes use of the constant FIELDs
    }
}

public class SnakeCaseyMapper extends Mapper {
    public SnakeCaseyMapper() {
        field = "snake_casey_field";
        // Lots of other fields instantiated
    }
}

public class CamelCaseyMapper extends Mapper {
    public CamelCaseyMapper() {
        field = "camelCaseyField";
        // Lots of other fields instantiated
    }
}

public class ClassThatsDoingLogic {
    var snakeCaseyMapper = new SnakeCaseyMapper();
    var result = snakeCaseyMapper.map(snakeCaseyBar);
    var camelCaseyMapper = new CamelCaseyMapper();
    var result = camelCaseyMapper.map(camelCaseyBar);
}

This way both classes use the same method logic in map() without duplicating the code. However, I think I lose the finality of the constant fields I had originally. Is there a way around this? Is there a way of handling this problem I'm missing?


Solution

  • As @Kayaman suggested, inheritance should be avoided, and in your case, it is all about parameterisation. If you can do it via configuration loading it would be great.

    A solution in the middle, could be possibly to instantiate a private constructor with all the arguments needed, and then provide one public constructor that would call the private one, setting the arguments needed under condition. (Note: untested code in examples below)

    public class Mapper {
    
        enum MapperType {
            CamelCase,
            SnakeCase
        }
    
        // Never define a public property. Use setters
        // and getters to modify them outside the class,
        // preserving the encapsulation principle.
        private MapperType mType;
        private int mProperty1;
    
        public Mapper(MapperType type) {
           this(type, type == MapperType.CamelCase ? 100 : 200);         
        }
    
        private Mapper(MapperType type, int mProperty1) {
            this.mType = type;
            this.mProperty1 = property1;
            // More properties here
        }
    
    }
    

    A deviation to this, would also be to use Factory-ish pattern (Note: take the definition with a grain of salt, as normally, a factory can be used in order to generate instances of different derived classes sharing the same base class).

    public class Mapper {
    
        enum MapperType {
            CamelCase,
            SnakeCase
        }
    
        private MapperType mType;
        private int mProperty1;
    
        public Mapper(MapperType type, int mProperty1) {
            this.mType = type;
            this.mProperty1 = property1;
            // More properties here
        }
    
    }
    

    Then, you can create a Factory "Wrapper" class for the initialization:

    public static class MapperFactory {
        public static Mapper instantiate(Mapper.MapperType type) {
    
            // Dummy example. Notice that we change all parameters.
            // a dispatch table can also be considered to avoid switching.
            switch(type) {
                case Mapper.MapperType.CamelCase:
                    return new Mapper(Mapper.MapperType.CamelCase, 100);
                case Mapper.MapperType.SnakeCase:
                    return new Mapper(Mapper.MapperType.SnakeCase, 200);
            } 
        }
    }
    

    and then, you can do:

    Mapper m = MapperFactory.instantiate(Mapper.MapperType.CamelCase);
    

    Consider though that, if you are just adding such a few parameters, such implementation is overengineering, just to show you an example. Use it only if you have LOTS of parameters for your objects and you want ti. In simple scenarios, just call the Mapper class with the appropriate parameters, or make a simple conditional check upon initialization.

    Also, regarding the difference between snake_case and camelCase fields, you can use regex in order to distinguish and properly initialize upon condition, but my sense is that you are asking mainly for the proper code segmentation, rather than fields distinction based on the style they are written.