Groovy: 有没有比copyWith方法更好的处理@Immutable对象的方法

Groovy: Is there a better way of handling @Immutable objects than copyWith method

我正在 groovy 中寻找一种 "modifying" 不可变对象的灵活方式(复制时更改了某些值)。有一个 copyWith 方法,但它只允许您替换对象的某些属性。好像不够方便

假设我们有一组 类 代表某个系统的领域设计:

@Immutable(copyWith = true)
class Delivery {
    String id
    Person recipient
    List<Item> items
}

@Immutable(copyWith = true)
class Person {
    String name
    Address address
}

@Immutable(copyWith = true)
class Address {
    String street
    String postalCode
}

假设我需要更改收件人的街道。在常规可变对象的情况下,执行就可以了:

delivery.recipient.address.street = newStreet

或(在某些情况下可能有用):

delivery.with {recipient.address.street = newStreet}

根据我的知识,如果要对不可变对象执行相同操作,最好的方法是:

def recipient = delivery.recipient
def address = recipient.address
delivery.copyWith(recipient:
                      recipient.copyWith(address:
                                             address.copyWith(street: newStreet)))

Spock 集成测试代码实际上需要它,因此可读性和表现力很重要。上面的版本不能使用 "on the fly" 所以为了避免创建大量的辅助方法,我实现了自己的 copyOn (因为 copyWith 被采用)方法使得可以写:

def deliveryWithNewStreet = delivery.copyOn { it.recipient.address.street = newStreet }

不过,我想知道是否有最终的解决方案,存在于 groovy 中或由某些外部库提供。谢谢

为了完整起见,我提供了 copyOn 方法的实现。它是这样的:

class CopyingDelegate {
    static <T> T copyOn(T source, Closure closure) {
        def copyingProxy = new CopyingProxy(source)
        closure.call(copyingProxy)
        return (T) copyingProxy.result
    }
}

class CopyingProxy {
    private Object nextToCopy
    private Object result
    private Closure copyingClosure

    private final Closure simplyCopy = { instance, property, value -> instance.copyWith(createMap(property, value)) }
    private final def createMap = { property, value -> def map = [:]; map.put(property, value); map }

    CopyingProxy(Object nextToCopy) {
        this.nextToCopy = nextToCopy
        copyingClosure = simplyCopy
    }

    def propertyMissing(String propertyName) {
        def partialCopy = copyingClosure.curry(nextToCopy, propertyName)
        copyingClosure = { object, property, value ->
            partialCopy(object.copyWith(createMap(property, value)))
        }
        nextToCopy = nextToCopy.getProperties()[propertyName]
        return this
    }

    void setProperty(String property, Object value) {
        result = copyingClosure.call(nextToCopy, property, value)
        reset()
    }

    private void reset() {
        nextToCopy = result
        copyingClosure = simplyCopy
    }
}

然后只需在 Delivery 中添加委托方法即可 class:

Delivery copyOn(Closure closure) {
    CopyingDelegate.copyOn(this, closure)
}

高级解释:

首先需要注意的是代码:delivery.recipient.address.street = newStreet被解释为:

  1. 正在访问 delivery 对象的 recipient 属性
  2. 正在访问 address 上面的结果
  3. 正在为 属性 street 分配 newStreet
  4. 的值

当然 class CopyingProxy 没有这些属性,所以 propertyMissing 方法将被涉及。

正如您所见,它是由 运行 setProperty 终止的 propertyMissing 方法调用链。

基本案例

为了实现所需的功能,我们维护两个字段:nextToCopy(开头为delivery)和copyingClosure(初始化作为使用 @Immutable(copyWith = true) 转换提供的 copyWith 方法的简单副本)。

在这一点上,如果我们有一个像 delivery.copyOn { it.id = '123' } 这样的简单代码,那么根据 simplyCopysetProperty 实现,它将被评估为 delivery.copyWith [id:'123']

递归步骤

现在让我们看看它如何使用更多级别的复制:delivery.copyOn { it.recipient.name = 'newName' }

首先,我们将在创建 CopyingProxy 对象时设置 nextToCopycopyingClosure 的初始值,方法与前面的示例相同。

现在让我们分析一下在第一次 propertyMissing(String propertyName) 调用期间会发生什么。因此,我们将在柯里化函数中捕获当前 nextToCopy(交付对象)、copyingClosure(基于 copyWith 的简单复制)和 propertyNamerecipient) - partialCopy.

然后这个复制将被合并到一个闭包中

{ object, property, value -> partialCopy(object.copyWith(createMap(property, value))) }

这将成为我们的新 copyingClosure。在下一步中,将按照 Base Case 部分中描述的方式调用此 copyingClojure

结论

然后我们执行了:delivery.recipient.copyWith [name:'newName']。然后 partialCopy 应用于给我们 delivery.copyWith[recipient:delivery.recipient.copyWith(name:'newName')]

的结果

所以它基本上是 copyWith 方法调用的树。

最重要的是,您可以看到一些 result 字段和 reset 函数的摆弄。要求在一个闭包中支持多个赋值:

delivery.copyOn { 
    it.recipient.address.street = newStreet
    it.id = 'newId' 
}