在使用不可变对象时,是否有一种特定的设计策略可用于解决大多数“鸡与蛋”问题?


17

来自OOP背景(Java),我自己学习Scala。虽然我很容易看到单独使用不可变对象的优点,但我很难理解如何设计这样的整个应用程序。我举一个例子:

假设我有代表“材料”及其属性的对象(我在设计游戏,所以我实际上确实有这个问题),例如水和冰。我将拥有一个拥有所有此类材料实例的“经理”。一种特性是凝固点和熔点,以及材料冻结或融化的温度。

[编辑]所有的实例都是“单例”,有点像Java枚举。

我想让“水”说在0C时冻结成“冰”,而“冰”说在1C时熔化成“水”。但是,如果水和冰是不可变的,则它们不能作为构造函数参数相互引用,因为其中一个必须首先创建,并且不能将尚未存在的另一个作为构造函数参数进行引用。我可以通过给他们两个都提供给经理的参考来解决这个问题,以便他们每次查询冻结/融化属性时都可以查询它以找到他们需要的其他材料实例,但是随后经理之间也遇到了同样的问题和材料,它们需要相互引用,但是只能在构造函数中为它们之一提供引用,因此管理器或材料不能是不变的。

他们只是无法解决这个问题,还是我需要使用“功能”编程技术或其他某种模式来解决它?


2
对我来说,你所说的方式,没有水也没有冰。这里还有h2o材料
蚊蚋

1
我知道这将具有更多的“科学意义”,但是在游戏中,将其建模为具有“固定”属性的两种不同材料,而不是根据上下文将其建模为具有“可变”属性的一种材料更为容易。
Sebastien Diot

单身汉是一个愚蠢的主意。
DeadMG 2011年

@DeadMG好吧。它们不是真正的Java单例。我只是说没有意义要创建多个实例,因为它们是不可变的并且彼此相等。实际上,我将没有任何真正的静态实例。我的“根”对象是OSGi服务。
Sebastien Diot

这个问题的答案的其他问题似乎证实了我的怀疑,事情变得有immutables复杂真的很快:programmers.stackexchange.com/questions/68058/...
塞巴斯蒂安Diot

Answers:


2

解决的办法是作弊一点。特别:

  • 创建A,但未将其对B的引用初始化(因为B还不存在)。

  • 创建B,并使其指向A。

  • 更新A指向B。此后请勿更新A或B。

这可以显式完成(在C ++中为示例):

struct List {
    int n;
    List *next;

    List(int n, List *next)
        : n(n), next(next);
};

// Return a list containing [0,1,0,1,...].
List *binary(void)
{
    List *a = new List(0, NULL);
    List *b = new List(1, a);
    a->next = b; // Evil, but necessary.
    return a;
}

或隐式(例如Haskell中的示例):

binary :: [Int]
binary = a where
    a = 0 : b
    b = 1 : a

Haskell示例使用惰性评估来实现相互依赖的不可变值的错觉。值开始为:

a = 0 : <thunk>
b = 1 : a

ab都是独立有效的正常形式。可以构造每个缺点,而无需其他变量的最终值。当评估thunk时,它将指向相同的数据b指向。

因此,如果要使两个不可变值相互指向,则必须在构造第二个值后更新第一个值,或者使用更高级别的机制来执行相同操作。


在您的特定示例中,我可以在Haskell中将其表示为:

data Material = Water {temperature :: Double}
              | Ice   {temperature :: Double}

setTemperature :: Double -> Material -> Material
setTemperature newTemp (Water _) | newTemp <= 0.0 = Ice newTemp
                                 | otherwise      = Water newTemp
setTemperature newTemp (Ice _)   | newTemp >= 1.0 = Water newTemp
                                 | otherwise      = Ice newTemp

但是,我回避了这个问题。我想象过,在一种面向对象的方法中,将一个setTemperature方法附加到每个Material构造函数的结果中,您将必须使构造函数指向彼此。如果将构造函数视为不可变值,则可以使用上面概述的方法。


