“一切都是地图”,我这样做对吗?


69

在我制作的这款游戏中,我观看了Stuart Sierra的演讲“ 数据中的思考 ”,并将其中的一种想法作为设计原则。不同之处在于他在Clojure中工作,而我在JavaScript中工作。我发现我们的语言之间存在一些主要差异:

  • Clojure是惯用的函数式编程
  • 大多数状态是不可变的

我从幻灯片“一切都是地图”中汲取了灵感(从11分钟6秒到> 29分钟)。他说的一些话是:

  1. 每当您看到一个带有2-3个参数的函数时,就可以将其变成一个映射并仅将一个映射传入。这有很多优点:
    1. 您不必担心参数顺序
    2. 您不必担心任何其他信息。如果有多余的键,那不是我们真正关心的。它们只是流过而不会干扰。
    3. 您不必定义架构
  2. 与传递对象相反,没有数据隐藏。但是,他认为数据隐藏可能会导致问题并被高估:
    1. 性能
    2. 易于实施
    3. 通过网络或跨流程进行通信后,无论如何,都必须让双方都同意数据表示。如果仅处理数据,则可以跳过这额外的工作。
  3. 与我的问题最相关。这是29分钟的时间: “使您的功能可组合”。这是他用来解释概念的代码示例:

    ;; Bad
    (defn complex-process []
      (let [a (get-component @global-state)
            b (subprocess-one a) 
            c (subprocess-two a b)
            d (subprocess-three a b c)]
        (reset! global-state d)))
    
    ;; Good
    (defn complex-process [state]
      (-> state
        subprocess-one
        subprocess-two
        subprocess-three))
    

    我了解大多数程序员对Clojure并不熟悉,因此我将以命令式重写此代码:

    ;; Good
    def complex-process(State state)
      state = subprocess-one(state)
      state = subprocess-two(state)
      state = subprocess-three(state)
      return state
    

    优点如下:

    1. 易于测试
    2. 轻松单独查看这些功能
    3. 只需注释一个步骤即可轻松注释掉其中一行,并查看结果是什么
    4. 每个子进程可以向该状态添加更多信息。如果子流程1需要与子流程3进行通信,则就像添加键/值一样简单。
    5. 没有样板可以从状态中提取所需的数据,以便可以将其保存回去。只需传递整个状态,然后让子进程分配所需的数据即可。

现在,回到我的情况:我上了本课并将其应用于我的游戏。就是说,几乎所有我的高级函数都使用并返回一个gameState对象。该对象包含游戏的所有数据。EG:badGuys列表,菜单列表,地面战利品等。这是我的更新功能的示例:

update(gameState)
  ...
  gameState = handleUnitCollision(gameState)
  ...
  gameState = handleLoot(gameState)
  ...

我在这里要问的是,我是否创造了某种可憎的东西,使一个仅在函数式编程语言中可行的想法扭曲了? JavaScript不是惯用的功能(尽管可以用这种方式编写),并且编写不可变的数据结构确实具有挑战性。我所关心的一件事是,他假定每个那些子过程是纯粹的。为什么需要做出这个假设?很少有我的函数是纯函数(通过这种方式,我是说它们经常修改gameState。除此之外,我没有任何其他复杂的副作用)。如果您没有不变的数据,这些想法会崩溃吗?

我担心有一天我会醒来,意识到整个设计是虚假的,我真的只是在实施Big Mud Of Mud反模式


老实说,我已经在编写此代码好几个月了,这很棒。我觉得我正在获得他所声称的所有优势。我的代码对我来说很容易推理。但是我是一个单人团队,所以我有知识的诅咒。

更新资料

我已经用这种模式编码了6个月以上。通常到这个时候,我会忘记自己所做的事情,那就是“我以一种干净的方式写的吗?” 发挥作用。如果没有,我真的会很努力。到目前为止,我一点都没有挣扎。

我了解如何需要另一组眼睛来验证其可维护性。我只能说我首先关心可维护性。无论在哪里工作,我始终是最干净的代码传播者。

我想直接回答那些对这种编码方式有不良经历的人。那时我还不知道,但是我认为我们实际上是在谈论两种不同的代码编写方式。我完成此操作的方式似乎比其他人所经历的更加结构化。当某人对“一切都是地图”有不好的个人经验时,他们会说这很难维护,因为:

  1. 您永远不会知道该功能所需的地图结构
  2. 任何函数都可以以您意想不到的方式改变输入。您必须遍历整个代码库,以查找特定键如何进入映射或为何消失。

对于那些有这种经验的人,也许代码库是:“一切都需要N种地图中的一种。” 我的想法是,“一切都需要1种地图中的1种”。如果您知道那一种类型的结构,那么您就知道一切的结构。当然,这种结构通常会随着时间而增长。这就是为什么...

