无需使用具有长参数列表的构造函数即可构建大型的不可变对象


96

我有一些大对象(超过3个字段),它们可以并且应该是不变的。每次遇到这种情况时,我倾向于使用长参数列表创建构造函数可憎的对象。

感觉不对,很难使用,并且可读性受到影响。

如果字段是某种类型的集合类型(如列表),那就更糟了。一个简单的方法addSibling(S s)可以极大地简化对象的创建过程,但是使对象可变。

你们在这种情况下使用什么?

我使用的是Scala和Java,但我认为只要语言是面向对象的,问题就与语言无关。

我能想到的解决方案:

  1. “带有长参数列表的构造函数可憎”
  2. 建造者模式

5
我认为构建器模式是对此的最好,最标准的解决方案。
扎卡里·赖特

@Zachary:您所倡导的内容仅适用于特殊形式的构建器模式,如Joshua Bloch所解释:drdobbs.com/java/208403883?pgno=2 我更喜欢使用相关的“流畅接口”一词来调用这种“生成器模式的特殊形式”(请参阅​​我的答案)。
SyntaxT3rr0r,2010年

Answers:


76

好吧,您希望创建后既易于阅读又不可变的对象吗?

我认为正确完成的界面会为您提供帮助。

看起来像这样(完全由示例组成):

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

我写了“正确完成”用粗体,因为大多数Java程序员错误地使用了流畅的接口,并用构建对象所需的方法污染了对象,这当然是完全错误的。

诀窍是只有build()方法实际上会创建一个Foo(因此Foo可能是不可变的)。

FooFactory.create(),其中XXX(..)withXXX(..)都创建“其他”。

还有别的东西可能是FooFactory,这是做到这一点的一种方法。

您的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 );
}

11
@ all:请不要抱怨“ FooImpl”中的“ Impl”后缀:该类隐藏在工厂内部,除了编写流畅接口的人之外,没有人会看到它。用户关心的只是他得到了“ Foo”。我也可以称其为“ FooImpl”“ FooPointlessNitpick”;)
SyntaxT3rr0r 2010年

5
感觉先发制人?;)过去您对此一视同仁。:)
Greg D

3
我相信常见的错误他指的是人们的“withXXX”(等)方法添加Foo对象,而不是一个单独的FooFactory
Dean Harding

3
您仍然拥有FooImpl带有8个参数的构造函数。有什么改进?
Mot

4
我不会调用此代码immutable,而且我会担心人们重用工厂对象,因为他们认为这是事实。我的意思是:现在只有女性FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();在哪里talls。不变的API不应发生这种情况。
Thomas Ahle 2014年

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))

31
+1用于发明Scala语言。是的,这是对声誉系统的滥用,但是...太糟糕了...我非常喜欢Scala,我不得不这样做。:)
Malax 2010年

1
哦,伙计...我刚刚回答的问题几乎完全相同!好吧,我在一起很好。:-)我想知道我之前没有看到你的答案吗?<耸肩>
Daniel C. Sobral 2010年

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))

当然,这确实有很多问题。例如,尝试使espouseOption[Person],然后使两个人结婚。我想不出一种方法,而不必诉诸于a private var和/或private构造函数加工厂。


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
  }
}

选项2

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


3
选项1很聪明(但不太聪明),所以我喜欢它。
蒂莫西

4
我早些时候做过,但是我认为这远不是一个好的解决方案,因为对象仍然是可变的,只有变异方法是“隐藏的”。也许我对此主题太挑剔...
Malax 2010年

选项1的变体具有不可变和可变变体的单独类。与接口方法相比,这提供了更好的安全性。可以说,只要给他们一个名为Immutable的可变对象,只要将其转换为可变接口,就对API使用者说谎。单独的类方法要求方法来回转换。JodaTime API使用此模式。请参见DateTime和MutableDateTime。
Toolbear 2011年

6

它有助于记住有不同种类的不变性。对于您的情况,我认为“冰棍”的不变性将非常有效:

冰棒的不可变性:我一度异想天开地将其一次写入的不可变性削弱了一点。可以想象一个对象或字段在初始化期间保持可变状态一段时间,然后永久冻结。这种不可变性对于相互循环引用的不可变对象或已序列化到磁盘并且在反序列化时需要“流通”直到完成整个反序列化过程的不可变对象特别有用,此时所有对象都可以冻结的。

因此,您可以初始化对象,然后设置某种“冻结”标志,以表明该对象不再可写。最好将突变隐藏在函数的后面,这样对于使用API​​的客户端来说,该函数仍然是纯函数。


1
否决票?任何人都想对为什么这不是一个好的解决方案发表评论?
朱丽叶

