不可变.js关系



想象一下,约翰有两个孩子爱丽丝和鲍勃,鲍勃有一只猫猎户座。

var Immutable = require('immutable');
var parent = Immutable.Map({name: 'John'});
var childrens = Immutable.List([
Immutable.Map({name: 'Alice', parent: parent}),
Immutable.Map({name: 'Bob', parent: parent})
]);
var cat = Immutable.Map({name: 'Orion', owner: childrens.get(1)});

几年后,约翰想改名为简。

var renamedParent = parent.set('name', 'Jane');

让孩子们知道这件事。

childrens = childrens.map(function(children) {
children.set('parent', renamedParent);
});

然后我必须更新cat,因为Bob变了。

cat = cat.set('owner', childrens.get(1));

当一个对象发生变化时,是否可以自动更新所有相关对象?我看了Cursors,但我不确定它们是否是一个解决方案。如果可能的话,你能给我举个例子吗?

问题释义:

当一个对象在不可变集合中发生变化时,是否可以自动更新所有相关对象

简短回答

没有。

答案很长

不,但是在不可变的数据结构中什么都不会改变,所以这不是问题。

更长的答案

它更复杂。。。

不可变性

不可变对象的全部意义在于,如果你引用了一个不可变对象,你就不必检查它的任何属性是否发生了变化。那么,这不是问题吗?好

后果

这会带来一些后果——它们是好是坏取决于你的期望:

  • 按值传递语义和按引用传递语义之间没有区别
  • 一些比较可能更容易
  • 当您将对对象的引用传递到某个位置时,您不必担心代码的其他部分会更改它
  • 当你从某个地方获得对某个对象的引用时,你知道它永远不会改变
  • 您可以避免并发性方面的一些问题,因为没有时间变化的概念
  • 当什么都没有改变时,你不必担心改变是否是原子性的
  • 使用不可变的数据结构更容易实现软件事务存储器(STM)

但世界是多变的

当然,在实践中,我们经常处理随着时间变化的价值观。不可变的状态似乎不能描述可变的世界,但人们有一些方法来处理它

这样看:如果你在某个身份证上有你的地址,并且你搬到了另一个地址,那么这个身份证应该被更改,以与新的真实数据一致,因为你住在那个地址已经不真实了。但是,当你买了包含你地址的东西,然后你更改了地址,收到发票时,发票会保持不变,因为发票开具时你确实住在那个地址。现实世界中的一些数据表示是不可变的,比如本例中的发票,还有一些是可变的,就像ID。

现在以您的例子为例,如果您选择使用不可变结构来对数据进行建模,那么您必须以我的例子中的发票的方式来考虑它。数据可能不是最新的,但在某个时间点上,它总是一致和真实的,而且永远不会改变。

如何应对变化

那么,如何用不可变的数据对变化进行建模呢?在Clojure中,有一种很好的方法是使用Vars、Refs、Atoms和Agents来解决它,ClojureScript(一种针对JavaScript的Clojure编译器)支持其中一些(特别是Atoms应该像Clojure一样工作,但没有Refs、STM、Vars或Agents-看看ClojureScript和Clojure在并发特性方面有什么区别)。

看看Atoms是如何在ClojureScript中实现的,您似乎可以使用普通的JavaScript对象来实现同样的目的。它适用于JavaScript对象本身是可变的,但它有一个引用不可变对象的属性-您将无法更改该不可变对象中的任何属性,但您可以构造一个不同的不可变对象,并在顶级可变对象中将旧对象交换为新对象。

其他像Haskell这样纯粹函数化的语言可能有不同的方式来处理可变世界,比如monads(这是一个众所周知的难以解释的概念——Douglas Crockford,JavaScript:The Good Parts的作者,JSON的发现者,在他的演讲monads和Gonads中将其归因于"monadc诅咒")。

你的问题看起来很简单,但它所涉及的问题实际上相当复杂。当然,在一个对象发生变化时,是否有可能自动更新所有相关对象的问题上只回答"否"是没有意义的,但这比这更复杂,说在不可变的对象中什么都不会改变(所以这个问题永远不会发生)同样没有帮助。

可能的解决方案

您可以有一个顶级对象或变量,您总是可以从中访问所有结构。假设你有:

var data = { value: Immutable.Map({...}) }

如果您总是使用data.value(或使用一些更好的名称)访问数据,那么您可以将data传递给代码的其他部分,并且每当您的状态发生变化时,您可以将一个新的Immutable.Map({…})分配给您的data.value,届时所有使用data的代码都将获得新的值。

如何以及何时将data.value更新为新的不可变结构,可以通过使其从用于更新状态的setter函数自动触发来解决。

另一种方法是在每个结构的级别上使用类似的技巧,例如,我使用变量的原始拼写:

var parent = {data: Immutable.Map({name: 'John'}) };
var childrens = {data: Immutable.List([
Immutable.Map({name: 'Alice', parent: parent}),
Immutable.Map({name: 'Bob', parent: parent})
])};

