【问题标题】:How to write maintainable merge() method?如何编写可维护的 merge() 方法?
【发布时间】:2017-06-21 14:09:58
【问题描述】:

假设我们有 Person 实体:

class Person {
    /*
        once the id is assigned, then must not be modified! 
        assume that the id will be assigned by the ORM framework
    */
    int id;
    String givenName;
    String familyName;
}

我们有两个人:原始人和更新人:

Person original = new Person("Frantisek", "Makovicka");
Person updated = new Person("Viktor", "Makovicka");

我想将更新的 Person 与原来的 Person 合并,所以我写了以下简单的方法:

// return number of changed fields
public int merge(Person original, Person updated) {
    int changes = 0;

    String oldGivenName = original.givenName;
    original.givenName = updated.givenName;
    if (changed(oldGivenName, original.givenName)) changes++;

    String oldFamilyName = original.familyName;
    original.familyName = updated.familyName;
    if (changed(oldFamilyName, original.familyName)) changes++;

    return changes;
}

它工作正常,但我发现一些问题:
每次在 Person 类中添加新字段时,程序员不要忘记更新 merge() 方法,如果 Person 的字段非常多,那么维护这个方法会很困难。

所以我的问题是:是否有任何更智能/更强大的方法来合并对象的状态而不使用语言的反射功能,以便您可以确保合并所有且唯一需要的字段? 提前致谢!

UPD

本来我问有没有不使用反射的写法,但忘了说不是限制!我还应该说我有一个想法,用反射+用一些自定义注释注释“可合并”字段来编写这个方法,然后跳过没有注释的字段。 所以,这些词的目的:“不使用反射”是为了发现其他可能不那么明显的解决方案:)

这个问题的灵感是这种函数式方法:(确保资源将被关闭,仅作为不太明显的解决方案和安全编程的示例)

public static void doWithResource(String name, Consumer<Resource> consumer) {
    Resource res = new Resource(name);
    consumer.accept(res);
    res.close();
}

【问题讨论】:

  • 更改未定义。你一定是指改变++。现在设置的方式,你总是会从上面的方法中得到 0。
  • 您正在用更新的内容覆盖原始内容,在这种情况下,您可能应该只是覆盖原始指针,而不是调用合并方法。如果你必须这样做,我认为你正在寻找反思。 (主要区别是,如果这是扩展 Person 的两个不同的类,是否应该合并?)
  • 你可以做一些反思,看看你班级的每个(可能是注释的)字段。
  • "不使用语言的反射特性" -- 为什么不呢?
  • @Inxsible 你是对的,这只是一个错字,已修复

标签: java merge maintainability


【解决方案1】:

我看到了您需要的 3 个功能:

  • “id”不得修改
  • 应返回更改计数
  • 对“更新”的任何更改都应应用于“原始”

尤其是前两个要求是一个问题。我认为没有任何基于库的解决方案可以做到这一点。但是,您可以通过一些 Java 反思编写自己的“合并”:

private int merge(Person p1, Person p2) throws IllegalAccessException {
    int changes = 0;
    for(Field field: Person.class.getDeclaredFields()) {
        if(!field.getName().equals("id")) {
            field.setAccessible(true);
            Object originalField = field.get(p1);
            Object updatedField = field.get(p2);
            if(!originalField.equals(updatedField)) {
                field.set(p1, updatedField);
                changes++;
            }
        }
    }
    return changes;
}

【讨论】:

  • 问题说“不使用反射”——我不知道为什么。
  • @Thomas Uhrig 谢谢您的回答!我想知道对原始 Object 类型的对象执行的相等操作是否会正确处理具有覆盖 equals()、hashCode() 方法的类型字段的方程?它不会只执行对象类public boolean equals(Object obj) { return (this == obj); }中定义的类型比较吗?
【解决方案2】:

您可以使用java.beans.Introspector获取PropertyDescriptors。这接近于使用反射,实际上 java.beans 包在内部使用了反射,但它至少更简洁一些,并且(大部分)将仅限于 bean 方法(get/set/is 方法):

public int merge(Person original, Person updated) {
    int changes = 0;

    try {
        BeanInfo info = Introspector.getBeanInfo(Person.class, Object.class);
        for (PropertyDescriptor property : info.getPropertyDescriptors()) {
            if (property.getName().equalsIgnoreCase("id")) {
                continue;
            }

            Method get = property.getReadMethod();
            Method set = property.getWriteMethod();
            if (set == null) {
                // Ignore read-only property.
                continue;
            }

            Object oldValue = get.invoke(original);
            Object newValue = get.invoke(updated);

            set.invoke(original, newValue);

            if (changed(oldValue, newValue)) {
                changes++;
            }
        }
    } catch (IntrospectionException | ReflectiveOperationException e) {
        // We should never get here.
        throw new RuntimeException(
            "Could not update properties of " + Person.class + ": " + e, e);
    }

    return changes;
}

但是,这总是执行浅拷贝。如果任何属性具有可变类型(如数组或集合类型,或可变对象类型,如理论上,地址),并且如果您的方法不对此类类型进行防御性复制,则这两个对象将共享对象,这将导致令人沮丧的休眠错误。

如果您的编码人员都知道执行防御性复制,或者如果您确定没有人会添加具有可变类型的属性,那么这不是问题。否则,它会变得足够复杂,以至于不值得尝试自动复制属性;至少,您需要检查数组或集合类型并克隆值。

【讨论】:

  • 感谢您的有趣回答!我正在研究它并试图掌握这种方法相对于清晰反思的优势。你能澄清一下吗? >并且将(大部分)限于 bean 方法(get/set/is 方法) 它意味着什么?此外,我认为具有清晰反射的方法更灵活,因为我可以例如跳过带注释的字段或可变字段(即您所说的集合)
  • 一个bean属性由一个getter方法表示,根据Java Beans规范,它的名字必须以get开头,除非它的返回类型是boolean,在这种情况下它必须以使用getis。如果 bean 属性不是只读的,它还有一个 setter 方法,其名称必须以set 开头。 java.beans 包遵守这些规则。
【解决方案3】:

https://commons.apache.org/proper/commons-beanutils/copyProperties(Object ToCopyTo, Obj ToCopyFrom) 可能对您有所帮助

【讨论】:

  • 这将覆盖所有属性,包括不应修改的“id”。
猜你喜欢
  • 2010-12-08
  • 2019-12-06
  • 2010-09-14
  • 1970-01-01
  • 2013-05-03
  • 1970-01-01
  • 1970-01-01
  • 2016-01-07
  • 1970-01-01
相关资源
最近更新 更多