使用方法链接时,我是否重用对象或创建对象?


37

使用方法链接时:

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

可能有两种方法:

  • 重用同一对象,如下所示:

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
    
  • Car在每个步骤中创建一个新的类型的对象,如下所示:

    public Car PaintedIn(Color color)
    {
        var car = new Car(this); // Clone the current object.
        car.Color = color; // Assign the values to the clone, not the original object.
        return car;
    }
    

第一个是错误的还是开发人员的个人选择?


我相信他的第一种方法可能很快导致直观/误导的代码。例:

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

// Would `specificModel` car be yellow or of neutral color? How would you guess that if
// `yellowCar` were in a separate method called somewhere else in code?

有什么想法吗?


1
这有什么错var car = new Car(Brand.Ford, 12345, Color.Silver);
詹姆斯

12
@James伸缩式构造函数,流畅的模式可以帮助区分可选参数和必需参数(如果它们是必需的构造函数args,如果不是可选的话)。流利的阅读效果也不错。
NimChimpsky

8
@NimChimpsky发生了什么事,以良好的老式(对C#)的特性,并具有所需的字段的构造-不,我爆流利的API,我是一个大风扇,但他们经常被滥用
克里斯小号

8
@ChrisS如果您依赖设置器(我来自Java),则必须使对象可变,而您可能不想这样做。使用流利语言时,您还可以获得更好的智能文本-所需的思维更少,ide几乎可以为您构造对象。
NimChimpsky

1
@NimChimpsky是的,我可以看到流利地实现Java的巨大飞跃
Chris S

Answers:


41

我将流利的api与其创建的对象分开放置到它自己的“ builder”类中。这样,如果客户端不想使用流利的api,您仍然可以手动使用它,并且不会污染域对象(遵循单一职责原则)。在这种情况下,将创建以下内容:

  • Car 这是领域对象
  • CarBuilder 拥有流畅的API

用法如下:

var car = CarBuilder.BuildCar()
    .OfBrand(Brand.Ford)
    .OfModel(12345)
    .PaintedIn(Color.Silver)
    .Build();

CarBuilder班是这样的(我在这里使用C#命名约定):

public class CarBuilder {

    private Car _car;

    /// Constructor
    public CarBuilder() {
        _car = new Car();
        SetDefaults();
    }

    private void SetDefaults() {
        this.OfBrand(Brand.Ford);
          // you can continue the chaining for 
          // other default values
    }

    /// Starts an instance of the car builder to 
    /// build a new car with default values.
    public static CarBuilder BuildCar() {
        return new CarBuilder();
    }

    /// Sets the brand
    public CarBuilder OfBrand(Brand brand) {
        _car.SetBrand(brand);
        return this;
    }

    // continue with OfModel(...), PaintedIn(...), and so on...
    // that returns "this" to allow method chaining

    /// Returns the built car
    public Car Build() {
        return _car;
    }

}

请注意,此类并非线程安全的(每个线程将需要它自己的CarBuilder实例)。还应注意,即使流利的api是一个非常酷的概念,但对于创建简单的域对象而言,它可能是过大的。

如果您要为更抽象的内容创建API,并且设置和执行更为复杂,那么这笔交易会更有用,这就是为什么它在单元测试和DI框架中表现出色的原因。您可以在Wikipedia Fluent接口文章的Java部分下看到一些其他示例,这些示例具有持久性,日期处理和模拟对象。


编辑:

从评论中注意到;您可以将Builder类设为静态内部类(在Car内部),并且Car可以不可变。让Car一成不变的这个例子似乎有点愚蠢。但是在更复杂的系统中,您绝对不想更改所构建对象的内容,那么您可能想要这样做。

下面是一个示例,说明如何同时执行静态内部类以及如何处理其构建的不可变对象的创建:

// the class that represents the immutable object
public class ImmutableWriter {

    // immutable variables
    private int _times; private string _write;

    // the "complex" constructor
    public ImmutableWriter(int times, string write) {
        _times = times;
        _write = write;
    }

    public void Perform() {
        for (int i = 0; i < _times; i++) Console.Write(_write + " ");
    }

    // static inner builder of the immutable object
    protected static class ImmutableWriterBuilder {

        // the variables needed to construct the immutable object
        private int _ii = 0; private string _is = String.Empty;

        public void Times(int i) { _ii = i; }

        public void Write(string s) { _is = s; }

        // The stuff is all built here
        public ImmutableWriter Build() {
            return new ImmutableWriter(_ii, _is);
        }

    }

    // factory method to get the builder
    public static ImmutableWriterBuilder GetBuilder() {
        return new ImmutableWriterBuilder();
    }
}

用法如下:

var writer = ImmutableWriter
                .GetBuilder()
                .Write("peanut butter jelly time")
                .Times(2)
                .Build();

writer.Perform();
// console writes: peanut butter jelly time peanut butter jelly time 

编辑2:皮特(Pete)在评论中发表了一篇博客文章,内容涉及在使用复杂域对象编写单元测试的上下文中使用具有lambda函数的构建器。这是使构建者更具表现力的一种有趣的替代方法。

在这种情况下,CarBuilder您需要使用以下方法:

public static Car Build(Action<CarBuilder> buildAction = null) {
    var carBuilder = new CarBuilder();
    if (buildAction != null) buildAction(carBuilder);
    return carBuilder._car;
}

可以这样使用:

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);

3
@Baqueta概述了乔什·布洛赫(Josh bloch)的有效Java
NimChimpsky

6
@Baqueta需要阅读Java开发人员imho。
NimChimpsky

3
恕我直言,一个巨大的优势是,您可以使用此模式(如果进行了适当的修改)来防止未完成的在建对象实例逃逸到构建器中。例如,您可以确保不会有颜色不确定的汽车。
Scarridge

1
嗯...我一直都称构建器模式build()(或Build())的最终方法,而不是它构建的类型的名称(Car()在您的示例中)。另外,如果Car是一个真正不可变的对象(例如,其所有字段均为readonly),则即使构建器也无法对其进行突变,因此该Build()方法将负责构造新实例。一种实现方法是Car只有一个构造函数,该构造函数以Builder为参数。然后该Build()方法就可以了return new Car(this);
丹尼尔Pryden

1
我在博客中谈到了一种基于lambda创建构建器的不同方法。该帖子可能确实需要一些编辑。我的上下文主要是在单元测试范围内,但是如果适用,它也可以应用于其他领域。可以在这里找到:petesdotnet.blogspot.com/2012/05/…–
Pete

9

那要看。

您的Car是实体还是Value对象?如果汽车是实体,则对象标识很重要,因此您应该返回相同的引用。如果对象是值对象,则它应该是不可变的,这意味着唯一的方法是每次都返回一个新实例。

后者的一个示例是.NET中的DateTime类,它是一个值对象。

var date1 = new DateTime(2012,1,1);
var date2 = date1.AddDays(1);
// date2 now refers to Jan 2., while date1 remains unchanged at Jan 1.

但是,如果模型是实体,我喜欢Spoike关于使用构建器类来构建对象的答案。换句话说,如果Car是值对象,那么您给出的示例仅对IMHO有意义。


1
为“实体”与“价值”问题+1。这是一个问题,您的类是可变类型还是不可变类型(是否应该更改此对象?),并且完全取决于您,尽管这会影响您的设计。我通常不希望方法链接可以在可变类型上工作,除非该方法返回了一个新对象。
Casey Kuball 2012年

6

创建一个单独的静态内部生成器。

使用普通的构造函数参数作为必需参数。并且流利的api是可选的。

设置颜色时不要创建新对象,除非您重命名方法NewCarInColour或类似名称。

我会根据需要做一些这样的事情,并选择其他品牌(这是java,但您的代码看起来像javascript,但可以肯定的是,它们可以互换,但可以选择):

Car yellowMercedes = new Car.Builder(Brand.MercedesBenz).PaintedIn(Color.Yellow).create();

Car specificYellowModel =new Car.Builder(Brand.MercedesBenz).WithModel(99).PaintedIn(Color.Yellow).create();

4

最重要的是,无论您选择哪种决定,方法名称和/或注释中均应明确说明。

没有标准,有时该方法将返回一个新对象(大多数String方法都这样做),或者出于链接目的或提高内存效率而返回此对象)。

