在不使用具有长参数列表的构造函数的情况下构建大的不可变对象

Mal*_*lax 96 java oop scala immutability

我有一些大的(超过3个字段)对象,它们可以而且应该是不可变的.每次遇到这种情况时,我倾向于使用长参数列表创建构造函数可恶.它感觉不对,难以使用,可读性受损.

如果字段是某种类似列表的集合类型则更糟糕.一个简单的方法addSibling(S s)可以简化对象的创建,但会使对象变得可变.

在这种情况下你们有什么用?我在使用Scala和Java,但我认为只要语言是面向对象的,问题就是语言不可知.

我能想到的解决方案:

  1. "具有长参数列表的构造函数可憎"
  2. Builder模式

感谢您的输入!

Syn*_*r0r 75

那么,你想要一个创建后更容易阅读和不可变的对象?

我认为一个流畅的界面正确完成会帮助你.

它看起来像这样(纯粹的例子):

final Foo immutable = FooFactory.create()
    .whereRangeConstraintsAre(100,300)
    .withColor(Color.BLUE)
    .withArea(234)
    .withInterspacing(12)
    .build();
Run Code Online (Sandbox Code Playgroud)

我用粗体写了"CORRECTLY DONE",因为大多数Java程序员错误地使用流畅的接口并用构建对象所需的方法污染他们的对象,这当然是完全错误的.

诀窍是只有build()方法实际上创建了一个Foo(因此你Foo可以是不可变的).

FooFactory.create(),其中XXX(...)withXXX(..)都创建"其他东西".

其他东西可能是一个FooFactory,这是一种方法来做到这一点....

You FooFactory看起来像这样:

// Notice the private FooFactory constructor
private FooFactory() {
}

public static FooFactory create() {
    return new FooFactory();
}

public FooFactory withColor( final Color col ) {
    this.color = color;
    return this;
}

public Foo build() {
    return new FooImpl( color, and, all, the, other, parameters, go, here );
}
Run Code Online (Sandbox Code Playgroud)

  • @ all:请不要抱怨"FooImpl"中的"Impl"后缀:这个类隐藏在工厂内,除了编写流畅界面的人之外,没有人会看到它.所有用户关心的是他得到了"Foo".我本可以称之为"FooImpl""FooPointlessNitpick";) (11认同)
  • 感觉先发制人?;)你过去一直在挑剔这件事.:) (5认同)
  • 我不会把这个代码称为"不可变的",我会害怕人们重复使用工厂对象,因为他们认为它是.我的意思是:`FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();``talls`现在只包含女性.使用不可变API不应该发生这种情况. (4认同)
  • 我相信他所指的常见错误是人们将"withXXX"(等)方法*添加到`Foo`对象,而不是单独的`FooFactory`. (3认同)
  • 你仍然有'FooImpl`构造函数有8个参数.有什么改进? (3认同)
  • @ pbh101:codeka评论说:在很多博客/ SO问题中,你看到人们通过在你想要构建的类中添加与对象构造相关的方法来做他们认为"流畅的界面".但是,是的,基本上流畅的界面是构建器模式的一种特定形式(在Joshua Bloch的文章中讨论了"特殊形式":drdobbs.com/java/208403883?pgno=2).但是现在这种结构的接受术语是"流畅的界面",而不是"构建器模式"(即使使用特定形式的构建器模式完成了流畅的界面). (2认同)

Mar*_*sky 60

在Scala 2.8中,您可以使用命名和默认参数以及copy案例类上的方法.这是一些示例代码:

case class Person(name: String, age: Int, children: List[Person] = List()) {
  def addChild(p: Person) = copy(children = p :: this.children)
}

val parent = Person(name = "Bob", age = 55)
  .addChild(Person("Lisa", 23))
  .addChild(Person("Peter", 16))
Run Code Online (Sandbox Code Playgroud)

  • +1用于发明Scala语言.是的,这是对声誉系统的滥用,但......哇......我非常喜欢斯卡拉,所以我不得不去做.:) (31认同)

Dan*_*ral 20

好吧,在Scala 2.8上考虑一下:

case class Person(name: String, 
                  married: Boolean = false, 
                  espouse: Option[String] = None, 
                  children: Set[String] = Set.empty) {
  def marriedTo(whom: String) = this.copy(married = true, espouse = Some(whom))
  def addChild(whom: String) = this.copy(children = children + whom)
}

