【问题标题】:Design of immutable and mutable objects in JavaJava中不可变和可变对象的设计
【发布时间】:2011-04-16 21:19:48
【问题描述】:

我的问题与 API 设计有关。

假设我正在设计一个向量(数学/物理含义)。我想要一个不可变的实现和一个可变的实现。

然后我的向量看起来像这样:

public interface Vector {
  public float getX(); public float getY();
  public X add(Vector v);
  public X subtract(Vector v);
  public X multiply(Vector v);
  public float length();
}

我想知道如何确保同时拥有可变和不可变的实现。我不太喜欢 java.util.List 的方法(默认允许可变性)和 Guava 的不可变实现所具有的 UnsupportedOperationException()。

如何设计一个“完美”的接口或抽象类 Vector 与这两种实现?

我想过这样的事情:

public interface Vector {
  ...
  public Vector add(Vector v);
  ...
}
public final class ImmutableVector implements Vector {
  ...
  public ImmutableVector add(Vector v) {
    return new ImmutableVector(this.x+v.getX(), this.y+v.getY());
  }
  ...
}
public class MutableVector implements Vector {
  ...
  public MutableVector add(Vector v) {
    this.x += v.getX();
    this.y += v.getY();
    return this;
  }
  ...
}

总而言之,我想检查一下这种方法是否存在明显的设计缺陷,它们是什么,我应该怎么做才能修复这些?


注意:“向量”是一个更一般用例的示例。为了我的问题,我本可以选择重写 List 接口或其他任何东西。请关注更一般的用例。


最终选择,在下面的答案之后,基于 Joda-time,正如有人解释但现在已编辑:

/** Basic class, allowing read-only access. */
public abstract class ReadableVector {
  public abstract float getX(); public abstract float getY();
  public final float length() {
    return Vectors.length(this);
  }
  // equals(Object), toString(), hashCode(), toImmutableVectors(), mutableCopy()
}
/** ImmutableVector, not modifiable implementation */
public final class ImmutableVector extends ReadableVector implements Serializable {
  // getters
  // guava-like builder methods (copyOf, of, etc.)
}
/** Mutable implementation */
public class Vector extends ReadableVector implements Serializable {
  // fields, getters and setters
  public void add (ReadableVector v) {/* delegate to Vectors */}
  public void subtract(ReadableVector v) {/* delegate to Vectors */}
  public void multiply(ReadableVector v) {/* delegate to Vectors */}
}
/** Tool class containing all the logic */
public final class Vectors {
  public static ImmutableVector add(ReadableVector v1, ReadableVector v2) {...}
  public static void addTo(Vector v1, ReadableVector v2) {...}
  ...
}

我将 Vector 从接口更改为抽象类,因为基本上矢量不应该是其他任何东西。

谢谢大家。

【问题讨论】:

  • 添加不添加而是返回新对象的方法?不理想!
  • 这叫流畅的界面,丰富。风靡一时。它确实添加了 - 再看一遍。
  • 同时拥有一个可变和不可变的接口实现会引入一个问题,如果一个对象是不可变的,而不在您的代码中回溯,您就无法依赖这个问题。如果它是一个返回对象的 3:rd 方库,这将成为一个更大的问题。您可能最终不得不将所有对象视为潜在可变的,从而使不可变实现变得多余。如果您通过向量接口的可变和不可变实现来解释您要解决的问题,那么我们可以帮助您通过更简洁的设计来解决这个问题。

标签: java class-design immutability mutable


【解决方案1】:

作为 Vector 库的用户,我不希望有一个修改当前 Object 的 add 实现和另一个返回新对象的 add 实现(相同的接口)。

最好有一组明确的不修改当前对象的方法,然后在可变向量中添加额外的方法来修改当前对象。

【讨论】:

  • 如果在接口契约中有很好的记录,“add”返回与操作结果对应的实例,并且这是应该在后续调用中使用的实例,那么它应该不是很大交易,也许“add”这个名字可能不是最合适的,但是如果有两个接口的实现,一个是不可变的,另一个是不可变的,只要它们都满足合同,这并没有什么错。确实,鲁莽的用户可能会因为错误地使用接口实例而很容易犯错,因为他们应该忘记它的类型
  • 可变性的问题更多的是我可能想重用原始(未更改的)对象进行另一个计算,而不是我必须记住使用返回值。所以至少要确保提供一个方法toImmutableVector(),它返回一个不可变的向量(在不可变的情况下可能是return this)。
  • @PAulo Eberman 不要误会我的意思,我认为你是对的。我只是想扮演魔鬼的拥护者,让争论更有趣。
  • 确实,我在原帖中没有提到它们,但是 toImmutableVector 和 toMutableVector 方法确实存在。在 SO 上发帖时,我总是尽量简化以专注于问题;也许我简化了太多。
