我有一些大对象(超过3个字段),它们可以并且应该是不变的。每次遇到这种情况时,我倾向于使用长参数列表创建构造函数可憎的对象。
感觉不对,很难使用,并且可读性受到影响。
如果字段是某种类型的集合类型(如列表),那就更糟了。一个简单的方法addSibling(S s)可以极大地简化对象的创建过程,但是使对象可变。
你们在这种情况下使用什么?
我使用的是Scala和Java,但我认为只要语言是面向对象的,问题就与语言无关。
我能想到的解决方案:
- “带有长参数列表的构造函数可憎”
- 建造者模式
我有一些大对象(超过3个字段),它们可以并且应该是不变的。每次遇到这种情况时,我倾向于使用长参数列表创建构造函数可憎的对象。
感觉不对,很难使用,并且可读性受到影响。
如果字段是某种类型的集合类型(如列表),那就更糟了。一个简单的方法addSibling(S s)可以极大地简化对象的创建过程,但是使对象可变。
你们在这种情况下使用什么?
我使用的是Scala和Java,但我认为只要语言是面向对象的,问题就与语言无关。
我能想到的解决方案:
Answers:
好吧,您希望创建后既易于阅读又不可变的对象吗?
我认为正确完成的界面会为您提供帮助。
看起来像这样(完全由示例组成):
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 );
}
Foo对象,而不是一个单独的FooFactory。
FooImpl带有8个参数的构造函数。有什么改进?
immutable,而且我会担心人们重用工厂对象,因为他们认为这是事实。我的意思是:现在只有女性FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();在哪里talls。不变的API不应发生这种情况。
在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],然后使两个人结婚。我想不出一种方法,而不必诉诸于a private var和/或private构造函数加工厂。
这里有更多其他选择:
使实现本身可变,但将其公开的接口变为可变和不可变的。这来自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
}
}
如果您的应用程序包含大量但预定义的不可变对象(例如,配置对象),则可以考虑使用Spring框架。
它有助于记住有不同种类的不变性。对于您的情况,我认为“冰棍”的不变性将非常有效:
冰棒的不可变性:我一度异想天开地将其一次写入的不可变性削弱了一点。可以想象一个对象或字段在初始化期间保持可变状态一段时间,然后永久冻结。这种不可变性对于相互循环引用的不可变对象或已序列化到磁盘并且在反序列化时需要“流通”直到完成整个反序列化过程的不可变对象特别有用,此时所有对象都可以冻结的。
因此,您可以初始化对象,然后设置某种“冻结”标志,以表明该对象不再可写。最好将突变隐藏在函数的后面,这样对于使用API的客户端来说,该函数仍然是纯函数。
clone()来派生新实例。
freeze()方法,则事情可能会变得很丑陋。
您还可以使不可变对象公开看起来像增变器(例如addSibling)但使它们返回新实例的方法。这就是不变的Scala集合所做的。
缺点是您可能会创建过多的实例。除非您不想处理部分构建的对象,否则它也仅适用于存在中间有效配置(例如,大多数情况下没有兄弟姐妹的某些节点)。
例如,没有目的地的图边不是有效的图边。
考虑四种可能性:
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),那里有很多小不变的对象,最好的模式是从旧对象中获取新闻对象。
请注意,我不是“模式粉丝俱乐部”的成员-当然,有些事情值得模仿,但是在我看来,一旦人们给他们起了名字和有趣的帽子,他们就会过着无助的生活。
我使用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:
重新设计您的类,不需要大量的参数。您可以将其职责划分为多个类别。或根据需要将参数不传递给构造函数,而仅传递给特定方法。并非总是可行的,但是当可行时,这是值得做的。