考虑这个简化的应用领域:
-
刑事调查数据库
-
Person
是参与调查的任何人 -
Report
是一些信息,是调查的一部分 -
A
Report
引用主要Person
(调查对象) -
A
Report
有第二相关的同谋(当然可能是其他调查或报告的主要内容) -
这些类具有用于将它们存储在数据库中的ID,因为它们的信息可能随时间而变化(例如,我们可能会找到一个人的新别名,或者将一些感兴趣的人添加到报告中)
Domain http://yuml.me/13fc6da0
如果这些存储在某种数据库中并且我希望使用不可变对象,那么似乎存在关于状态和引用的问题 .
假设我更改了一些关于 Person
的元数据 . 由于我的 Person
对象是不可变的,我可能会有一些代码:
class Person(
val id:UUID,
val aliases:List[String],
val reports:List[Report]) {
def addAlias(name:String) = new Person(id,name :: aliases,reports)
}
所以我的带有新别名的 Person
成为一个新对象,也是不可变的 . 如果 Report
指的是那个人,但别名在系统的其他地方被更改,我的 Report
现在指的是"old"人,即没有新别名的人 .
同样,我可能会:
class Report(val id:UUID, val content:String) {
/** Adding more info to our report */
def updateContent(newContent:String) = new Report(id,newContent)
}
由于这些对象不知道谁引用了它们,因此我不清楚如何让所有“引用者”知道有一个新对象可用来代表最近的状态 .
这可以通过将来自中央数据存储的所有对象"refresh"以及创建新的,更新的对象的所有操作存储到中央数据存储来完成,但这感觉就像是基础语言引用的简洁重新实现 . 也就是说,让这些"secondary storable objects"变得更加清晰 . 因此,如果我向 Person
添加别名,则所有引用都会看到新值而不执行任何操作 .
当我们想要避免可变性时,如何解决这个问题,或者这是不可变性无效的情况?
3 回答
我建议你阅读他们如何处理clojure和Akka中的问题 . 阅读Software transactional memory . 还有一些我的想法......
不变性不是为了它本身而存在的 . 不变性是抽象的 . 它本质上不“存在” . 世界是可变的,世界永远在变化 . 因此,数据结构可变是很自然的 - 它们描述了在给定时刻的真实或模拟对象的状态 . 它看起来像OOP rulez . 在概念层面,这种态度的问题是RAM中的对象!=真实对象 - 数据可能不准确,它带有延迟等
因此,在最琐碎的要求的情况下,你可以使用一切可变的 - 人,报告等实际问题将出现在:
从并发线程修改
数据结构
用户为相同的对象提供令人震惊的更改
用户提供无效数据,应该回滚
使用天真的可变模型,您很快就会得到不一致的数据和破碎系统 . 可变性容易出错,不可变性是不可能的 . 您需要的是世界的交易视图 . 在交易程序中看到不可变的世界 . STM管理以一致且线程安全的方式应用的更改 .
我想你正在努力摆平圈子 . Person是不可变的,Person上的Reports列表是Person的一部分,Reports列表可以更改 .
对于一个不可变的Person是否有可能引用一个可变的PersonRecord来保存Reports和Aliases之类的东西?
如果X指向Y,两者都是不可变的,并且Y改变(即用更新的副本替换它),那么你别无选择,只能替换X(因为它已经改变,因为新的X指向新的Y,不是旧的) .
在高度互联的数据结构中维护这种情况很快就会成为头疼你有三种一般方法 .
一般忘记不变性 . 使链接变得可变 . 根据需要修复它们 . 确保你确实修复了它们,否则你可能会遇到内存泄漏(X指的是旧的Y,它指的是旧的X,它指的是较旧的Y等) .
不存储直接链接,而是存储您可以查找的ID代码(例如,键入哈希映射) . 然后,您需要处理查找失败案例,但其他方面非常强大 . 当然,这比直接链接慢一点 .
改变整个世界 . 如果某些内容发生了变化,那么链接到它的所有内容也必须进行更改(并且在复杂的数据集中同时执行此操作很棘手,但理论上可行,或者至少可以隐藏它的可变方面,例如有很多惰性值) .
我希望,哪个更好取决于您的查找和更新速度 .