Khi sử dụng phương thức xích, tôi có sử dụng lại đối tượng hoặc tạo một đối tượng không?


37

Khi sử dụng phương thức xích như:

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

có thể có hai cách tiếp cận:

  • Sử dụng lại cùng một đối tượng, như thế này:

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
  • Tạo một đối tượng kiểu mới Carở mỗi bước, như thế này:

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

Là cái đầu tiên sai hay đó là một lựa chọn cá nhân của nhà phát triển?


Tôi tin rằng cách tiếp cận đầu tiên của anh ta có thể nhanh chóng gây ra mã trực quan / gây hiểu lầm. Thí dụ:

// 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?

Có suy nghĩ gì không?


1
Có chuyện gì với bạn var car = new Car(Brand.Ford, 12345, Color.Silver);vậy?
James

12
Nhà xây dựng kính thiên văn @James, mẫu thông thạo có thể giúp phân biệt giữa các tham số tùy chọn và bắt buộc (nếu chúng là đối số của hàm tạo, nếu không phải là tùy chọn). Và thông thạo là khá tốt để đọc.
NimChimpsky

8
@NimChimpsky điều gì đã xảy ra với các thuộc tính lỗi thời (đối với C #) và một công cụ xây dựng có các trường bắt buộc - không phải là tôi đang làm nổ API trôi chảy, tôi là một fan hâm mộ lớn nhưng họ thường bị lạm dụng
Chris S

8
@ChrisS nếu bạn dựa vào setters (tôi đến từ java), bạn phải làm cho các đối tượng của mình có thể thay đổi, điều mà bạn có thể không muốn làm. Và bạn cũng nhận được intocatext đẹp hơn khi sử dụng thành thạo - đòi hỏi ít suy nghĩ hơn, ide gần như xây dựng đối tượng của bạn cho bạn.
NimChimpsky

1
@NimChimpsky yeh Tôi có thể thấy sự thông thạo là một bước tiến lớn đối với Java
Chris S

Câu trả lời:


41

Tôi đã đặt api trôi chảy vào lớp "người xây dựng" riêng biệt với đối tượng mà nó đang tạo. Theo cách đó, nếu khách hàng không muốn sử dụng api trôi chảy, bạn vẫn có thể sử dụng thủ công và nó không gây ô nhiễm đối tượng miền (tuân thủ nguyên tắc trách nhiệm duy nhất). Trong trường hợp này, những điều sau đây sẽ được tạo ra:

  • Car đó là đối tượng miền
  • CarBuilder chứa API thông thạo

Việc sử dụng sẽ như thế này:

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

Các CarBuilderlớp học sẽ giống như thế này (tôi đang sử dụng C # ước đặt tên ở đây):

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

}

Lưu ý rằng lớp này sẽ không an toàn cho luồng (mỗi luồng sẽ cần đối tượng CarBuilder của riêng nó). Cũng lưu ý rằng, mặc dù api trôi chảy là một khái niệm thực sự tuyệt vời, nhưng nó có thể là quá mức cho mục đích tạo ra các đối tượng miền đơn giản.

Thỏa thuận này hữu ích hơn nếu bạn đang tạo API cho một thứ gì đó trừu tượng hơn nhiều và có thiết lập và thực thi phức tạp hơn, đó là lý do tại sao nó hoạt động tuyệt vời trong thử nghiệm đơn vị và khung DI. Bạn có thể xem một số ví dụ khác trong phần Java của bài viết Giao diện thông thạo wikipedia với các đối tượng kiên trì, xử lý ngày và giả.


CHỈNH SỬA:

Theo ghi nhận từ các ý kiến; bạn có thể làm cho lớp Builder trở thành một lớp bên trong tĩnh (bên trong Xe) và Xe có thể được tạo thành bất biến. Ví dụ về việc để cho Car là bất biến có vẻ hơi ngớ ngẩn; nhưng trong một hệ thống phức tạp hơn, nơi bạn hoàn toàn không muốn thay đổi nội dung của đối tượng được xây dựng, bạn có thể muốn làm điều đó.

Dưới đây là một ví dụ về cách thực hiện cả lớp bên trong tĩnh và cách xử lý việc tạo đối tượng bất biến mà nó tạo ra:

// 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();
    }
}

Cách sử dụng sẽ như sau:

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

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

Chỉnh sửa 2: Pete trong các bình luận đã tạo một bài đăng trên blog về việc sử dụng các trình xây dựng với các hàm lambda trong bối cảnh viết các bài kiểm tra đơn vị với các đối tượng miền phức tạp. Đó là một thay thế thú vị để làm cho người xây dựng biểu cảm hơn một chút.