但您必须记住,您所拥有的值不是不可变的结构,而是那些引用了不可变结构的附加对象,这些对象引入了额外的间接级别。

一些阅读

我的建议是看看其中的一些项目和文章:

  • 彼得·豪塞尔的Immutable React文章
  • Morearty.js-在React中使用不可变状态,类似于在Om中,但使用纯JavaScript编写
  • react游标-用于Facebook的功能状态管理抽象
  • Omniscient-一个为React组件提供抽象的库,允许使用不可变数据进行快速自上而下的渲染*Om-用于React的ClojureScript接口
  • mori-一个在JavaScript中使用ClojureScript持久数据结构的库
  • Fluxy-Facebook Flux架构的实现
  • Facebook的Immutable-JavaScript的持久数据收集,当与Facebook React和Facebook Flux结合时,会面临类似的问题(搜索如何将Immutable与React和Flux结合可能会给你一些好主意)

我希望这个答案即使没有给出一个简单的解决方案也会有所帮助,因为用不可变结构描述可变世界是一个非常有趣和重要的问题。

其他方法

关于不变性的另一种方法,使用不可变对象作为不一定是常数的数据的代理,请参阅Yegor Bugayenko的不可变对象与常识网络研讨会及其文章:

  • 对象应该是不可变的
  • 不变性的帮助
  • 一个不可变的物体是如何具有状态和行为的
  • 不可变物体不是傻瓜

Yegor Bugayenko使用"不可变"一词的意义与它在函数编程中的通常含义略有不同。他不使用不可变或持久的数据结构和函数编程,而是提倡使用该术语最初意义上的面向对象编程,这样你就永远不会真正改变任何对象,但你可以要求它改变一些状态,或改变一些数据,这些数据本身被认为是与对象分离的。很容易想象一个与关系数据库对话的不可变对象。对象本身可以是不可变的,但它仍然可以更新存储在数据库中的数据。很难想象存储在RAM中的一些数据可以被认为与数据库中的数据一样与对象分离,但实际上没有太大区别。如果你把对象看作是暴露某些行为的自治实体,并且尊重它们的抽象边界,那么把对象看作不仅仅是数据的东西实际上是很有意义的,这样你就可以拥有具有可变数据的不可变对象。

如果有什么需要澄清,请发表评论。

正如rsp所阐明的,这个问题反映了不变性所提供的内在权衡。

下面是一个示例react/flux-ish应用程序,演示了处理此问题的一种方法。这里的关键是数字ID用于引用,而不是Javascript引用。

据我所知,您使用不可变对象的级别可能有点太高。据我所知,不可变对象对于表示时间捕获状态很有用。所以比较state1===state2很容易。然而,在您的示例中,您也捕捉到了该州的时间关系。将完整状态与另一个完整状态进行比较是很好的,因为它是不变的数据,所以可读性很强,但也很难更新。我建议在添加另一个库来解决这个问题之前,试着看看是否可以在较低的级别实现Immutable,在那里你实际上需要比较时间捕获的状态。例如,了解1个实体(人/猫)是否发生了更改。

因此,如果您确实想使用不可变对象,请使用它们。但是,如果您想要动态对象,就不要使用不可变对象。

可以通过使用ids引用父记录而不是父记录本身来解决这种情况。我在这里向SQL(sorta)寻求指导:一条记录不应该有指向内存中另一条记录的指针,而应该知道另一条纪录的id

在我的应用程序中,我喜欢让每个对象只知道其引用对象的主键,这样我就不必担心对象更改后会发生什么问题。只要它的密钥保持不变(就像它应该的那样),我总能找到它:

var Immutable = require('immutable');
var people = Immutable.OrderedMap({
0: Immutable.Map({name: 'John', id: '0'}),
1: Immutable.Map({name: 'Alice', id: '1', parentId: '0'}),
2: Immutable.Map({name: 'Bob', id: '2', parentId: '0'})
});
var cat = Immutable.Map({name: 'Orion', ownerId: '2'});

现在,我可以使用标准的immutable.js工具更改任何记录的任何特性(当然除了id),而无需进行任何额外的更新。

people = people.set('0', people.get('0').set('name', 'Jane'))

在对这个完全相同的问题感到沮丧和不知所措之后,我决定将关系完全从对象中移出,并分别管理各种常见关联(一对一、一对多、多对多)的逻辑。

从本质上讲,我使用双向映射来管理关系,并将关系逻辑集中在一个对象中,而不是像传统情况那样集中在我的每个建模数据中。这意味着我需要想出逻辑来观察,例如,当父关系发生变化时,并将变化传播到子关系(因此是bimap)。然而,由于它是在自己的模块中抽象的,因此可以重用它来建模未来的关系。

我决定不使用ImmutableJS来处理这个问题,而是使用我自己的库(https://github.com/jameslk/relatedjs)和使用ES6功能,但我认为可以用类似的方式完成。

最新更新