在一个地方可以找到参考实现(即模式)。此参考实现是游戏使用的代码,因此它不会过时。

关于第二点,我没有在参考实现之外向地图添加/删除键,我只是对已经存在的内容进行了突变。我也有一大套自动化测试。

如果该体系结构最终因其自身的重量而崩溃,我将添加第二个更新。否则,假设一切顺利:)


2
好问题(+1)!我发现尝试以非功能性(或功能不强的语言)实现功能性习语是非常有用的练习。
Giorgio

15
任何要告诉您OO样式信息隐藏(带有属性和访问器函数)的人都是一件坏事,因为(通常可以忽略不计)性能下降,然后告诉您将所有参数转换为映射,这给了您每次尝试检索值时,哈希查找的开销(大得多)都可以安全地忽略。
梅森·惠勒2013年

4
@MasonWheeler可以说您对此是正确的。您是否会因为这一件事是错误的而使他提出的其他观点无效?
丹尼尔·卡普兰

9
在Python(我相信大多数动态语言,包括Javascript)中,对象实际上只是字典/映射的语法糖。
Lie Ryan

6
@EvanPlaice:大O符号可能具有欺骗性。一个简单的事实是,与使用两个或三个单独的机器代码指令进行直接访问相比,任何事情都比较慢,并且在像函数调用这样频繁发生的事情上,这些开销将很快加起来。
梅森惠勒2014年

Answers:


42

我以前曾经支持过“一切都是地图”的应用程序。这是一个可怕的主意。请不要这样做!

当您指定传递给函数的参数时,将很容易知道函数需要什么值。它避免了将多余的数据传递给只会分散程序员注意力的功能-传递的每个值都暗示着它是必需的,这使得支持您代码的程序员必须弄清楚为什么需要数据。

另一方面,如果您将所有内容作为地图传递,则支持您的应用的程序员将必须以各种方式完全了解被调用的函数,以了解地图需要包含哪些值。更糟糕的是,重新使用传递给当前函数的映射以将数据传递给下一个函数非常诱人。这意味着支持您的应用的程序员需要了解当前函数调用的所有函数,才能了解当前函数的功能。这与编写函数的目的恰恰相反-将问题抽象化,这样您就不必考虑它们了!现在,假设有5个通话深度和5个通话宽度。这是您要记住的很多难题,并且要犯很多错误。

“一切都是地图”似乎也导致使用地图作为返回值。我已经看到了它。再说一次,这很痛苦。被调用的函数永远不需要覆盖彼此的返回值-除非您了解所有功能,并且知道输入映射值X需要为下一个函数调用替换。并且当前函数需要修改映射以返回其值,该值有时必须覆盖先前的值,有时必须不覆盖。

编辑-示例

这是一个有问题的示例。这是一个Web应用程序。用户输入已从UI层接受,并放置在地图中。然后调用函数来处理请求。第一个功能集将检查输入是否错误。如果有错误,则会在地图中显示错误消息。调用函数将检查映射中的该条目,并将该值写入ui(如果存在)。

下一个功能集将启动业务逻辑。每个函数都会获取地图,删除一些数据,修改一些数据,对地图中的数据进行操作并将结果放入地图中,等等。后续功能将期望来自地图中先前功能的结果。为了修复后续功能中的错误,您必须调查所有先前的功能以及调用者,以确定可能在哪里设置了期望值。

接下来的功能将从数据库中提取数据。或者,相反,他们会将地图传递给数据访问层。DAL将检查映射是否包含某些值以控制查询的执行方式。如果“ justcount”是键,则查询将是“ count from bar select foo”。先前调用的任何函数都可能是在地图上添加“ justcount”的函数。查询结果将添加到同一地图。

结果将冒泡给调用者(业务逻辑),后者将检查地图以查找操作。其中一些来自最初的业务逻辑添加到地图中的东西。有些可能来自数据库中的数据。知道它来自哪里的唯一方法是找到添加它的代码。并且其他位置也可以添加它。

该代码实际上是一团糟,您必须全部了解才能知道地图中单个条目的来源。


2
您的第二段对我来说很有意义,听起来确实很糟糕。从您的第三段中我可以感觉到,我们并不是真正在谈论相同的设计。要点是“重用”。避免它是错误的。我真的和你的最后一段无关。我让每个函数gameState不了解它之前或之后发生的事情。它只是对给定的数据做出反应。您是如何陷入功能会互相踩到脚趾的情况?能给我举个例子吗?
丹尼尔·卡普兰

