对于行为相同但 class 常量不同的两个 classes,推荐的模式是什么?

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

我有两个具有相同行为的 classes,除了 class SnakeCaseyMapper 使用 snake_case 常量字段和 class CamelCaseyMapper 使用camelCase 常量字段。

在要求其中两个 classes 之前,我的逻辑大致如下:

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);
}

现在我需要同样的方法,map(Bar bar) 但使用 camelCase 常量,以及使用 snake_case.

的原始实现

我的想法是利用抽象 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);
}

这样,两个 class 都在 map() 中使用相同的方法逻辑,而无需重复代码。但是,我想我失去了我原来拥有的常量字段的最终性。有没有解决的办法?有没有办法处理我所缺少的这个问题?

正如@Kayaman 所建议的那样,应避免继承,在您的情况下,一切都与参数化有关。如果你能通过配置加载来做到这一点,那就太好了。

中间的一个解决方案,可能是用所有需要的参数实例化一个私有构造函数,然后提供一个 public 构造函数来调用私有构造函数,根据条件设置所需的参数。 (注意: 下面示例中的代码未经测试)

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
    }

}

与此不同的是,也可以使用 Factory-ish 模式(注意: 对定义持保留态度,通常情况下,工厂可以用于为了生成共享相同基础 class) 的不同派生 classes 的实例。

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
    }

}

然后,你可以创建一个Factory"Wrapper"class来初始化:

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);
        } 
    }
}

然后,您可以:

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

考虑一下,如果你只是添加这么几个参数,这样的实现是过度工程,只是给你举个例子。 如果您的对象有很多参数并且您想要 ti,请使用它。在简单的场景中,只需调用 Mapper class 并带上适当的参数,或者在初始化时进行简单的条件检查。

此外,关于 snake_casecamelCase 字段之间的区别,您可以使用正则表达式来区分并根据条件正确初始化,但我的感觉是您主要要求正确的代码分割,而不是基于编写风格的字段区分。

添加到我的评论中。因为当有不同的行为时可以使用继承,所以这绝对不是合适的地方。

下面是 "least effort" 的 3 个示例,尽管它们仍然至少需要您在映射器中拥有字段的行数。

public class Mapper {
    private final String FIELD;
    private String FIELD2 = "defaultCamelCase";
    private final String FIELD3;

    public Mapper(boolean snakeCase) {
        // This would work for final instance fields
        FIELD = snakeCase ? "snakey_case_field" : "camelCaseField";
        // or fields having default values
        if(snakeCase) {
            FIELD2 = toSnakeCase(FIELD2);
            // or some kind of similar mechanism
        }
        // or final instance fields with a private constructor helper
        // that returns either the parameter as-is, or converts it
        FIELD3 = initField("fieldName", snakeCase);
    }

    private String initField(String field, boolean snakeCase) {
        if(!snakeCase)
            return field;

        return Arrays.stream(field.split("(?=[A-Z])")).map(String::toLowerCase).collect(Collectors.joining("_"));
    }
}