+1。也许有人不赞成这样做,因为您暗示要使用clone()来派生新实例。
finnw

也许是因为它可能损害Java中的线程安全性:java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5
finnw 2010年

缺点是,它要求将来的开发人员谨慎处理“冻结”标志。如果稍后添加了mutator方法而忘记断言该方法未冻结,则可能会出现问题。同样,如果编写了一个新的构造函数,该构造函数应该(但不应该)调用该freeze()方法,则事情可能会变得很丑陋。
stalepretzel

5

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

缺点是您可能会创建过多的实例。除非您不想处理部分构建的对象,否则它也仅适用于存在中间有效配置(例如,大多数情况下没有兄弟姐妹的某些节点)。

例如,没有目的地的图边不是有效的图边。


所谓的缺点-创建了不必要的实例-并不是什么大问题。对象分配非常便宜,短期对象的垃圾回收也很便宜。默认情况下,当启用转义分析时,这种“中间对象”很可能是堆栈分配的,并且几乎不需要创建任何东西。
gustafc

2
@gustafc:是的。Cliff Click曾经说过一个故事,他们如何在其中一个大盒子(864个内核,768 GB RAM)上对Rich Hickey的Clojure Ant Colony模拟进行基准测试:700个并行线程在700个内核上运行,每个内核100%运行,生成20 GB以上的线程每秒的临时垃圾。GC甚至都没有汗水。
约尔格W¯¯米塔格

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");

对我来说,2、3和4中的每一个都适应不同的情况。由于OP提到的原因,第一个很难被爱,并且通常是设计经历了一些蠕变并需要进行一些重构的征兆。

当“工厂”后面没有任何状态时,我列出的(2)很好,而当有状态时,(3)是选择的设计。当我不想担心线程和同步时,我发现自己使用的是(2)而不是(3),也不需要担心在许多对象的生产上摊销一些昂贵的设置。另一方面,当实际工作进入工厂建设时(从SPI进行设置,读取配置文件等),就会调用(3)。

最后,其他人的答案提到了选项(4),那里有很多小不变的对象,最好的模式是从旧对象中获取新闻对象。

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


6
这是生成器模式(选项2)
Simon Nickerson 2010年

那不是一个发出他不可变对象的工厂(“ builder”)对象吗?
bmargulies 2010年

这种区别似乎很语义。豆如何制成?与建筑商有何不同?
卡尔2010年

您不希望将Java Bean约定包用于构建器(或用于其他许多事情)。
Tom Hawtin-大头钉

4

另一个可能的选择是重构具有较少的可配置字段。如果字段组只能(大部分)相互配合,请将它们收集到自己的小型不变对象中。该“小”对象的构造函数/构建器应该更易于管理,对此“大”对象的构造函数/构建器也将更易于管理。


1
注意:此答案的里程可能因问题,代码库和开发人员技能而异。
卡尔2010年

2

我使用C#,这是我的方法。考虑:

class Foo
{
    // private fields only to be written inside a constructor
    private readonly int i;
    private readonly string s;
    private readonly Bar b;

    // public getter properties
    public int I { get { return i; } }
    // etc.
}

选项1.带有可选参数的构造函数

public Foo(int i = 0, string s = "bla", Bar b = null)
{
    this.i = i;
    this.s = s;
    this.b = b;
}

用作例如new Foo(5, b: new Bar(whatever))。不适用于4.0之前的Java或C#版本。但仍然值得展示,因为这是一个示例,说明并非所有解决方案都与语言无关。

选项2.构造函数采用单个参数对象

public Foo(FooParameters parameters)
{
    this.i = parameters.I;
    // etc.
}

class FooParameters
{
    // public properties with automatically generated private backing fields
    public int I { get; set; }
    public string S { get; set; }
    public Bar B { get; set; }

    // All properties are public, so we don't need a full constructor.
    // For convenience, you could include some commonly used initialization
    // patterns as additional constructors.
    public FooParameters() { }
}

用法示例:

FooParameters fp = new FooParameters();
fp.I = 5;
fp.S = "bla";
fp.B = new Bar();
Foo f = new Foo(fp);`

从3.0开始的C#使用对象初始化程序语法(在语义上等同于前面的示例)使此操作更加优雅:

FooParameters fp = new FooParameters { I = 5, S = "bla", B = new Bar() };
Foo f = new Foo(fp);

选项3:
重新设计您的类,不需要大量的参数。您可以将其职责划分为多个类别。或根据需要将参数不传递给构造函数,而仅传递给特定方法。并非总是可行的,但是当可行时,这是值得做的。

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.