2
我添加了一个示例,试图使其更加清晰。希望能帮助到你。另外,传递定义良好的状态对象与传递因许多原因而在许多地方更改的集合的Blob传递,混合ui逻辑,业务逻辑和数据库访问逻辑之间也存在差异
atk 2013年

28

就个人而言,我不会在任何一个范例中都推荐这种模式。这样一来,编写起来就容易些,但代价是以后很难推理。

例如,尝试回答有关每个子流程功能的以下问题:

  • state它需要哪些领域?
  • 它会修改哪些字段?
  • 哪些字段不变?
  • 您可以安全地重新排列功能的顺序吗?

使用这种模式,您必须先阅读整个功能,才能回答这些问题。

在面向对象的语言中,模式意义不大,因为跟踪状态就是对象的工作。


2
“不变性带来的好处随着您不变性对象的增长而下降”,为什么呢?这是关于性能或可维护性的评论吗?请详细说明那句话。
丹尼尔·卡普兰

8
@tieTYT不可变项在某些较小的情况下(例如,数字类型)可以很好地工作。您可以以较低的成本复制它们,创建它们,丢弃它们,轻量化它们。当您开始处理由深,大的地图,树,列表以及数十个(甚至不是数百个)变量组成的整个游戏状态时,复制或删除它的成本就会上升(而且权重变得不切实际)。

3
我懂了。是“命令式语言中的不可变数据”问题还是“不可变数据”中的问题?IE:也许这不是Clojure代码中的问题。但是我可以看到它在JS中的情况。编写所有样板代码来完成它也很痛苦。
丹尼尔·卡普兰

3
@MichaelT和Karl:公平地说,您应该真正提及不变性/效率故事的另一面。是的,天真的使用可能会导致效率低下,这就是为什么人们想出更好的方法的原因。有关更多信息,请参见Chris Okasaki的作品。

3
@MattFenwick我个人非常喜欢不变性。在处理线程时,我了解不可变的东西,可以安全地工作和复制它们。我将其放在参数调用中,然后将其传递给另一个参数调用,而不必担心有人会在返回给我时对其进行修改。如果有人在谈论一种复杂的游戏状态(该问题以该示例为例-我将把“简单”的东西视为nethack游戏状态是不可变的,我会感到恐惧),那么不变性可能是错误的方法。

12

实际上,您似乎正在做的是手动的State monad;我要做的是构建一个(简化的)绑定组合器,并使用该表达式重新表达您的逻辑步骤之间的联系:

function stateBind() {
    var computation = function (state) { return state; };
    for ( var i = 0 ; i < arguments.length ; i++ ) {
        var oldComp = computation;
        var newComp = arguments[i];
        computation = function (state) { return newComp(oldComp(state)); };
    }
    return computation;
}

...

stateBind(
  subprocessOne,
  subprocessTwo,
  subprocessThree,
);

您甚至可以stateBind用来从子子流程中构建各种子流程,并继续沿绑定组合树向下移动以适当地构建计算结构。

有关完整,未简化的State monad的说明,以及有关JavaScript中一般monad的出色介绍,请参见此博客文章


1
好的,我会调查一下(稍后再评论)。但是您如何看待使用模式的想法?
丹尼尔·卡普兰

1
@tieTYT我认为模式本身是一个很好的主意;通常,State monad是用于伪可变算法(不可变但模拟可变性的算法)的有用的代码结构工具。
Ptharien's Flame 2013年

2
+1表示此模式本质上是Monad。但是,我不同意这是一种实际上具有可变性的语言的好主意。Monad是一种使用不允许突变的语言提供全局/可变状态功能的方法。IMO,在不强制不变性的语言中,Monad模式只是心理自慰。
Lie Ryan

6
实际上,@ LieRyan Monad实际上与可变性或全局变量无关。只有State monad专门这样做(因为这正是它的设计目的)。我也不同意State monad在具有可变性的语言中没有用,尽管依赖于其下的可变性的实现可能比我给出的不变性更有效(尽管我对此不确定)。monadic接口可以提供高级功能,而这些功能否则很难访问,stateBind我给出的组合器就是一个非常简单的例子。
Ptharien's Flame 2013年

1
@LieRyan我第二个Ptharien的评论-大多数monads都不是关于状态或可变性的,甚至那个也不是关于全局状态的。Monad实际上可以在OO /命令式/可变语言中很好地工作。

11

因此,在Clojure中这种方法的有效性之间似乎有很多讨论。我认为,看一下Rich Hickey关于他为什么创建Clojure来以这种方式支持数据抽象的哲学可能是有用的:

福格斯:因此,一旦减少了偶然的复杂性,克洛瑞尔将如何帮助解决眼前的问题?例如,理想的面向对象范例旨在促进重用,但Clojure并非传统上是面向对象的—我们如何构造代码以进行重用?