我曾经设计一个3D Vector对象,并且为每个数学运算都实现了这两种方法。即时缩放方法:

Vector3D scaleLocal(float factor){
    this.x *= factor; 
    this.y *= factor; 
    this.z *= factor; 
    return this;
}

Vector3D scale(float factor){
    Vector3D that = new Vector3D(this); // clone this vector
    return that.scaleLocal(factor);
}

3
+1。很好的一点。我真的不明白为什么这会引起反对。但是,我要注意的是,您选择的名称不是很清楚。我称它们scale为“变种器”和scaledBy“生成器”。
back2dos 2012年

好一点,名字可能更清楚了。命名遵循我在库中使用的其他数学类的约定。为了避免混淆,该方法的javadoc注释中也说明了这种效果。
XGouchet,2012年

3

我在这里看到一些我可能会感到困惑的问题...问题中的第一行:

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

您正在调用一个构造函数(新)和一个create方法... create()方法几乎总是一个静态方法或一个builder方法,并且编译器应将其捕获为警告或错误,以使您知道的方式,这种语法是错误的或具有一些可怕的名称。但是稍后,您不会同时使用两者,因此让我们来看一下。

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

再次使用create,而不是使用新的构造函数。问题是,我认为您正在寻找的是copy()方法。因此,如果是这样,并且只是一个不好的名字,让我们看一件事...您将其称为mercedes.Paintedin(Color.Yellow).Copy()-应该很容易看清楚并告诉它正在被“涂漆”复制之前-对我来说,这只是正常的逻辑流程。因此,将副本放在第一位。

