避免重复代码:有许多Java类共享字段

4
我正在开发一个应用程序,它将数据写入NoSQL数据库(具体来说是Elasticsearch),我必须管理十几个(随着时间的推移数量会增长)不同的文档类,这些类有许多字段、它们的getter和setter以及将该类转换为JSONObject的方法(每个字段都用@JsonProperty(PROPERTY_NAME)进行注释,我们使用JSON解析器)。
所有这些类都有一些共同的字段和方法,这些都包含在一个超类中(我们称之为DocZero),但它们都有自己的自定义字段,这就使我到了这个点。虽然这些字段是定制的,但是有些字段以一种非线性的方式在不同的类之间共享,也就是说,我有我的文档类Doc1,... DocN,我有一些字段集合(目前大约有10个)以非常混乱的方式在它们之间共享。
以下是一些最佳情况下传达情况的例子: Doc1 包含 Set1Set5Doc2 包含 Set1Set2Set5Set8Doc3 包含 Set6Set7Doc4 包含 Set5Set7Doc5 包含 Set1Set2Set5Set7Set10
鉴于我需要获取和设置这些字段,并且不时地使用它们来操作文档,我从每个Set#中制作了接口,每个接口都包含(抽象的)setter 和 getter。因此,当我声明一个类时,请参考以下示例:
public class DocX implements SetA, SetB, SetC

我会提醒自己实现方法并添加所需的字段,但这意味着实现相同集合的所有类都需要具有相同的参数和相同的方法,这意味着我需要多次编写相同的代码(有时比getter和setter方法更多)。
将所有字段添加到 DocZero 而放弃不同的 Doc#类是一个解决方案,但我不想使用它,因为我希望区分不同的文档类型,并且由于该情况在代码的另一部分以较小的程度存在,即AnotherDocZero AnotherDoc# AnotherSet#,由于其他约束条件,不能进行合并,并且我希望潜在的解决方案也能够工作。
我觉得这是那种可以通过多重继承来解决问题的情况之一,但不幸的是Java不允许使用它。
在这种情况下,我应该如何避免重复?您有什么建议可以改善我的处理方式吗?

虽然我不知道如何避免简单getter/setter的重复,但是非平凡部分的重复可以通过委托来避免:https://dev59.com/oWYr5IYBdhLWcg3w--0I - user2956272
1
你可以将一组字段提取到它们自己的类型中,并使用这些对象组成实际文档,因此它实际上被建模为“Doc2 包含 Set2”,其中 Set2 是一个实际的类实例。 - M. Prokhorov
@dyukha:当我说“非平凡部分”时,我应该更清楚,但是事实上,当我处理集合时,我更喜欢实现add方法(因此首先检查参数是否非空)而不是使用设置器。其他非平凡的方法在其他地方管理。 - Ukitinu
@M.Prokhorov:我认为我仍然存在代码重复问题,但我无法确定它们是从哪里来的或者起源于何处。可能是我实现得不好,我将来可以再尝试这种方法。 - Ukitinu
1
@Phraeq,可能你遇到了重复问题,因为你委托给了SetX对象的方法。我建议你在文档中只使用getSetX() - M. Prokhorov
显示剩余3条评论
4个回答

3
我强烈建议保持你的数据类简单,即使这意味着你需要重复许多字段定义——POJOs维护和理解起来肯定更容易,如果你将所有字段放在一个地方,就可以了解“结果”数据对象的样子——多层继承会很快变得混乱。
为了拥有适当的getter约束,您应该像使用接口一样。你甚至可以为每个getter创建单个接口,并将它们分组到另一个接口中。
public interface Set1To5 extends Set1, Set2, Set3, Set4, Set5 {}

为了避免getter/setter的重复,你可以使用一些附加库,比如lombok,或者考虑不使用getter/setter(将数据文档类中的所有字段都设置为public,但如果需要使用接口来约束类,则不能采用此选项)。

谢谢您推荐Lombok,我会尽快查看。至于“多个接口”,考虑到“Set#”接口实现的高可变性,我将不得不创建太多(不真正相关的)接口集合,这是不值得的。 - Ukitinu

3
如果几种字段经常被分组在一起,那么这意味着分组是程序域的自然部分,并应该作为这样来表示。
因此,如果你的类中经常出现这种情况,
   int xCoordinate;
   int yCoordinate;

您应该改为引入。
public final class Point ... {
   private final int x;
   private final int y;

   Point(int x, int y) {
      ...
   }

   ...
}

那么,不要重复使用xy,请写成:

   Point position;

1
这确实是代码重用(组合)的标准模式。但在这种情况下,我们必须考虑数据库的需求,而其中字段的布局可能超出我们的控制范围(因此我们必须在模型类中将它们展平)。 - Thilo
@Thilo 一些ORM支持嵌入式对象,请看JPA的@Embeddable - Adrian
这种方法的问题在于,我可以分组的所有字段都已经在“sets”接口中分组了。虽然在我给出的示例中,集合1和2总是在一起,但这只是一个巧合,而不是一个模式,因为它们代表的字段完全不相关。 - Ukitinu
1
@Thilo:要将它们展开,可以使用注释 @JsonUnwrapped。我之前尝试过这种方法(请参见问题下的评论),但由于某些我现在无法记起的问题而不得不放弃。 - Ukitinu
最终,我实现了一个类似这个的解决方案,但保持接口以信号化字段/对象的存在。因此,尽管解决方案非常主观,我感到应该标记您的答案为已接受,谢谢。 - Ukitinu