Hickey:我会讨论OO和重用,但是可以肯定的是,能够重用事物使手头的问题变得更简单,因为您不是在重新发明轮子而不是在制造汽车。Clojure在JVM上的使用可以使很多轮子(库)可用。是什么使库可重用?它应该做好一件事或几件事,相对自给自足,并且对客户端代码的需求很少。所有这些都不是面向对象的,并不是所有的Java库都满足这个条件,但是许多都满足。

当我们下降到算法级别时,我认为OO可以严重阻碍重用。特别是,使用对象表示简单的信息数据在生成每条信息的微语言(即类方法)方面几乎是犯罪的,而与关系代数之类的更强大,声明性和通用方法相比,这几乎是犯罪的。发明一个带有自己的接口来容纳一条信息的类就像发明一种新的语言来编写每个简短的故事一样。这是反重用,并且我认为会导致典型的OO应用程序中的代码爆炸。Clojure避开了这一点,而是提倡一种简单的信息关联模型。有了它,就可以编写可在各种信息类型之间重用的算法。

这种关联模型只是Clojure提供的几种抽象之一,而这些正是其重用方法的真正基础:抽象函数。具有开放的大型功能集可对开放的小型可扩展抽象集进行操作,这是算法重用和库互操作性的关键。Clojure函数的绝大部分是根据这些抽象定义的,并且库作者也根据这些抽象来设计其输入和输出格式,从而实现了独立开发的库之间的巨大互操作性。这与DOM和您在OO中看到的其他此类事物形成鲜明对比。当然,您可以在带有接口的OO中进行类似的抽象,例如java.util集合,但是您可以像在java.io中一样轻松地做到这一点。

Fogus在他的《Functional Javascript》一书中重申了这些观点:

在整本书中,我将采用使用最小数据类型来表示抽象的方法,从集合到树再到表。但是,在JavaScript中,尽管其对象类型非常强大,但是提供给它们使用的工具并不能完全起作用。取而代之的是,与JavaScript对象关联的更大的使用模式是为多态调度目的附加方法。幸运的是,您还可以将未命名(不是通过构造函数构建的)JavaScript对象视为简单的关联数据存储。

如果只能对Book对象或Employee类型的实例执行的操作是setTitle或getSSN,则我们已将数据锁定为每个信息的微语言(Hickey,2011年)。一种更灵活的数据建模方法是关联数据技术。JavaScript对象,甚至不包括原型机,都是进行关联数据建模的理想工具,其中命名值可以构造为形成更高级别的数据模型,并以统一的方式进行访问。

尽管在JavaScript本身内用于将数据对象作为数据映射进行操作和访问的工具很少,但是幸好Underscore提供了许多有用的操作。掌握的最简单的函数是_.keys,_。values和_.pluck。_.keys和_.values均根据其功能命名,即获取一个对象并返回其键或值的数组...


2
我之前读过这篇Fogus / Hickey的访谈,但直到现在我还是无法理解他在说什么。感谢您的回答。仍然不确定Hickey / Fogus是否会给我的设计以祝福。我担心我将他们的建议的精神带到了极致。
丹尼尔·卡普兰

9

恶魔的拥护者

我认为这个问题值得恶魔的拥护者(但我当然有偏见)。我认为@KarlBielefeldt提出了很好的观点,我想解决一下。首先,我想说的是他的观点很好。

因为他提到即使在函数式编程中这也不是一个好的模式,所以我将在答复中考虑JavaScript和/或Clojure。这两种语言之间的一个极其重要的相似之处是它们是动态键入的。如果我以Java或Haskell之类的静态类型语言实现这一点,我会更同意他的观点。但是,我将考虑“一切皆是地图”模式的替代方案,这是使用JavaScript而不是静态类型语言的传统OOP设计(我希望这样做不会引起稻草人的争论,请告诉我)。

例如,尝试回答有关每个子流程功能的以下问题:

  • 它需要哪些状态领域?

  • 它会修改哪些字段?

  • 哪些字段不变?

使用动态类型的语言,您通常如何回答这些问题?函数的第一个参数可以命名为foo,那是什么?数组?一个东西?一个对象数组的对象?你怎么知道的?我知道的唯一方法是

  1. 阅读文档
  2. 看一下功能体
  3. 看测试
  4. 猜测并运行该程序以查看其是否有效。

我认为“一切都是地图”模式在这里没有任何区别。这些仍然是我知道回答这些问题的唯一方法。

还要记住,在JavaScript和大多数命令式编程语言中,任何人function都可以要求,修改和忽略它可以访问的任何状态,并且签名没有区别:函数/方法可以对全局状态或单例执行某些操作。签名经常说谎。