(假设我理解Haskell的语法)我认为我当前的解决方案实际上非常相似,但是我想知道这是否是“正确的解决方案”,或者是否存在更好的解决方案。首先,我为每个(尚未创建)的对象创建一个“句柄”(引用),然后创建所有对象,为它们提供所需的句柄,最后初始化这些对象的句柄。对象本身是不可变的,但不是句柄。
Sebastien Diot

6

在您的示例中,您正在对对象应用转换,因此我将使用类似于ApplyTransform()返回a 的方法之类的方法,BlockBase而不是尝试更改当前对象。

例如,要通过加热将IceBlock更改为WaterBlock,我会称呼类似

BlockBase currentBlock = new IceBlock();
currentBlock = currentBlock.ApplyTemperature(1); 
// currentBlock is now a WaterBlock 

并且该IceBlock.ApplyTemperature()方法将如下所示:

public class IceBlock() : BlockBase
{
    public BlockBase ApplyTemperature(int temp)
    {
        return (temp > 0 ? new WaterBlock((BlockBase)this) : this);
    }
}

这是一个很好的答案,但是不幸的是,因为我没有提到我的“材料”,实际上是我的“块”,都是单身,所以不能选择新的WaterBlock()。那是不可变的主要优点,您可以无限地重用它们。我在RAM中没有500,000个块,而是有500,000个对100个块的引用。便宜得多!
Sebastien Diot

那么返回BlockList.WaterBlock而不是创建一个新的块呢?
雷切尔

是的,这就是我的工作,但是如何获得阻止名单?显然,必须在块列表之前创建块,因此,如果该块确实是不可变的,则它不能接收作为参数的块列表。那么,它是从哪儿得到清单的呢?我的一般观点是,通过使代码更加复杂,可以在一个层次上解决“鸡与蛋”问题,而在下一个层次又可以解决。基本上,我看不到基于不变性创建整个应用程序的方法。它似乎仅适用于“小对象”,而不适用于容器。
Sebastien Diot

@Sebastien我想这BlockList只是一个static负责每个块的单个实例的类,因此您无需创建BlockList(我习惯于C#)的实例
Rachel

@Sebastien:如果您使用Singeltons,则需要支付费用。
DeadMG 2011年

6

打破循环的另一种方法是用某种组合语言将物质和trans变的关注点分开:

water = new Block("water");
ice = new Block("ice");

transitions = new Transitions([
    new transitions.temperature.Below(0.0, water, ice),
    new transitions.temperature.Above(0.0, ice, water),
]);

呵呵,一开始我很难读懂这篇文章,但是我认为这基本上是我提倡的相同方法。
艾丹·库利(Aidan Cully)

1

如果您要使用功能性语言,并且想实现不变性的好处,那么您应该牢记这一点。您试图定义一个可以支持一定温度范围的“冰”或“水”对象类型-为了支持不变性,则每次温度变化时都需要创建一个新对象,这很浪费。因此,请尝试使块类型和温度的概念更独立。我不知道Scala(它在我的学习列表上:-)),但是从Haskell的Joey Adams Answer借来的东西,我建议如下:

data Material = Water | Ice

blockForTemperature :: Double -> Material
blockForTemperature x = 
  if x < 0 then Ice else Water

或许:

transitionForTemperature :: Material -> Double -> Material
transitionForTemperature oldMaterial newTemp = 
  case (oldMaterial, newTemp) of
    (Ice, _) | newTemp > 0 -> Water
    (Water, _) | newTemp <= 0 -> Ice

(注意:我没有尝试运行它,我的Haskell有点生锈。)现在,转换逻辑与材料类型分离,因此不会浪费太多内存,并且(我认为)它相当更加注重功能。


我实际上并没有尝试使用“功能语言”,因为我只是不明白!我通常不会从任何平凡的函数式编程示例中保留的唯一内容是:“该死,我更聪明!” 这对任何人都有意义,这只是我的事。从我的学生时代开始,我就记得Prolog(基于逻辑),Occam(默认情况下并行运行的所有程序)甚至汇编程序都是有意义的,但是Lisp只是疯狂的东西。但是我的意思是将导致“状态更改”的代码移到“状态”之外。
塞巴斯蒂安·迪奥
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.