2
有一个模式需要探索。我不知道它是否已经存在或是否有特定的名称。
考虑:
1. Java 8+接口可以具有默认方法。这些方法可以使用其他接口方法来定义附加/默认逻辑。实现这种接口的类自动获得这些方法,而无需实现它们。 2. 另外,一个类可以实现多个接口。
以上两个可以用来在Java中拥有“易于组合”的类型。
例如:
创建一个基本接口,可以存储/检索数据。这可以非常简单,如下所示:
public interface Document {
    <T> T get(String key);
    void set(String key, Object value);
}

这是所有特定数据对象都将使用的基本能力。
现在,使用上述接口定义两个仅包含特定字段getter/setter的接口:
public interface Person extends Document {
    default String getName(){
        return get("name");
    }

    default void setName(String name){
        set("name", name);
    }
}

另一个:
public interface Salaried extends Document {
    default double getSalary(){
        return get("salary");
    }

    default void setSalary(double salary){
        set("salary", salary);
    }
}

明白了吗?这是建立在基本的获取/设置功能之上的简单模式。在实际应用中,您可能希望将字段名称定义为常量。
但到目前为止,这都是接口。它没有与真正的东西(如数据库)链接起来。因此,我们必须定义一个使用DB存储的Document实现:
public class DBDoc implements Document {
    private final Map<String,Object> data;

    public DBDoc(HashMap<String, Object> data) {
        this.data = new HashMap<>(data);
    }

    public DBDoc(){
        this.data = new HashMap<>();
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T get(String key) {
        return (T) this.data.get(key);
    }

    @Override
    public void set(String key, Object value) {
        this.data.put(key, value);
    }
}

我们使用了一个简单的地图来存储,但也可以使用数据库连接或特定于数据库的文档来获取/设置数据。这取决于您使用的数据库或存储方式。
最后,我们有能力从这些接口中组成类型。
public class Employee extends DBDoc implements Person, Salaried { }

并使用它们:
public static void main(String[] args) {
   Employee employee = new Employee();
   employee.setName("Joe");
   employee.setSalary(1000.00);

   System.out.println(employee.getName());
   System.out.println(employee.getSalary());
}

这是一个非常有趣和深思熟虑的解决方案。目前唯一的缺点是,为了完全清楚类中可用的字段,每个字段应该实现一个接口。但是,像许多其他情况一样,真正的解决方案是选择你的毒药并坚持下去。总的来说,由于您的答案正好符合我的要求,我想再等一两天,然后接受它 :-) - Ukitinu
1
这里的一个缺点是它也暴露了通用的 get()/set() 方法,因此任何人都可以使用垃圾字段名称调用 set,并查询不存在的字段。 - M. Prokhorov
@M.Prokhorov 我认为可以通过将 Document 接口和实现的作用域限制在包级别来避免这种情况。 - S.D.
1
@S.D. 在接口中没有办法声明任何东西为包范围,它的成员只能是 public。如果你将基本接口设为特定于包的,则公共接口(例如 Salaried)扩展自基本接口时,“重新导出”其方法作为实现中的公共方法(至少按照 Eclipse 编译器的说法)。所以我认为对于实例方法,如果它们是泛型方法,则没有任何技巧可以隐藏它们。 - M. Prokhorov
1
@M.Prokhorov 啊,接口导出方法。这肯定会暴露原始的IO方法。 - S.D.
1
自Java 9以来(超出了我的问题范围),接口中也可以有私有方法,但它们不能在其外部使用。 - Ukitinu

0

3
如果Set是实际的字段,那么默认方法是无法帮助的。 - M. Prokhorov
@M.Prokhorov说:“我会被提醒实现这些方法并添加所需的字段,但这意味着所有实现相同设置的类都需要具有相同的参数和相同的方法,这意味着我需要多次编写相同的代码。”如果他选择这个解决方案,则默认方法可以帮助解决问题。 - nguyentt
@nguyentt,无论如何,您的回答缺乏细节。请详细描述默认方法如何帮助。 - user2956272
1
他在谈论必须编写相同的代码。当您需要添加无操作方法时,默认方法会有所帮助。在他的情况下,最好的方法是使用无操作setter和始终为空的getter。他仍然需要每个类中的实际实现,除了这一次不会有编译器错误,因此他甚至可能错过需要添加这些字段的某些实例。最终,他将拥有更多这些方法实现的实例,而不是更少。虽然默认方法不是真正的代码重复,但这部分是正确的。 - M. Prokhorov
也许在我渴望尽可能抽象的时候,我没有表达清楚,但是是的,我不能在接口中使用默认方法,因为getter和setter需要访问实际的非最终参数,据我所知,我无法在接口中定义这些内容。无论如何,谢谢@nguyentt。 - Ukitinu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接