【解决方案2】:

我不认为你的设计有什么明显的错误。我觉得它完全有效。如果我是你,我会考虑以下几点:

  • 鲁莽的用户可能会为 界面矢量思考他们 实现总是可变的。
  • 不可变性通常意味着更多的对象和性能损失,因为需要将越来越多的对象放入堆中并强制垃圾收集做更多的工作。如果您的应用程序需要执行许多“添加”操作,您可能需要付出代价。但是,嘿,这就是拥有可变版本的全部目的,对吧?
  • 另外,如果您正在为多线程环境编写代码,那么当您不确定实现时,您仍然需要同步对 Vector 类型的共享变量的访问,尤其是如果您想确保可以切换实现而不产生任何后果。这再次证明,在编写代码时忽略实现细节是很困难的。
  • 虽然我在其他帖子中与@Paulo Eberman 发生了一些争论,但我相信他是完全正确的。我认为最好有两个独立的接口,一个用于不可变对象,一个用于可变对象(可以扩展后者)。

当然,这些观点大部分都是有争议的,这些只是我的观点。

【讨论】:

    【解决方案3】:

    您的想法很好,但并不完美。

    您遗漏了泛型。

    您假设为您的 Vector 所持有的类型定义了诸如加法和减法之类的算术运算,这可能不是真的。 (泛型可能对此有所帮助。)

    我不知道不可变向量在数学和物理方面有多大用处。

    一个完美的 API 应该有一个类似的 Matrix 类,因为您需要为数学和物理做线性代数。

    我想看看 Apache 的通用数学库以获得灵感。它是 JAMA 的继承人。我发现查看我的优秀者的成功设计和实现是一种很好的学习方式。

    【讨论】:

    • 这里的向量是一个例子。我可以选择重写 List 接口。请专注于在没有 UnsupportedOperationException 的情况下处理 Immutable/Mutable 对象的方式。 -- 在这种情况下,泛型如何帮助我?我想将一个向量添加到另一个。我不在乎参数是否可变。我真的看不出泛型在哪里可以帮助我......
    【解决方案4】:

    我觉得这个设计不是很好。拥有可变算术对象is not good,即使您将它们明确标记为可变。此外,我不会将向量操作放在类向量中。因为现在你只有加法和乘法,明天你会想要别的东西,你的类会随着你添加这个或什么向量运算而增长和增长。如果我是你,我会创建一个像这样的不可变向量

    public class Vector {
    
        private Double X;
        private Double Y;
    
        public Vector(Double x, Double y) {
            X = x;
            Y = y;
        }
    
        public Double getX() {
            return X;
        }
    
        public Double getY() {
            return Y;
        }
    }
    

    然后我会创建一个类来执行基本的向量操作:

    public class BaseVectorAlgebra {
    
        public static Vector add(Vector arg1, Vector arg2) {
            return new Vector(arg1.getX() + arg2.getX(), arg1.getY() + arg2.getY());
        }
    
    }
    

    通过这种方式,您将有一种简单的方法来扩展系统,而无需触及现有的类,也不会引入可变性,这只会使事情复杂化。

    更新:

    如果您仍然想使用可变向量,那么我会将 SetX 和 SetY 设置器添加到 Vector 类中,但将可变性决策放入 BaseVectorAlgebra 中,如下所示:

    public static Vector addInto(Vector arg1, Vector arg2) {
        arg1.setX(arg1.getX() + arg2.getX());
        arg1.setY(arg1.getY() + arg2.getY());
    
        return arg1;
    }
    

    但我真的不喜欢这里的可变性,因为它引入了不必要的复杂性

    【讨论】:

    • 我也不太喜欢可变向量。但问题是,对于内存/性能问题,可能需要它们。在这种情况下,我希望有一个强大的 API 良好的性能。因此问题。
    • @ogregoire 好吧,然后尝试考虑我的最后一个示例。 Vector不应该知道任何关于它的数学。这是我的看法;)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多