【问题标题】:Avoid duplicate code with numerous Java classes sharing fields避免大量 Java 类共享字段的重复代码
【发布时间】:2019-09-04 08:38:22
【问题描述】:

我正在开发一个在 NoSQL 数据库(确切地说是 Elasticsearch)上写入的应用程序,我必须管理十几个(数量随时间增长)不同的文档类,即具有众多字段的类,它们的getter 和 setter 以及将类转换为 JSONObject 的方法(每个字段都用 @JsonProperty(PROPERTY_NAME) 注释,我们使用 JSON 解析器)。

所有这些类都有一些共同的字段和方法,它们都包含在一个超类中(让我们称之为DocZero),但它们都有自己的自定义字段,这让我明白了这一点。
确实这些字段是自定义的,但有些字段以非线性方式在不同的类之间共享,也就是说我有我的文档类Doc1,...DocN,我有一些字段集(大约 10截至目前)由他们以非常狂野的方式分享。

一些最能表达情况的例子:
Doc1 包含Set1 到Set5;
Doc2 包含Set1、Set2 和Set5 到Set8;Doc3 包含 Set6 和 Set7;
Doc4 包含 Set5 和 Set7;
Doc5 包含 Set1、Set2 和 @987654434343 @到Set10。

鉴于我需要获取和设置这些字段,并且不时使用它们来操作文档,我使用 Set# 制作了接口,每个接口都包含(抽象)setter 和 getter。
因此,当我声明一个类时

public class DocX implements SetA, SetB, SetC

我被提醒实现方法并因此添加必填字段,但这意味着实现相同集合的所有类都需要具有相同的参数和相同的方法,这意味着我需要编写许多相同的代码次(有时超过 getter 和 setter 方法)。

将所有字段添加到 DocZero 之前的不同 Doc# 类是我不热衷于使用的解决方案,因为我更喜欢区分不同的文档类型,并且由于这种情况存在,在较低的数量级,在另一个代码的一部分,AnotherDocZero、AnotherDoc# 和 AnotherSet# 由于其他限制而无法合并,我也希望有一个潜在的解决方案。
我觉得这是多重继承可以解决问题的情况之一,但不幸的是 Java 不允许这样做。

在这种情况下如何避免重复?你有什么建议可以改善我对这个问题的处理吗?

【问题讨论】:

  • 虽然我不知道如何避免重复简单的 getter/setter,但可以通过委托避免重复重要部分:stackoverflow.com/questions/13245610/…
  • 您可以提取各自类型的字段集,并编写这些对象的实际文档,因此它实际上被建模为Doc2 contains Set2,Set2 是一个实际的类实例。
  • @dyukha:当我说“非平凡的部分”时,我应该更清楚,但问题是,当我处理集合时,我更喜欢而不是 setter实现add 方法(因此首先检查参数是否为非空)。其他重要的方法在别处管理。
  • @M.Prokhorov:您是否建议使用接口所需的参数创建类,然后使用@JsonUnwrap 之类的东西来获取平面JSON?我记得不久前做过类似的事情,当我第一次尝试在没有接口的情况下解决问题时,但由于其他复杂性而放弃了它(我不记得这些是什么了)。
  • @Phraeq,您可能遇到了欺骗问题,因为您委托给了SetX 对象的方法。我建议您在文档中添加 getSetX()。

标签: java database java-8 code-duplication


【解决方案1】:

如果几种字段经常组合在一起,这表明分组是程序领域的自然组成部分,应该这样表示。

所以,如果你经常在课堂上发现这个

   int xCoordinate;
   int yCoordinate;

你应该改为介绍

public final class Point ... {
   private final int x;
   private final int y;

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

   ...
}

那么不要重复x和y,而是写

   Point position;

【讨论】:

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

我强烈建议保持你的数据类简单,即使这确实意味着你需要重复许多字段定义 - 如果你有所有字段,POJO 肯定更容易维护和理解“结果”数据对象的样子一个地方 - 多级继承很快就会造成混乱