我不是要在“一切都是地图”和设计不良的 OO代码之间建立错误的二分法。我只是想指出,具有更少/更多精细/粗粒度参数的签名并不能保证您知道如何隔离,设置和调用函数。

但是,如果您允许我使用这种错误的二分法:与以传统OOP方式编写JavaScript相比,“一切都是地图”似乎更好。以传统的OOP方式,该函数可能会要求,修改或忽略您传入的状态或未传入的状态。使用这种“一切都是地图”模式,您仅需要,修改或忽略您通过的状态在。

  • 您可以安全地重新排列功能的顺序吗?

在我的代码中,是的。请参阅我对@Evicatos答案的第二条评论。也许这只是因为我在做游戏,我不能说。在游戏中真实更新60X第二,它其实并不重要,如果dead guys drop lootgood guys pick up loot反之亦然。不管它们运行的​​顺序如何,每个函数仍然可以按照预期的方式执行。update如果您交换订单,则相同的数据只会在不同的调用中被馈入它们。如果有的good guys pick up lootdead guys drop loot,好家伙会在下一个捡起战利品,update没什么大不了的。人类将无法注意到差异。

至少这是我的一般经验。我真的很容易公开承认这一点。也许认为这样做还可以,这是一件非常非常糟糕的事情。让我知道我是否在这里犯了一些可怕的错误。但是,如果有的话,重新安排功能非常容易,因此顺序dead guys drop loot又可以good guys pick up loot了。它所花费的时间少于编写本段所需的时间:P

也许您认为“死人应该先抢夺战利品。如果您的代码强制执行该命令,那就更好了”。但是,为什么敌人必须先弃掉战利品才能拾取战利品?对我而言,这没有任何意义。也许战利品是在100 updates年前掉落的。无需检查是否有任意坏蛋必须捡起已经在地面上的战利品。这就是为什么我认为这些操作的顺序完全是任意的。

用这种模式编写解耦步骤是很自然的,但是很难注意到传统OOP中的耦合步骤。如果我正在编写传统的OOP,自然的,天真的思维方式是使dead guys drop lootreturn成为Loot我必须传递给的对象good guys pick up loot。由于第一个返回第二个的输入,因此我将无法对这些操作进行重新排序。

在面向对象的语言中,模式意义不大,因为跟踪状态就是对象的工作。

对象具有状态,并且对状态进行突变是很常见的,使状态的历史消失了……除非您手动编写代码来跟踪它。跟踪状态以什么方式表示“他们做什么”?

同样,不变性的好处随着不变性对象的增长而降低。

是的,正如我所说,“我的功能很少是纯净的”。它们始终仅根据其参数进行操作,但是会更改其参数。当我将此模式应用于JavaScript时,这是我必须做出的妥协。


4
“函数的第一个参数可以命名为foo,那是什么?” 这就是为什么您不将参数命名为“ foo”,而是将“重复项”,“父项”和其他名称命名为与函数名称结合使用时显而易见的原因。
塞巴斯蒂安·雷德尔

1
我必须在所有方面都同意你的观点。Javascript真正给这种模式带来的唯一问题是,您正在处理可变数据,因此,您很可能会改变状态。但是,有一个库可让您访问普通javascript中的clojure数据结构,尽管我忘记了它的名字。传递参数作为对象也不是闻所未闻的,jQuery会在多个位置进行传递,但是会记录它们使用对象的哪些部分。不过,就我个人而言,我将UI字段和GameLogic字段分开,但任何适合您的方法:)
Robin Heggelund Hansen

@SebastianRedl我应该通过parent什么?是repetitions数字或字符串数​​组,还是没关系?也许重复只是一个数字,代表我想要的重复次数?那里有很多api,它们仅带有一个options对象。如果您正确命名事物,那么世界是一个更好的地方,但这并不能保证您会知道如何使用api,不会问任何问题。
丹尼尔·卡普兰

8

我发现我的代码趋向于像下面这样结构化:

  • 带有地图的功能往往更大,并且有副作用。
  • 带有参数的函数往往更小并且是纯净的。

我没有打算创建这种区别,但是通常这就是我代码中的区别。我认为使用一种样式不一定会否定另一种样式。

纯函数易于进行单元测试。带有地图的较大地图会更多地进入“集成”测试区域,因为它们往往涉及更多的运动部件。

在javascript中,有很多帮助的事情是使用Meteor的Match库之类的东西来执行参数验证。它非常清楚该函数期望什么并且可以非常干净地处理地图。

例如,

function foo (post) {
  check(post, {
    text: String,
    timestamp: Date,
    // Optional, but if present must be an array of strings
    tags: Match.Optional([String])
    });

  // do stuff
}

