เมื่อใช้วิธีการผูกมัดฉันจะนำวัตถุมาใช้ใหม่หรือสร้างใหม่ได้หรือไม่


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);?
James

12
@James telescopic constructor รูปแบบที่คล่องแคล่วสามารถช่วยแยกแยะความแตกต่างระหว่างพารามิเตอร์ทางเลือกและพารามิเตอร์ที่ต้องการ (ถ้าเป็นพารามิเตอร์ Constructor ที่ต้องการ และความคล่องแคล่วค่อนข้างดีที่จะอ่าน
NimChimpsky

8
@NimChimpsky เกิดอะไรขึ้นกับคุณสมบัติแบบเก่า (สำหรับ C #) ที่ดีและคอนสตรัคเตอร์ที่มีฟิลด์ที่ต้องใช้ไม่ใช่ว่าฉันกำลังใช้ Fluent APIs ฉันเป็นแฟนตัวยง แต่พวกเขามักใช้มากเกินไป
Chris S

8
@ChrisS หากคุณพึ่งพา setters (ฉันมาจาก java) คุณต้องทำให้วัตถุของคุณไม่แน่นอนซึ่งคุณอาจไม่ต้องการทำ และคุณยังได้ nicer intellitext เมื่อใช้ความคล่องแคล่ว - ต้องใช้ความคิดน้อยกว่า IDE เกือบจะสร้างวัตถุของคุณให้คุณ
NimChimpsky

1
@NimChimpsky Yeh ฉันสามารถดูวิธีการได้อย่างคล่องแคล่วเป็นก้าวกระโดดครั้งใหญ่สำหรับ Java
คริส S

คำตอบ:


41

ฉันจะใส่API อย่างคล่องแคล่วให้กับมันเป็น "ผู้สร้าง" แยกชั้นจากวัตถุที่มันกำลังสร้าง ด้วยวิธีนี้หากลูกค้าไม่ต้องการใช้ 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 คุณสามารถดูตัวอย่างอื่น ๆ ได้ในส่วน Java ของบทความ wikipedia Fluent Interface ที่มีการคงอยู่การจัดการวันที่และการจำลองวัตถุ


แก้ไข:

ตามที่ระบุไว้จากความคิดเห็น; คุณสามารถทำให้คลาส Builder เป็นคลาสภายในแบบคงที่ (ภายในรถยนต์) และรถยนต์อาจไม่เปลี่ยนรูป ตัวอย่างของการปล่อยให้รถไม่เปลี่ยนรูปนั้นดูโง่ไปหน่อย แต่ในระบบที่ซับซ้อนมากขึ้นซึ่งคุณไม่ต้องการเปลี่ยนเนื้อหาของวัตถุที่ถูกสร้างขึ้นอย่างแน่นอนคุณอาจต้องการทำมัน

ด้านล่างเป็นตัวอย่างหนึ่งของวิธีการทำทั้งคลาสภายในแบบสแตติกและวิธีจัดการกับการสร้างวัตถุที่ไม่เปลี่ยนรูปที่มันสร้างขึ้นมา:

// 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ในความคิดเห็นที่โพสต์บล็อกเกี่ยวกับการใช้ผู้สร้างกับฟังก์ชั่นแลมบ์ดาในบริบทของการเขียนการทดสอบหน่วยด้วยวัตถุโดเมนที่ซับซ้อน มันเป็นทางเลือกที่น่าสนใจที่จะทำให้ผู้สร้างแสดงออกได้มากขึ้น

ในกรณีที่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 ที่ระบุไว้
NimChimpsky

6
@Baqueta ต้องการการอ่านสำหรับ java dev, imho
NimChimpsky

3
IMHO มีข้อได้เปรียบอย่างมากคือคุณสามารถใช้รูปแบบนี้ (หากปรับเปลี่ยนอย่างเหมาะสม) เพื่อป้องกันอินสแตนซ์ของวัตถุที่อยู่ระหว่างการก่อสร้างซึ่งไม่เสร็จสมบูรณ์จากการหลบหนีจากตัวสร้าง เช่นคุณสามารถมั่นใจได้ว่าจะไม่มีรถยนต์ที่มีสีที่ไม่ได้กำหนด
scarfridge

1
อืม ... ฉันมักจะเรียกวิธีสุดท้ายของรูปแบบตัวสร้างbuild()(หรือBuild()) ไม่ใช่ชื่อของชนิดที่สร้าง ( Car()ในตัวอย่างของคุณ) นอกจากนี้หากCarเป็นวัตถุที่เปลี่ยนรูปไม่ได้อย่างแท้จริง (เช่นทุกสาขาของมันreadonly) แล้วแม้แต่ผู้สร้างจะไม่สามารถกลายพันธุ์ได้ดังนั้นBuild()วิธีการจะกลายเป็นความรับผิดชอบในการสร้างอินสแตนซ์ใหม่ วิธีหนึ่งในการทำเช่นนี้คือการมีCarConstructor เพียงตัวเดียวซึ่งใช้ตัวสร้างเป็นอาร์กิวเมนต์ แล้ววิธีการก็สามารถBuild() return new Car(this);
Daniel Pryden

1
ฉันทำบล็อกเกี่ยวกับแนวทางที่แตกต่างในการสร้างผู้สร้างจาก lambdas โพสต์อาจต้องแก้ไขเล็กน้อย บริบทของฉันส่วนใหญ่นั้นอยู่ในขอบเขตของการทดสอบหน่วย แต่มันสามารถนำไปใช้กับพื้นที่อื่นเช่นกันถ้ามี สามารถพบได้ที่นี่: petesdotnet.blogspot.com/2012/05/…
Pete

9

ขึ้นอยู่กับว่า

รถยนต์ของคุณเป็นเอนทิตี้หรือวัตถุที่คุ้มค่าหรือไม่? หากรถยนต์เป็นเอนทิตีเอกลักษณ์ของวัตถุนั้นมีความสำคัญดังนั้นคุณควรส่งคืนการอ้างอิงเดียวกัน หากวัตถุนั้นเป็นวัตถุที่มีมูลค่าควรจะไม่เปลี่ยนรูปซึ่งหมายถึงวิธีเดียวคือส่งคืนอินสแตนซ์ใหม่ทุกครั้ง

ตัวอย่างของหลังจะเป็นคลาส DateTime ใน. NET ซึ่งเป็นวัตถุค่า

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 ในการใช้คลาส builder เพื่อสร้างออบเจ็กต์ให้คุณ กล่าวอีกนัยหนึ่งตัวอย่างที่คุณให้ไว้นั้นสมเหตุสมผล IMHO ถ้ารถยนต์เป็นวัตถุที่มีค่า


1
+1 สำหรับคำถาม 'เอนทิตี' กับ 'ค่า' เป็นคำถามว่าคลาสของคุณเป็นประเภทที่ไม่แน่นอนหรือไม่เปลี่ยนรูปแบบ (ควรเปลี่ยนวัตถุนี้หรือไม่) และขึ้นอยู่กับคุณอย่างสมบูรณ์แม้ว่ามันจะมีผลต่อการออกแบบของคุณ ฉันมักจะไม่คาดหวังว่าการผูกมัดวิธีการทำงานกับประเภทที่ไม่แน่นอนยกเว้นว่าวิธีการส่งกลับวัตถุใหม่
Casey Kuball

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

สิ่งที่สำคัญที่สุดคือไม่ว่าการตัดสินใจของคุณจะระบุไว้ชัดเจนในชื่อวิธีการและ / หรือความคิดเห็น

ไม่มีมาตรฐานบางครั้งวิธีจะส่งคืนวัตถุใหม่ (ส่วนใหญ่ของวิธีการสตริงทำเช่นนั้น) หรือจะส่งคืนวัตถุนี้เพื่อวัตถุประสงค์ในการผูกมัดหรือเพื่อประสิทธิภาพหน่วยความจำ)

ฉันเคยออกแบบวัตถุ 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

จุดดีชื่ออาจชัดเจนกว่า การตั้งชื่อเป็นไปตามหลักการของคลาสคณิตศาสตร์อื่น ๆ ที่ฉันใช้จากห้องสมุด ผลที่ได้ยังระบุไว้ในความคิดเห็น javadoc ของวิธีการเพื่อหลีกเลี่ยงความสับสน
XGouchet

3

ฉันเห็นปัญหาเล็กน้อยที่นี่ซึ่งฉันคิดว่าอาจทำให้สับสน ... บรรทัดแรกของคุณในคำถาม:

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

คุณกำลังเรียกนวกรรมิก (ใหม่) และวิธีการสร้าง ... วิธีการสร้าง () มักจะเป็นวิธีคงที่หรือวิธีการสร้างและคอมไพเลอร์ควรจับมันในคำเตือนหรือข้อผิดพลาดเพื่อให้คุณรู้ว่าทั้ง ทางไวยากรณ์นี้อาจผิดหรือมีชื่อแย่ ๆ แต่ต่อมาคุณไม่ได้ใช้ทั้งคู่ลองดูที่

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

อีกครั้งกับการสร้างแม้ว่าจะไม่ได้มีตัวสร้างใหม่ สิ่งนี้คือฉันคิดว่าคุณกำลังมองหาวิธีคัดลอก () แทน ดังนั้นถ้าเป็นกรณีนี้และเป็นเพียงชื่อที่ไม่ดีลองดูสิ่งหนึ่ง ... คุณเรียก mercedes.Paintedin (สีเหลือง) คัดลอก () - มันควรจะง่ายที่จะดูและบอกว่ามันถูกทาสี 'ก่อนที่จะถูกคัดลอก - แค่ตรรกะธรรมดา ๆ สำหรับฉัน ดังนั้นให้คัดลอกก่อน

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

สำหรับฉันมันเป็นเรื่องง่ายที่จะเห็นว่าคุณกำลังวาดภาพสำเนาทำให้รถสีเหลืองของคุณ


+1 สำหรับการชี้ให้เห็นถึงความไม่ลงรอยกันระหว่างใหม่และสร้าง ();
Joshua Drake

1

วิธีแรกมีข้อเสียเปรียบที่คุณพูดถึง แต่ตราบใดที่คุณทำให้ชัดเจนใน docs coder ที่มีความสามารถครึ่งหนึ่งไม่ควรมีปัญหา รหัสการผูกมัดวิธีการทั้งหมดที่ฉันเคยทำงานด้วยตนเองได้ทำงานในลักษณะนี้

วิธีที่สองเห็นได้ชัดว่ามีข้อเสียเปรียบในการทำงานมากขึ้น คุณต้องตัดสินใจด้วยว่าสำเนาที่คุณส่งคืนนั้นจะทำสำเนาแบบตื้นหรือลึกซึ่งดีที่สุดอาจแตกต่างกันไปในแต่ละชั้นเรียนหรือวิธีการวิธีการดังนั้นคุณจะได้รับการแนะนำที่ไม่สอดคล้องกัน เป็นที่น่าสังเกตว่านี่เป็นตัวเลือกเดียวสำหรับวัตถุที่ไม่เปลี่ยนรูปเช่นสตริง

ไม่ว่าคุณจะทำอะไรอย่าผสมและจับคู่ในคลาสเดียวกัน!


1

ฉันอยากจะคิดเหมือนกลไก "วิธีการขยาย"

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

0

นี่คือการเปลี่ยนแปลงในวิธีการข้างต้น ความแตกต่างคือมีวิธีการคงที่ในคลาสรถยนต์ที่ตรงกับชื่อวิธีการใน Builder ดังนั้นคุณไม่จำเป็นต้องสร้าง Builder อย่างชัดเจน:

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

คุณสามารถใช้ชื่อวิธีการเดียวกันกับที่คุณใช้ในการเรียกตัวสร้างการเชื่อมโยง:

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

นอกจากนี้ยังมีวิธีการ. คัดลอก () ในชั้นเรียนที่ส่งกลับตัวสร้างที่เติมด้วยค่าทั้งหมดจากอินสแตนซ์ปัจจุบันดังนั้นคุณสามารถสร้างรูปแบบในรูปแบบ:

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);
        }
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.