构build大的,不可变的对象,而不使用具有长参数列表的构造函数

我有一些大的(超过3个领域)对象,可以而且应该是不可变的。 每当我遇到这种情况下,我倾向于用长参数列表创build构造函数可憎。 它感觉不对,难以使用和可读性受损。

如果这些字段是某种types的收集types,比如列表,则更为糟糕。 一个简单的addSibling(S s)将会很容易地减less对象的创build,但会使对象addSibling(S s)可变的。

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

我能想到的解决scheme:

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

感谢您的input!

那么,你想同时创build一个更容易阅读和不可变的对象?

我认为一个stream畅的界面正确完成将帮助你。

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

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

我用粗体写了“CORRECTLY DONE” ,因为大多数Java程序员都会遇到stream畅的接口错误,并且用构build对象所必需的方法来污染对象,这当然是完全错误的。

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

FooFactory.create() ,其中XXX(..)withXXX(..)都创build“别的东西”。

其他的东西可能是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 ); } 

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

那么,考虑一下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)) 

当然,这确实有一些问题。 比如,试着做出“ espouse和“ Option[Person] ,然后让两个人结婚。 我不能想办法解决这个问题,而不是采用private var和/或private构造函数加上工厂。

这里有几个选项:

选项1

使实现本身是可变的,但将它公开的接口分开为可变和不可变。 这是来自Swing库devise。

 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

如果您的应用程序包含一个大的但是预先定义的不可变对象集(例如,configuration对象),那么您可以考虑使用Spring框架。

你也可以使不可变的对象暴露看起来像增加的方法(像addSibling),但让他们返回一个新的实例。 这就是不可变的Scalacollections。

缺点是你可能会创build更多的实例。 这也适用于存在中间有效configuration的情况(比如某些不含兄弟姐妹的节点在大多数情况下都可以),除非您不想处理部分构build的对象。

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

考虑四种可能性:

 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引用的原因,通常是一个devise的症状,已经经历了一些蠕变,需要一些重构。

(2)列出的是“工厂”后面没有状态的好事情,而(3)是有状态时的selectdevise。 当我不想担心线程和同步时,我发现自己使用(2)而不是(3),并且我不必担心在生成许多对象时分摊一些昂贵的设置。 (3)另一方面,当实际工作进入工厂build设(从SPIbuild立,读取configuration文件等)时,会被提出。

最后,别人的回答提到了选项(4),在那里你有许多小的不变的对象,最好的模式是从旧的对象中得到新闻。

请注意,我不是“模式粉丝俱乐部”的成员 – 当然,有些事情值得效仿,但在我看来,一旦人们给他们的名字和有趣的帽子,他们将自己的生活没有帮助。

它有助于记住有不同种类的不变性 。 对于你的情况,我认为“冰棒”不变性将工作得很好:

冰棒不变性:我奇怪地称为一次写入不变性略微减弱。 人们可以想象一个物体或者场在初始化过程中保持一段时间可变,然后永远被“冻结”。 这种不变性对于循环引用的不可变对象或已经序列化到磁盘的不可变对象是特别有用的,在反序列化之后需要“stream动”,直到完成整个反序列化过程,此时所有的对象可能是冻结。

所以你初始化你的对象,然后设置一个“冻结”标志,表明它不再可写。 最好,你会隐藏在一个函数后面的突变,所以这个函数对于使用你的API的客户端来说仍然是纯粹的。

另一个可能的select是重构具有较less的可configuration字段。 如果一组领域只相互工作(大部分),把他们收集到自己的小的不变的对象。 这个“小”对象的构造函数/构build器应该更易于pipe理,这个“大”对象的构造函数/构build器也是如此。

我使用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#版本。 但仍值得展示,因为这是一个例子,并非所有解决scheme都是语言不可知的。

选项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:
重新devise你的class级不需要这么多的参数。 你可以把它的可撤销性分成多个类。 或者将parameter passing给构造函数,但只传递给特定方法。 不总是可行的,但是当它是时候,这是值得的。