有关更多信息,请参见http://docs.meteor.com/#match

::更新::

斯图尔特·塞拉(Stuart Sierra)录制的Clojure / West的“大Clojure”录像带也触及了这一主题。像OP一样,他将副作用作为地图的一部分进行控制,因此测试变得更加容易。他也有一篇博客文章,概述了他当前的Clojure工作流程,这似乎很重要。


1
我认为我对@Evicatos的评论将在这里详细说明我的立场。是的,我正在变异,功能不纯。但是,我的功能确实很容易测试,尤其是在事后回想我没有计划进行测试的回归缺陷时。一半的功劳归功于JS:仅使用测试所需的数据来构造“地图” /对象非常容易。然后,就像传递它并检查突变一样简单。副作用总是显示地图中,因此易于测试。
丹尼尔·卡普兰

1
我相信两种方法的务实使用是“正确的”方法。如果测试对您来说很容易,并且您可以缓解将必需的字段传达给其他开发人员的元问题,那么这听起来像是一场胜利。谢谢你的问题; 我很喜欢阅读您已经开始的有趣的讨论。
2013年

5

我可以想到的主要反对意见是,很难判断一个函数实际需要什么数据。

这意味着代码库中的未来程序员将必须知道被调用的函数在内部如何工作以及任何嵌套函数的调用才能进行调用。

我考虑得越多,您的gameState对象闻起来就越像全局对象。如果那是它的使用方式,为什么还要传递它呢?


1
是的,因为我通常对其进行突变,所以它是全局的。为什么要烦扰传递它?我不知道,这是一个有效的问题。但是我的直觉告诉我,如果我停止传递它,我的程序将立即变得难以推理。每个功能可以做任何一切没有全球状态。现在,您可以在函数签名中看到这种潜力。如果您不能告诉我,我对此并不自信:)
Daniel Kaplan 2013年

1
顺便说一句:re:反对它的主要论点:无论是clojure还是javascript,这似乎都是事实。但这是很有价值的一点。也许列出的好处远大于其负面影响。
丹尼尔·卡普兰

2
我现在知道为什么即使它是一个全局变量也要麻烦传递它:它允许我编写纯函数。如果更改gameState = f(gameState)f(),则测试起来会困难得多ff()每次我叫它可能会返回不同的东西。但是,很容易f(gameState)每次输入相同的输入就返回相同的东西。
丹尼尔·卡普兰

3

比起大泥巴球,您做的事情还有更多合适的名字。您正在做的事情称为上帝对象模式。乍一看似乎不是这样,但是在Javascript中,它们之间几乎没有区别

update(gameState)
  ...
  gameState = handleUnitCollision(gameState)
  ...
  gameState = handleLoot(gameState)
  ...

{
  ...
  handleUnitCollision: function() {
    ...
  },
  ...
  handleLoot: function() {
    ...
  },
  ...
  update: function() {
    ...
    this.handleUnitCollision()
    ...
    this.handleLoot()
    ...
  },
  ...
};

一个好主意可能取决于情况。但这当然符合Clojure的方式。Clojure的目标之一是消除Rich Hickey所谓的“偶然复杂性”。多个通信对象肯定比单个对象复杂。如果将功能划分为多个对象,则突然必须担心沟通和协调以及责任划分。这些都是仅与您编写程序的原始目标有关的复杂性。您应该会看到Rich Hickey的“ 简单 ”演讲变得容易。我认为这是一个很好的主意。



“在面向对象的编程中,上帝对象是知道太多或做得太多的对象。上帝对象是反模式的一个例子。” 因此,上帝的对象不是一件好事,但是您的信息似乎在说相反的话。这让我有些困惑。
丹尼尔·卡普兰

@tieTYT您没有在进行面向对象的编程,所以没关系
user7610 2014年

您是如何得出这一结论的(“可以”的结论)?
Daniel Kaplan 2014年

OO中的上帝对象的问题是“对象变得如此了解所有事物,或者所有对象都变得如此依赖上帝对象,以至于在进行更改或错误修复时,它成为实现的真正噩梦。” 源代码在您的代码中,除了上帝对象之外还有其他对象,因此第二部分不是问题。关于第一部分,您的上帝对象程序,您的程序应该了解所有内容。所以也可以。
user7610 2014年

2

我只是在今天早些时候面对一个新项目时碰到了这个话题。我正在Clojure从事扑克游戏。我将面值和西服表示为关键字,并决定将卡牌表示为地图,例如

{ :face :queen :suit :hearts }

我也可以使它们成为两个关键字元素的列表或向量。我不知道它是否会使内存/性能有所不同,所以我现在才使用地图。

