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

发布于 2024-09-01 15:35:39 字数 340 浏览 8 评论 0原文

我有一些大的(超过 3 个字段)对象,它们可以而且应该是不可变的。每次遇到这种情况,我都会创建带有长参数列表的令人厌恶的构造函数。

感觉不太对劲,很难使用,而且可读性也受到影响。

如果字段是某种集合类型(例如列表),情况会更糟。一个简单的 addSibling(S s) 可以大大简化对象的创建,但会使对象变得可变。

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

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

我能想到的解决方案:

  1. “带有长参数列表的构造函数令人厌恶”
  2. 构建器模式

I have some big (more than 3 fields) objects that can and should be immutable. Every time I run into that case I tend to create constructor abominations with long parameter lists.

It doesn't feel right, it is hard to use, and readability suffers.

It is even worse if the fields are some sort of collection type like lists. A simple addSibling(S s) would ease the object creation so much but renders the object mutable.

What do you guys use in such cases?

I'm on Scala and Java, but I think the problem is language agnostic as long as the language is object oriented.

Solutions I can think of:

  1. "Constructor abominations with long parameter lists"
  2. The Builder Pattern

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

著墨染雨君画夕 2024-09-08 15:35:39

那么,您想要一个更易于阅读且创建后不可变的对象吗?

我认为流畅的界面正确完成会对您有所帮助。

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

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

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

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

FooFactory.create()whereXXX(..)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 );
}

Well, you want both an easier to read and immutable object once created?

I think a fluent interface CORRECTLY DONE would help you.

It would look like this (purely made up example):

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

I wrote "CORRECTLY DONE" in bold because most Java programmers get fluent interfaces wrong and pollute their object with the method necessary to build the object, which is of course completely wrong.

The trick is that only the build() method actually creates a Foo (hence you Foo can be immutable).

FooFactory.create(), whereXXX(..) and withXXX(..) all create "something else".

That something else may be a FooFactory, here's one way to do it....

You FooFactory would look like this:

// 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 );
}
我的影子我的梦 2024-09-08 15:35:39

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

In Scala 2.8, you could use named and default parameters as well as the copy method on a case class. Here's some example code:

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))
夏末 2024-09-08 15:35:39

好吧,考虑一下 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],然后让两个人结婚。我想不出一种方法来解决这个问题,而不求助于 private var 和/或 private 构造函数加上工厂。

Well, consider this on 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))

This does have its share of problems, of course. For instance, try making espouse and Option[Person], and then getting two persons married to each other. I can't think of a way to solve that without resorting to either a private var and/or a private constructor plus a factory.

风渺 2024-09-08 15:35:39

这里还有几个选项:

选项 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 框架。

Here are a couple of more options:

Option 1

Make the implementation itself mutable, but separate the interfaces that it exposes to mutable and immutable. This is taken from the Swing library design.

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

Option 2

If your application contains a large but pre-defined set of immutable objects (e.g., configuration objects), you might consider using the Spring framework.

戒ㄋ 2024-09-08 15:35:39

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

冰棒不变性:就是我
异想天开地称之为轻微削弱
一次写入的不变性。一个可以
想象一个物体或一个场
暂时保持不稳定
在其初始化期间,然后
被永远“冻结”了。这种
不变性特别有用
对于循环的不可变对象
互相引用,或者不可变
已序列化的对象
磁盘并在反序列化时需要
保持“流动”,直到整个
反序列化过程完成,在
所有物体可能在哪个点
冻结。

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

It helps to remember there are different kinds of immutability. For your case, I think "popsicle" immutability will work really well:

Popsicle immutability: is what I
whimsically call a slight weakening of
write-once immutability. One could
imagine an object or a field which
remained mutable for a little while
during its initialization, and then
got “frozen” forever. This kind of
immutability is particularly useful
for immutable objects which circularly
reference each other, or immutable
objects which have been serialized to
disk and upon deserialization need to
be “fluid” until the entire
deserialization process is done, at
which point all the objects may be
frozen.

So you initialize your object, then set a "freeze" flag of some sort indicating that its no longer writable. Preferably, you'd hide the mutation behind a function so the function is still pure to clients consuming your API.

不弃不离 2024-09-08 15:35:39

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

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

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

You could also make the immutable objects expose methods that look like mutators (like addSibling) but let them return a new instance. That's what the immutable Scala collections do.

The downside is that you might create more instances than necessary. It's also only applicable when there exist intermediate valid configurations (like some node without siblings which is ok in most cases) unless you don't want to deal with partially built objects.

For example a graph edge which has no destination yet isn't a valid graph edge.

鼻尖触碰 2024-09-08 15:35:39

考虑四种可能性:

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),其中有很多不可变的小对象,更好的模式是从旧对象中获取新对象。

请注意,我不是“模式粉丝俱乐部”的成员——当然,有些东西值得效仿,但在我看来,一旦人们给它们起了名字和有趣的帽子,它们就会过上无益的生活。

Consider four possibilities:

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

To me, each of 2, 3, and 4 is adapted to a difference situation. The first one is hard to love, for the reasons cited by the OP, and is generally a symptom of a design that has suffered some creep and needs some refactoring.

What I'm listing as (2) is good when there is no state behind the 'factory', whereas (3) is the design of choice when there is state. I find myself using (2) rather than (3) when I don't want to worry about threads and synchronization, and I don't need to worry about amortizing some expensive setup over the production of many objects. (3), on the other hand, is called forth when real work goes into the construction of the factory (setting up from an SPI, reading configuration files, etc).

Finally, someone else's answer mentioned option (4), where you have lots of little immutable objects and the preferable pattern is to get news ones from old ones.

Note that I'm not a member of the 'pattern fan club' -- sure, some things are worth emulating, but it seems to me that they take on an unhelpful life of their own once people give them names and funny hats.

书信已泛黄 2024-09-08 15:35:39

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

Another potential option is to refactor to have fewer configurable fields. If groups of fields only work (mostly) with each other, gather them up into their own small immutable object. That "small" object's constructors/builders should be more manageable, as will the constructor/builder for this "big" object.

浮萍、无处依 2024-09-08 15:35:39

我使用 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:
重新设计您的类,使其不需要如此大量的参数。您可以将其职责分为多个类。或者根据需要,不将参数传递给构造函数,而仅传递给特定方法。并不总是可行,但当可行时,就值得去做。

I use C#, and these are my approaches. Consider:

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

Option 1. Constructor with optional parameters

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

Used as e.g. new Foo(5, b: new Bar(whatever)). Not for Java or C# versions before 4.0. but still worth showing, as it's an example how not all solutions are language agnostic.

Option 2. Constructor taking a single parameter object

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() { }
}

Usage example:

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

C# from 3.0 on makes this more elegant with object initializer syntax (semantically equivalent to the previous example):

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

Option 3:
Redesign your class not to need such a huge number of parameters. You could split its repsonsibilities into multiple classes. Or pass parameters not to the constructor but only to specific methods, on demand. Not always viable, but when it is, it's worth doing.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文