scala> Person("Joseph").marriedTo("Mary").addChild("Jesus")
res1: Person = Person(Joseph,true,Some(Mary),Set(Jesus))
Run Code Online (Sandbox Code Playgroud)

当然,这确实有一些问题.例如,尝试制作espouseOption[Person],然后要结婚了对方两个人.如果不诉诸于private var和/或private构造函数以及工厂,我无法想出解决这个问题的方法.


Lit*_*les 11

以下是几个选项:

选项1

使实现本身可变,但将它公开的接口分离为可变和不可变.这取自Swing库设计.

public interface Foo {
  X getX();
  Y getY();
}

public interface MutableFoo extends Foo {
  void setX(X x);
  void setY(Y y);
}

public class FooImpl implements MutableFoo {...}

public SomeClassThatUsesFoo {
  public Foo makeFoo(...) {
    MutableFoo ret = new MutableFoo...
    ret.setX(...);
    ret.setY(...);
    return ret; // As Foo, not MutableFoo
  }
}
Run Code Online (Sandbox Code Playgroud)

选项2

如果您的应用程序包含大量但预定义的不可变对象集(例如,配置对象),则可以考虑使用Spring框架.

  • 我之前做过这个,但在我看来它远远不是一个好的解决方案,因为对象仍然是可变的,只有变异的方法是"隐藏的".也许我对这个问题太挑剔了...... (4认同)
  • 选项1很聪明(但不是*太*聪明),所以我喜欢它. (3认同)

Jul*_*iet 6

它有助于记住存在不同类型的不变性.对于你的情况,我认为"冰棒"不变性将非常有效:

冰棒不变性:我异想天开地称之为一次性不变性的轻微弱化.可以想象一个物体或一个在初始化过程中保持一段时间可变的物体,然后永远"冻结".这种不变性对于循环引用彼此的不可变对象特别有用,或者已经被序列化到磁盘的不可变对象,并且在反序列化时需要"流畅"直到整个反序列化过程完成,此时所有对象都可能是冻结.

所以你初始化你的对象,然后设置某种"冻结"标志,表明它不再可写.优选地,您可以隐藏函数背后的变异,因此该函数对于使用API​​的客户端仍然是纯粹的.


bma*_*ies 5

考虑四种可能性:

new Immutable(one, fish, two, fish, red, fish, blue, fish); /*1 */

params = new ImmutableParameters(); /*2 */
params.setType("fowl");
new Immutable(params);

factory = new ImmutableFactory(); /*3 */
factory.setType("fish");
factory.getInstance();

Immutable boringImmutable = new Immutable(); /* 4 */
Immutable lessBoring = boringImmutable.setType("vegetable");
Run Code Online (Sandbox Code Playgroud)

对我来说,2,3和4中的每一个都适应不同的情况.第一个是用心的去爱,由OP提到的原因,一般是遭受了一些野外,需要一些重构设计的一种症状.

我所列的(2)在"工厂"背后没有状态时是好的,而(3)是有状态时的选择设计.当我不想担心线程和同步时,我发现自己使用(2)而不是(3),并且我不需要担心在许多对象的生产中分摊一些昂贵的设置.另一方面,当实际工作进入工厂建设时(从SPI设置,读取配置文件等),会调出(3).

最后,别人的回答提到了选项(4),其中你有很多小的不可变对象,而最好的模式是从旧的获取新闻.

请注意,我不是"模式粉丝俱乐部"的成员 - 当然,有些事情值得效仿,但在我看来,一旦人们给他们起名字和有趣的帽子,他们就会有一种无益的生活.

  • 这是Builder模式(选项2) (6认同)

zig*_*tar 5

您还可以使不可变对象公开看起来像mutator的方法(如addSibling),但让它们返回一个新实例.这就是不可变的Scala集合所做的.

缺点是您可能会创建多于必要的实例.它也只适用于当不存在中间有效配置(如没有兄弟姐妹这是确定在大多数情况下,一些节点),除非你不想处理部分建成的对象.

例如,没有目的地的图形边缘不是有效的图形边缘.

  • @gustafc:是的.Cliff Click曾讲述过他们如何将Rich Hickey的Clojure Ant Colony仿真基于其中一个大盒子(864个核心,768 GB RAM)进行基准测试:700个并行线程在700个核心上运行,每个核心100%运行,产生超过20 GB的短暂的垃圾*每秒*.GC甚至没有出汗. (2认同)