Trong trường hợp CarBuilderbạn cần phải có phương pháp này thay thế:

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

Mà có thể được sử dụng như thế này:

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

3
@Baqueta đây là phác thảo java hiệu quả của josh bloch
NimChimpsky

6
@Baqueta yêu cầu đọc cho java dev, imho.
NimChimpsky

3
IMHO một lợi thế rất lớn là, bạn có thể sử dụng mẫu này (nếu được sửa đổi một cách thích hợp) để ngăn các trường hợp của đối tượng đang xây dựng không hoàn thành thoát khỏi trình xây dựng. Ví dụ: Bạn có thể đảm bảo rằng sẽ không có Xe nào có Màu không xác định.
Scarfridge

1
Hmm ... Tôi đã luôn gọi phương thức cuối cùng của mẫu trình tạo build()(hoặc Build()), không phải tên của kiểu mà nó xây dựng ( Car()trong ví dụ của bạn). Ngoài ra, nếu Carlà một đối tượng thực sự bất biến (ví dụ: tất cả các trường của nó là readonly), thì ngay cả người xây dựng cũng không thể thay đổi nó, vì vậy Build()phương thức này chịu trách nhiệm xây dựng thể hiện mới. Một cách để làm điều này là Carchỉ có một hàm tạo duy nhất, lấy một Builder làm đối số của nó; sau đó Build()phương pháp có thể chỉ return new Car(this);.
Daniel Pryden

1
Tôi đã viết blog về một cách tiếp cận khác nhau để tạo các nhà xây dựng dựa trên lambdas. Bài viết có lẽ cần một chút chỉnh sửa. Bối cảnh của tôi chủ yếu là trong phạm vi của một bài kiểm tra đơn vị, nhưng nó cũng có thể được áp dụng cho các lĩnh vực khác nếu có thể áp dụng. Nó có thể được tìm thấy ở đây: petesdotnet.blogspot.com/2012/05/ Khăn
Pete

9

Mà phụ thuộc.

Xe của bạn là một thực thể hay một đối tượng Giá trị ? Nếu chiếc xe là một thực thể, thì danh tính đối tượng có tầm quan trọng, vì vậy bạn nên trả lại cùng một tham chiếu. Nếu đối tượng là một đối tượng giá trị, nó sẽ không thay đổi, có nghĩa là cách duy nhất là trả về một thể hiện mới mỗi lần.

Một ví dụ về cái sau sẽ là lớp DateTime trong .NET là một đối tượng giá trị.

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.

Tuy nhiên, nếu mô hình là một thực thể, tôi thích câu trả lời của Spoike về việc sử dụng lớp trình tạo để xây dựng đối tượng của bạn. Nói cách khác, ví dụ mà bạn đưa ra chỉ có ý nghĩa IMHO nếu Xe là một đối tượng giá trị.


1
+1 cho câu hỏi 'Thực thể' so với 'Giá trị'. Đó là một câu hỏi về việc liệu lớp của bạn là một loại có thể thay đổi hoặc bất biến (đối tượng này nên được thay đổi?), Và hoàn toàn tùy thuộc vào bạn, mặc dù nó sẽ ảnh hưởng đến thiết kế của bạn. Tôi thường không mong đợi phương thức chuỗi hoạt động trên một loại có thể thay đổi, trừ khi phương thức trả về một đối tượng mới.
Casey Kuball

6

Tạo một trình xây dựng bên trong tĩnh riêng biệt.

Sử dụng các đối số constructor bình thường cho các tham số cần thiết. Và thông thạo api cho tùy chọn.

Không tạo đối tượng mới khi cài đặt màu, trừ khi bạn đổi tên phương thức NewCarInColour hoặc một cái gì đó tương tự.

Tôi sẽ làm một cái gì đó như thế này với thương hiệu theo yêu cầu và phần còn lại là tùy chọn (đây là java, nhưng của bạn trông giống như javascript, nhưng khá chắc chắn rằng chúng có thể hoán đổi cho nhau với một chút chọn lọc nit):

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

Điều quan trọng nhất là bất cứ quyết định nào bạn chọn, nó đều được nêu rõ trong tên phương thức và / hoặc nhận xét.

Không có tiêu chuẩn, đôi khi phương thức sẽ trả về một đối tượng mới (hầu hết các phương thức String làm như vậy) hoặc sẽ trả về đối tượng này cho mục đích xâu chuỗi hoặc cho hiệu quả bộ nhớ).