对于拥有适当的 getter 的限制,您应该像您一样使用接口。您甚至可以为每个 getter 创建单个接口,并将它们分组到另一个接口中,例如

public interface Set1To5 extends Set1, Set2, Set3, Set4, Set5 {}

为了避免 getter/setter 的重复,您可以使用一些额外的库,例如 lombok 或考虑根本不使用 getter/setter(只需将数据文档类中的所有字段设置为 public - 但这当然是如果您需要使用接口约束类,则不是该选项)

【讨论】:

  • 感谢lombok的推荐,我会尽快查看。就“多个接口”而言,鉴于“Set#”接口实现的高度可变性,我将不得不创建太多(不是真正相关的)接口的“集合”,这是不值得的。
【解决方案3】:

有一种模式可供探索。我不知道它已经存在或有一个特定的名称。

考虑:

  1. Java 8+ 接口可以有default methods。这些方法可以使用其他接口方法来定义附加/默认逻辑。实现此类接口的类会自动获取这些方法,而无需实现它们。

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

明白了吗?这是一个建立在基本 get/set 能力之上的简单模式。您可能希望在实际应用程序中将字段名称定义为常量。

但到目前为止,都是接口。它没有链接到真实的东西,比如数据库。因此,我们必须为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());
}

【讨论】:

  • 这是一个非常有趣和周到的解决方案。我现在可以看到的唯一缺点是每个字段应该只实现一个接口,以完全清楚类中可用的字段。但是,就像在许多其他情况下一样,真正的解决方案是挑选你的毒药并坚持下去。总而言之,既然你的答案正是我想要的,我想我会再等一两天然后接受它:-)
  • 这里的一个缺点是它也公开了通用的get()/set() 方法,因此任何人都可以使用垃圾字段名称调用set,并查询不存在的字段。
  • @M.Prokhorov 我认为这可以通过 Document 接口和实现的包级别范围来避免。
  • @S.D.没有办法将接口上的任何内容声明为包范围,它的成员只能是public。如果您制作特定于基础接口包的公共接口,例如Salaried,它从基础“重新导出”其方法作为实现中的公共接口(至少根据 Eclipse 编译器)。所以我认为如果泛型方法是实例方法,我认为没有任何技巧可以隐藏它们。
  • @M.Prokhorov 啊,接口导出方法。这肯定暴露了原始 IO 方法。
【解决方案4】:

【讨论】:

  • 如果Sets 是实际字段,默认方法没有帮助。
  • @M.Prokhorov:“我被提醒实现方法并因此添加必填字段,但这意味着实现相同集合的所有类都需要具有相同的参数和相同的方法这意味着我需要多次编写相同的代码”。如果他采用此解决方案,默认方法会有所帮助。
  • @nguyentt,无论如何,您的回答都缺乏细节。请准确描述默认方法的帮助。
  • 他说的是必须编写相同的代码。当您需要添加无操作方法时,默认方法会有所帮助。在他最好的情况下,他可以做一个无操作的 setter 和一个永远为空的 getter。而且他仍然需要每个类中的实际实现,除了这次不会出现编译器错误,因此他甚至可能会错过一些需要添加这些字段的实例。最后,他将拥有更多这些方法实现的实例,而不是更少。虽然默认方法不是真正的代码欺骗,但那部分是正确的。
  • 也许我希望尽可能抽象,我没有说清楚,但是是的,我不能在接口中使用默认方法,因为 getter 和 setter 需要访问实际的非最终参数,据我所知,我无法在接口中定义它们。无论如何,谢谢@nguyentt。
猜你喜欢
  • 1970-01-01
  • 2023-03-22
  • 2020-02-21
  • 1970-01-01
  • 1970-01-01
  • 2014-03-07
  • 2020-03-01
相关资源
最近更新 更多