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

zbwhf8kr  于 2023-02-18  发布在  Java
关注(0)|答案(9)|浏览(163)

我有一些大的(超过3个字段)对象可以并且应该是不可变的。每次我遇到这种情况,我倾向于创建带有长参数列表的构造函数。
感觉不对,很难使用,可读性也受到影响。
如果字段是某种集合类型(如列表),情况就更糟了,简单的addSibling(S s)可以大大简化对象的创建,但会使对象变得可变。
这种情况下你们用什么
我使用Scala和Java,但我认为只要语言是面向对象的,问题就在于语言不可知论。
我能想到的解决方案:
1.“具有长参数列表的构造函数令人厌恶”
1.构建器模式

b09cbbtk

b09cbbtk1#

好吧,你想要一个更容易阅读和不可变的对象一旦创建?
我认为流畅的界面正确完成会对您有所帮助。
它看起来像这样(纯粹是编造的例子):

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

我用粗体写 “CORRECTLY DONE” 是因为大多数Java程序员错误地使用了流畅的接口,并用构建对象所必需的方法污染了他们的对象,这当然是完全错误的。
诀窍在于 * 只有build()方法真正创建了一个Foo*(因此您的Foo可以是不可变的)。
创建(),其中XXX(..) 和 * with XXX(..)* 都创建“其他东西”。
其他东西可能是一个食品工厂,这里有一个方法来做到这一点...
你的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 );
}
gfttwv5a

gfttwv5a2#

在Scala 2.8中,可以在case类中使用命名参数、默认参数以及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))
k4ymrczo

k4ymrczo3#

在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构造函数加上工厂。

cxfofazt

cxfofazt4#

下面是几个选项:

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

ecr0jaav

ecr0jaav5#

记住有different kinds of immutability是很有帮助的。对于您的情况,我认为“冰棒”不变性会非常好:
冰棒不变性:是我异想天开地称之为一次写入不变性的轻微削弱。可以想象一个对象或字段在初始化期间保持了一段时间的可变性,然后被永远“冻结”。这种不变性对于循环引用彼此的不可变对象特别有用,或者已串行化到磁盘且在反串行化时的不可变对象需要是“流动的”,直到完成整个反串行化过程为止,此时所有对象可被冻结。
所以你初始化你的对象,然后设置一个“冻结”标志,表示它不再是可写的。最好是,你把变异隐藏在一个函数后面,这样这个函数对于使用你的API的客户端来说仍然是纯粹的。

mklgxw1f

mklgxw1f6#

你也可以让不可变对象公开看起来像mutator的方法(比如addSibling),但是让它们返回一个新的示例,这就是不可变Scala集合的作用。
缺点是你可能会创建过多的示例,它也只适用于存在中间有效配置的情况(比如一些节点没有兄弟节点,这在大多数情况下是可以的),除非你不想处理部分构建的对象。
例如,还没有目标的图边不是有效的图边。

9o685dep

9o685dep7#

考虑四种可能性:

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),其中您有许多小的不可变对象,并且更可取的模式是从旧对象中获取新对象。
请注意,我不是“模式粉丝俱乐部”的成员--当然,有些东西值得效仿,但在我看来,一旦人们给他们起了名字,戴了滑稽的帽子,他们就开始了自己无益的生活。

nfzehxib

nfzehxib8#

另一个可能的选择是重构以拥有更少的可配置字段。如果一组字段(大部分)只能相互作用,则将它们聚集到自己的小的不可变对象中。这个“小”对象的构造器/构建器应该更易于管理,这个“大”对象的构造器/构建器也是如此。

sirbozc5

sirbozc59#

我使用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);`

C#从3.0开始使用对象初始化器语法(语义上等同于前面的例子)使这一点变得更加优雅:

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

备选案文3:

重新设计类,使其不需要如此大量的参数。你可以将类分解为多个类。或者不将参数传递给构造函数,而是根据需要只传递给特定的方法。虽然不总是可行的,但如果可行,就值得这样做。

相关问题