如果以后再改变主意,我决定程序的大多数部分都应该通过“接口”来访问卡的各个部分,以便对实现细节进行控制和隐藏。我有功能

(defn face [card] (card :face))
(defn suit [card] (card :suit))

该程序的其余部分使用的。卡作为地图传递给函数,但是函数使用一致同意的界面来访问地图,因此不应混乱。

在我的程序中,一张卡可能只会是一个二值地图。在问题中,整个游戏状态作为地图传递。游戏状态会比一张纸牌复杂得多,但我认为使用地图不会引起任何错误。用一种命令式语言,我同样可以拥有一个大的GameState对象并调用其方法,并且存在相同的问题:

class State
  def complex-process()
    state = clone(this) ; or just use 'this' below if mutation is fine
    state.subprocess-one()
    state.subprocess-two()
    state.subprocess-three()
    return state

现在它是面向对象的。它有什么特别的问题吗?我不这么认为,您只是将工作委派给了知道如何处理State对象的函数。而且,无论您使用的是地图还是对象,都应警惕何时将其拆分成较小的部分。因此,我说使用地图非常好,只要您对对象的使用保持相同的谨慎即可。


2

从我所看到的(一点)来看,在功能语言中,至少在纯语言中,使用地图或其他嵌套结构制作这样的单个全局不可变状态对象是相当普遍的,尤其是在使用State Monad作为@ Ptharien'sFlame的情况下精神病

我已经看到/阅读过的有效使用此功能的两个障碍(这里提到的其他答案)是:

  • 在(不可变)状态下使(深度)嵌套值突变
  • 从不需要状态的功能中隐藏大部分状态,只给它们一点处理/变异所需的功能

有几种不同的技术/通用模式可以帮助缓解这些问题:

第一个是Zippers:它们使一个人穿越并改变了不可变的嵌套层次结构中的状态。

另一个是Lenses:它们使您可以将结构聚焦到特定位置并在其中读取/更改值。您可以将不同的镜头组合在一起以专注于不同的事物,就像OOP中的可调属性链(您可以在其中用变量代替实际的属性名称!)

Prismatic最近在一篇博客文章中介绍了如何在JavaScript / ClojureScript中使用这种技术,您应该查看一下。他们使用游标(将其与拉链进行比较)来将窗口状态用于功能:

Om使用游标恢复封装和模块化。游标为应用程序状态的特定部分(类似于拉链)提供了可更新的窗口,使组件可以仅引用全局状态的相关部分,并以无上下文的方式更新它们。

IIRC,他们在那篇文章中还谈到了JavaScript的不变性。


提到的讨论OP还讨论了使用update-in函数来限制函数可以更新为状态图的子树的范围。我认为还没有人提出这一点。
user7610 2014年

@ user7610不错,我不敢相信我忘了提到一个-我喜欢那个功能(和assoc-in其他功能)。猜猜我只是在大脑上有Haskell。我想知道是否有人完成过JavaScript移植?人们可能没有提出来,因为(像我一样)他们没有看谈话:)
paul 2014年

@paul在某种意义上是有的,因为它在ClojureScript中可用,但是我不确定这是否在您的脑海中浮现。它可能存在于PureScript中,我相信至少有一个库可以在JavaScript中提供不变的数据结构。我希望其中至少有一个,否则使用起来会很不方便。
Daniel Kaplan 2014年

@tieTYT当我发表评论时,我一直在考虑本机JS实现,但是您对ClojureScript / PureScript提出了很好的看法。我应该研究不可变的JS,看看那里有什么,我之前从未使用过它。
保罗

1

这是否是一个好主意,将取决于您对这些子流程中的状态所做的工作。如果我正确理解Clojure示例,则返回的状态词典与传入的状态词典不同。它们是副本,可能还会进行添加和修改,(我认为)Clojure能够有效创建,因为语言的功能性质取决于它。每个功能的原始状态字典都不会进行任何修改。

如果我理解正确,那么您正在修改传递给javascript函数的状态对象,而不是返回副本,这意味着您正在做的事情与Clojure代码所做的非常非常不同。正如Mike Partridge所指出的那样,这基本上只是一个全局变量,您可以毫无理由地显式传递给函数或从函数返回。在这一点上,我认为这只是让您认为自己正在做的事情实际上不是。

如果您实际上是在明确创建状态的副本,请对其进行修改,然后返回该修改后的副本,然后继续进行。我不确定这是否一定是完成您要使用Javascript进行操作的最佳方法,但它可能与Clojure示例所做的工作“接近”。