Tôi đã từng thiết kế một đối tượng Vector 3D và cho mọi hoạt động toán học, tôi đã thực hiện cả hai phương pháp. Để ngay lập tức phương pháp tỷ lệ:

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. Điểm rất tốt. Tôi không thực sự thấy lý do tại sao điều này có một downvote. Tôi sẽ lưu ý tuy nhiên, tên bạn chọn không rõ ràng lắm. Tôi sẽ gọi họ scale(bộ biến đổi) và scaledBy(bộ tạo).
back2dos

Điểm tốt, tên có thể có rõ ràng hơn. Việc đặt tên theo một quy ước của các lớp toán học khác mà tôi đã sử dụng từ thư viện. Hiệu quả cũng được nêu trong các bình luận javadoc của phương pháp để tránh nhầm lẫn.
XGouchet

3

Tôi thấy một vài vấn đề ở đây mà tôi nghĩ có thể gây nhầm lẫn ... Dòng đầu tiên của bạn trong câu hỏi:

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

Bạn đang gọi một hàm tạo (mới) và một phương thức tạo ... Một phương thức tạo () hầu như luôn luôn là một phương thức tĩnh hoặc một phương thức xây dựng và trình biên dịch sẽ bắt nó trong một cảnh báo hoặc lỗi để cho bạn biết, hoặc cách này, cú pháp này là sai hoặc có một số tên khủng khiếp. Nhưng sau này, bạn không sử dụng cả hai, vì vậy hãy xem xét điều đó.

// 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();

Một lần nữa với việc tạo mặc dù, không phải với một hàm tạo mới. Thay vào đó, tôi nghĩ rằng bạn đang tìm kiếm một phương thức copy (). Vì vậy, nếu đó là trường hợp và đó chỉ là một cái tên nghèo nàn, hãy nhìn vào một điều ... bạn gọi mercedes.Painedin (Color.Yellow) .Copy () - Thật dễ dàng để nhìn vào đó và nói rằng nó đang được vẽ 'trước khi được sao chép - chỉ là một luồng logic thông thường, với tôi. Vì vậy, đặt bản sao đầu tiên.

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

với tôi, thật dễ dàng để thấy rằng bạn đang vẽ bản sao, làm cho chiếc xe màu vàng của bạn.


+1 để chỉ ra sự bất hòa giữa mới và Tạo ();
Joshua Drake

1

Cách tiếp cận đầu tiên có nhược điểm mà bạn đề cập, nhưng miễn là bạn làm rõ trong tài liệu, bất kỳ lập trình viên nửa có thẩm quyền nào cũng không gặp vấn đề. Tất cả các mã chuỗi phương thức mà cá nhân tôi đã làm việc đã hoạt động theo cách này.

Cách tiếp cận thứ hai rõ ràng có nhược điểm là làm việc nhiều hơn. Bạn cũng phải quyết định xem các bản sao bạn trả lại sẽ là bản sao nông hay sâu: tốt nhất có thể thay đổi từ lớp này sang lớp khác hoặc phương pháp này sang phương pháp khác, vì vậy bạn sẽ đưa ra sự không nhất quán hoặc thỏa hiệp với hành vi tốt nhất. Điều đáng chú ý là đây là tùy chọn duy nhất cho các đối tượng bất biến, như chuỗi.

Dù bạn làm gì, đừng trộn và kết hợp trong cùng một lớp!


1

Tôi thà nghĩ giống như cơ chế "Phương pháp mở rộng".

public Car PaintedIn(this Car car, Color color)
{
    car.Color = color;
    return car;
}

0

Đây là một biến thể của các phương pháp trên. Sự khác biệt là có các phương thức tĩnh trên lớp Xe khớp với tên phương thức trên Trình tạo, do đó bạn không cần phải tạo một Trình dựng rõ ràng:

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

Bạn có thể sử dụng cùng tên phương thức mà bạn sử dụng cho các lệnh gọi của trình tạo chuỗi:

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

Ngoài ra, có một phương thức .copy () trên lớp trả về một trình xây dựng được điền với tất cả các giá trị từ thể hiện hiện tại, vì vậy bạn có thể tạo một biến thể trên một chủ đề:

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

Cuối cùng, phương thức .build () của trình xây dựng kiểm tra xem tất cả các giá trị bắt buộc đã được cung cấp và ném nếu có bất kỳ thiếu. Có thể nên yêu cầu một số giá trị trên hàm tạo của trình xây dựng và cho phép phần còn lại là tùy chọn; trong trường hợp đó, bạn muốn một trong các mẫu trong các câu trả lời khác.

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);
        }
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.