var yellowCar = mercedes.Copy().PaintedIn(Color.Yellow)

对我来说,很容易看到您正在绘制副本,从而制作出黄色的汽车。


+1指出new和Create()之间的不协调;
约书亚·德雷克

1

第一种方法确实具有您提到的缺点,但是只要您在文档中明确指出,任何半能干的编码器都不会出现问题。我亲自使用过的所有方法链代码都以这种方式工作。

第二种方法显然具有工作量大的缺点。您还必须决定返回的副本是浅层副本还是深层副本:最好的复制可能因类而异,或因方法而异,因此您要么引入不一致,要么损害最佳行为。值得注意的是,这是不可变对象(如字符串)的唯一选择。

无论您做什么,都不要在同一个班级内混搭!



0

这是上述方法的变体。不同之处在于Car类上的静态方法与Builder上的方法名称匹配,因此您无需显式创建Builder:

Car car = Car.builder().ofBrand(Brand.Ford).ofColor("Green")...

您可以使用在链接的构建器调用中使用的相同方法名称:

Car car = Car.ofBrand(Brand.Ford).ofColor("Green")...

另外,类上有一个.copy()方法,该方法返回一个使用当前实例中所有值填充的构建器,因此您可以在主题上创建一个变体:

Car red = car.copy().paintedIn("Red").build();

最后,构建器的.build()方法检查是否已提供所有必需的值,如果缺少则抛出该异常。可能更需要在构建器的构造函数上要求一些值,而让其余值是可选的。在这种情况下,您需要其他答案中的一种模式。

public enum Brand {
    Ford, Chrysler, GM, Honda, Toyota, Mercedes, BMW, Lexis, Tesla;
}

public class Car {
    private final Brand brand;
    private final int model;
    private final String color;

    public Car(Brand brand, int model, String color) {
        this.brand = brand;
        this.model = model;
        this.color = color;
    }

    public Brand getBrand() {
        return brand;
    }

    public int getModel() {
        return model;
    }

    public String getColor() {
        return color;
    }

    @Override public String toString() {
        return brand + " " + model + " " + color;
    }

    public Builder copy() {
        Builder builder = new Builder();
        builder.brand = brand;
        builder.model = model;
        builder.color = color;
        return builder;
    }

    public static Builder ofBrand(Brand brand) {
        Builder builder = new Builder();
        builder.brand = brand;
        return builder;
    }

    public static Builder ofModel(int model) {
        Builder builder = new Builder();
        builder.model = model;
        return builder;
    }

    public static Builder paintedIn(String color) {
        Builder builder = new Builder();
        builder.color = color;
        return builder;
    }

    public static class Builder {
        private Brand brand = null;
        private Integer model = null;
        private String color = null;

        public Builder ofBrand(Brand brand) {
            this.brand = brand;
            return this;
        }

        public Builder ofModel(int model) {
            this.model = model;
            return this;
        }

        public Builder paintedIn(String color) {
            this.color = color;
            return this;
        }

        public Car build() {
            if (brand == null) throw new IllegalArgumentException("no brand");
            if (model == null) throw new IllegalArgumentException("no model");
            if (color == null) throw new IllegalArgumentException("no color");
            return new Car(brand, model, color);
        }
    }
}
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.