1
最后,它真的是 “非常非常不同”吗?在Clojure示例中,他用新状态覆盖了旧状态。是的,没有真正的突变,身份正在改变。但是在他写的“好”示例中,他没有办法获取传递到第二子流程的副本。该值的标识已被覆盖。因此,我认为“非常非常不同”只是一个语言实现细节。至少在您提出的内容中。
丹尼尔·卡普兰

2
Clojure示例发生了两件事:1)第一个示例取决于以特定顺序调用的函数,以及2)这些函数是纯函数,因此它们没有任何副作用。因为第二个示例中的函数是纯函数并且共享相同的签名,所以您可以对它们重新排序,而不必担心对它们调用顺序的任何隐藏依赖性。如果要在函数中改变状态,则没有相同的保证。状态突变意味着您的版本不易组合,这是字典的最初原因。
Evicatos

1
您将不得不向我展示一个示例,因为根据我的经验,我可以随意移动周围的事物,并且效果不大。为了向自己证明这一点,我在update()函数中间移动了两个随机子过程调用。我将一个移到顶部,将一个移到底部。我所有的测试仍然通过,当我玩游戏时,我没有发现不良影响。我觉得我的功能只是作为Clojure的例子作为组合的。每个步骤之后,我们都将丢弃旧数据。
丹尼尔·卡普兰

1
您的测试通过并没有注意到任何不良影响,这意味着您当前没有在其他地方突变任何具有意外副作用的状态。尽管您不能保证总是这样,但由于您的函数并不纯净。如果您说您的功能不是纯粹的,但是在每一步之后都将旧数据丢弃,我想我一定会对您的实现产生根本性的误解。
Evicatos

1
@Evicatos-纯粹并且具有相同的签名并不意味着函数的顺序无关紧要。想象一下在应用固定折扣和百分比折扣的情况下计算价格。(-> 10 (- 5) (/ 2))返回2.5。(-> 10 (/ 2) (- 5))返回
0。– Zak

1

如果您将全局状态对象(有时称为“神对象”)传递给每个进程,那么最终会混淆许多因素,所有这些因素都会增加耦合,同时会降低内聚力。这些因素都以负面方式影响长期可维护性。

流水线耦合这是由于通过各种方法传递数据而无需将几乎所有数据传递到实际可以处理的地方。这种耦合类似于使用全局数据,但可以包含更多内容。流浪汉耦合与“需要知道”相反,后者用于定位效果并包含一个错误的代码片段可能对整个系统造成的损害。

数据导航示例中的每个子流程都需要知道如何准确地获取其所需的数据,并且需要能够对其进行处理,并可能构造一个新的全局状态对象。这是流浪耦合的逻辑结果;为了对基准进行操作,需要基准的整个上下文。同样,非本地知识是一件坏事。

如果您传递的是“拉链”,“镜头”或“光标”(如@paul所述),那是一回事。您将包含访问权限,并允许拉链等控制数据的读写。

单一责任违背宣称“子过程一”,“子过程二”和“子过程三”中的每一个仅具有单一责任,即产生其中具有正确值的新全局状态对象,这是令人震惊的简化主义。到底是不是全部,不是吗?

我的意思是,让游戏的所有主要组成部分承担相同的职责,因为游戏会破坏委托和保理的目的。

系统影响

设计的主要影响是可维护性低。您可以将整个游戏牢记在心的事实说明您很可能是一名优秀的程序员。我设计了很多东西,可以为整个项目牢牢记住。不过,这不是系统工程的重点。关键是要使一个系统可以工作的范围大于一个人可以同时控制的范围

添加另一个或两个或八个程序员,将导致您的系统几乎立即崩溃。

  1. 上帝物品的学习曲线是平坦的(即,要在其中胜任需要很长时间)。每个其他程序员都需要学习所有您知道的知识,并将其掌握在自己的头脑中。您将只能雇用比您更好的程序员,前提是您可以支付给他们足够的薪水以维护庞大的上帝对象。
  2. 在您的描述中,测试配置仅是白盒。您需要了解上帝对象的每个细节以及被测模块,以便进行测试,运行并确定a)做对了,b)没有做任何事情。 10,000个错误的事物。对你不利的可能性很大。
  3. 添加新功能要求您a)遍历每个子流程并确定该功能是否影响其中的任何代码,反之亦然; b)遍历全局状态并设计添加项,c)进行每个单元测试并对其进行修改检查没有被测单元对新功能有不利影响

最后

  1. 可变神对象一直是我编程生活的祸根,是我自己做的事情,还有一些我被困住的事情。
  2. State monad无法缩放。状态呈指数增长,其中包含对测试和操作的所有暗示。在现代系统中,我们控制状态的方法是通过委派(职责划分)和范围界定(将访问限制为仅对状态的子集进行访问)。“一切都是地图”方法与控制状态完全相反。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.