Java: วิธีการใช้เครื่องมือสร้างขั้นตอนซึ่งลำดับของ setters ไม่สำคัญ?


10

แก้ไข:ฉันต้องการจะชี้ให้เห็นว่าคำถามนี้อธิบายถึงปัญหาทางทฤษฎีและฉันรู้ว่าฉันสามารถใช้อาร์กิวเมนต์ตัวสร้างสำหรับพารามิเตอร์บังคับหรือส่งข้อยกเว้น runtime ถ้าใช้ API ไม่ถูกต้อง อย่างไรก็ตามฉันกำลังมองหาวิธีแก้ปัญหาที่ไม่ต้องการตัวสร้างอาร์กิวเมนต์หรือการตรวจสอบรันไทม์

ลองนึกภาพคุณมีCarอินเทอร์เฟซเช่นนี้:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional
}

ตามความคิดเห็นที่จะแนะนำCarต้องมีEngineและTransmissionแต่Stereoเป็นตัวเลือก นั่นหมายความว่าสร้างที่สามารถเพียงตัวอย่างที่เคยควรจะมีวิธีการถ้าและมีทั้งรับแล้วมอบให้กับผู้สร้างอินสแตนซ์ ด้วยวิธีการตรวจสอบชนิดจะปฏิเสธที่จะรวบรวมรหัสใด ๆ ที่พยายามที่จะสร้างอินสแตนซ์โดยไม่ต้องหรือbuild()Carbuild()EngineTransmissionCarEngineTransmission

สายนี้สำหรับสร้างขั้นตอน โดยทั่วไปแล้วคุณจะใช้สิ่งนี้:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine);
        }
    }

    public class BuilderWithEngine {
        private Engine engine;
        private BuilderWithEngine(Engine engine) {
            this.engine = engine;
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission);
        }
    }

    public class CompleteBuilder {
        private Engine engine;
        private Transmission transmission;
        private Stereo stereo = null;
        private CompleteBuilder(Engine engine, Transmission transmission) {
            this.engine = engine;
            this.transmission = transmission;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        public CompleteBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

มีห่วงโซ่ของการเรียนการสร้างที่แตกต่างกัน (เป็นBuilder, BuilderWithEngine, CompleteBuilder) ที่เพิ่มวิธีการตั้งค่าที่จำเป็นหนึ่งหลังจากที่อื่นกับการเรียนที่ผ่านมามีทั้งหมดวิธีการตั้งค่าตัวเลือกเช่นกัน
ซึ่งหมายความว่าผู้ใช้งานของผู้สร้างขั้นตอนนี้จะถูก จำกัด อยู่ในลำดับที่ผู้เขียนทำให้ผู้เซ็ตอัพสามารถใช้งานได้ นี่คือตัวอย่างของการใช้งานที่เป็นไปได้ (โปรดทราบว่าทั้งหมดนั้นมีคำสั่งอย่างเคร่งครัด: engine(e)ก่อนตามด้วยtransmission(t)และในที่สุดก็เป็นทางเลือกstereo(s))

new Builder().engine(e).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).engine(e).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).engine(e).build();
new Builder().engine(e).transmission(t).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).stereo(s).build();

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

ทางออกเดียวที่ฉันคิดได้ว่ามีความซับซ้อนมาก:สำหรับการรวมกันของคุณสมบัติบังคับที่ได้รับการตั้งค่าหรือยังไม่ได้ตั้งค่าฉันสร้างคลาสตัวสร้างเฉพาะที่รู้ว่าตัวบังคับที่จำเป็นอื่น ๆ สถานะที่build()ควรมีวิธีการและแต่ละตัวตั้งค่าเหล่านั้นส่งคืนชนิดของตัวสร้างที่สมบูรณ์มากขึ้นซึ่งเป็นขั้นตอนเดียวที่มีbuild()วิธีการบรรจุมากกว่า
ฉันเพิ่มรหัสด้านล่าง แต่คุณอาจบอกว่าฉันใช้ระบบประเภทเพื่อสร้าง FSM ที่ให้คุณสร้าง a Builderซึ่งสามารถเปลี่ยนเป็น a BuilderWithEngineหรือBuilderWithTransmissionซึ่งทั้งสองสามารถกลายเป็น a CompleteBuilderซึ่งใช้build()วิธี. setters ที่เป็นตัวเลือกสามารถถูกเรียกใช้บนอินสแตนซ์ตัวสร้างใด ๆ เหล่านี้ ป้อนคำอธิบายรูปภาพที่นี่

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder extends OptionalBuilder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            return new BuilderWithTransmission(transmission, stereo);
        }
        @Override
        public Builder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class OptionalBuilder {
        protected Stereo stereo = null;
        private OptionalBuilder() {}
        public OptionalBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
    }

    public class BuilderWithEngine extends OptionalBuilder {
        private Engine engine;
        private BuilderWithEngine(Engine engine, Stereo stereo) {
            this.engine = engine;
            this.stereo = stereo;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        @Override
        public BuilderWithEngine stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class BuilderWithTransmission extends OptionalBuilder {
        private Transmission transmission;
        private BuilderWithTransmission(Transmission transmission, Stereo stereo) {
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public BuilderWithTransmission stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class CompleteBuilder extends OptionalBuilder {
        private Engine engine;
        private Transmission transmission;
        private CompleteBuilder(Engine engine, Transmission transmission, Stereo stereo) {
            this.engine = engine;
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public CompleteBuilder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

อย่างที่คุณสามารถบอกได้ว่าสิ่งนี้ไม่ได้ปรับขนาดได้อย่างดีเนื่องจากจำนวนคลาสตัวสร้างที่แตกต่างกันที่ต้องการคือO (2 ^ n)โดยที่nคือจำนวนเซ็ตเตอร์บังคับ

ดังนั้นคำถามของฉัน: สิ่งนี้สามารถทำได้อย่างสวยงามยิ่งขึ้น?

(ฉันกำลังมองหาคำตอบที่ใช้งานได้กับ Java แม้ว่า Scala จะเป็นที่ยอมรับเช่นกัน)


1
อะไรทำให้คุณไม่เพียงแค่ใช้คอนเทนเนอร์ IoC เพื่อยืนการพึ่งพาทั้งหมดเหล่านี้ นอกจากนี้ฉันไม่ชัดเจนว่าทำไมถ้าสั่งไม่ได้เรื่องที่คุณได้กล่าวหาคุณสามารถไม่เพียง แต่ใช้วิธีหมาสามัญว่าการกลับมาthis?
Robert Harvey

การเรียก.engine(e)สองครั้งสำหรับผู้สร้างหนึ่งหมายความว่าอย่างไร
Erik Eidt

3
หากคุณต้องการตรวจสอบแบบสแตติกโดยไม่ต้องเขียนคลาสด้วยตนเองสำหรับชุดค่าผสมทุกชุดคุณอาจต้องใช้สิ่งที่ระดับคอ - ดรัมเช่นมาโครหรือแม่แบบ Java นั้นไม่เพียงพอสำหรับความรู้ของฉันและความพยายามที่น่าจะไม่คุ้มกับโซลูชันที่ผ่านการตรวจสอบแบบไดนามิกในภาษาอื่น
Karl Bielefeldt

1
Robert: เป้าหมายคือเพื่อให้ตัวตรวจสอบชนิดบังคับใช้ความจริงที่ว่าทั้งเครื่องยนต์และระบบส่งกำลังได้รับคำสั่ง วิธีนี้คุณไม่สามารถแม้แต่จะเรียกbuild()ถ้าคุณยังไม่ได้เรียกengine(e)และtransmission(t)ก่อนที่จะ
derabbink

Erik: คุณอาจต้องการเริ่มต้นด้วยการEngineใช้งานเริ่มต้นและเขียนทับมันในภายหลังด้วยการใช้งานที่เฉพาะเจาะจงมากขึ้น แต่ส่วนใหญ่นี้จะทำให้ความรู้สึกมากขึ้นถ้าengine(e)ไม่ได้เป็นหมา addEngine(e)แต่เป็นงูพิษที่: สิ่งนี้จะเป็นประโยชน์สำหรับCarผู้สร้างที่สามารถผลิตรถยนต์ไฮบริดที่มีมากกว่าหนึ่งเครื่องยนต์ / มอเตอร์ เนื่องจากนี่เป็นตัวอย่างที่วางแผนไว้ฉันไม่ได้ลงรายละเอียดว่าทำไมคุณถึงอยากทำเช่นนี้ - เพื่อความกระชับ
derabbink

คำตอบ:


3

ดูเหมือนว่าคุณมีข้อกำหนดที่แตกต่างกันสองประการโดยขึ้นอยู่กับวิธีการโทรของคุณ

  1. เครื่องยนต์หนึ่งตัว (จำเป็น), การส่งสัญญาณเพียงหนึ่งอัน (จำเป็น) และสเตอริโอ (อุปกรณ์เสริม) หนึ่งตัวเท่านั้น
  2. เครื่องยนต์หนึ่งตัวหรือมากกว่า (จำเป็น) เครื่องยนต์หนึ่งเครื่องขึ้นไป (จำเป็น) เกียร์และสเตอริโอหนึ่งเสียงหรือมากกว่านั้น

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

รถยนต์สามารถมีหนึ่งเครื่องยนต์และหนึ่งเกียร์เท่านั้น แม้แต่รถยนต์ไฮบริดก็มีเพียงเครื่องยนต์เดียว (อาจเป็นGasAndElectricEngine)

ฉันจะกล่าวถึงการใช้งานทั้งสองอย่าง:

public class CarBuilder {

    public CarBuilder(Engine engine, Transmission transmission) {
        // ...
    }

    public CarBuilder setStereo(Stereo stereo) {
        // ...
        return this;
    }
}

และ

public class CarBuilder {

    public CarBuilder(List<Engine> engines, List<Transmission> transmission) {
        // ...
    }

    public CarBuilder addStereo(Stereo stereo) {
        // ...
        return this;
    }
}

หากจำเป็นต้องใช้เครื่องยนต์และเกียร์ก็ควรอยู่ใน Constructor

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


2

ทำไมไม่ใช้รูปแบบวัตถุ null? กำจัดผู้สร้างนี้รหัสที่งดงามที่สุดที่คุณสามารถเขียนได้คือรหัสที่คุณไม่ต้องเขียน

public final class CarImpl implements Car {
    private final Engine engine;
    private final Transmission transmission;
    private final Stereo stereo;

    public CarImpl(Engine engine, Transmission transmission) {
        this(engine, transmission, new DefaultStereo());
    }

    public CarImpl(Engine engine, Transmission transmission, Stereo stereo) {
        this.engine = engine;
        this.transmission = transmission;
        this.stereo = stereo;
    }

    //...

}

นี่คือสิ่งที่ฉันคิดทันที แม้ว่าฉันจะไม่มีคอนสตรัคเตอร์สามตัว แต่คอนสตรัคเตอร์พารามิเตอร์สองตัวที่มีองค์ประกอบบังคับและเซ็ตเตอร์สำหรับสเตอริโอตามที่เป็นตัวเลือก
Encaitar

1
ในตัวอย่างง่ายๆ (ที่มีการวางแผน) เช่นCarนี้จะสมเหตุสมผลเมื่อจำนวนอาร์กิวเมนต์ c'tor มีขนาดเล็กมาก อย่างไรก็ตามทันทีที่คุณจัดการกับสิ่งที่ซับซ้อนในระดับปานกลาง (> = 4 ข้อโต้แย้งที่บังคับ) สิ่งทั้งหมดจะยากต่อการจัดการ / อ่านง่ายกว่า ("เครื่องยนต์หรือระบบส่งกำลังมาก่อนหรือไม่") นั่นเป็นเหตุผลที่คุณจะใช้เครื่องมือสร้าง: API บังคับให้คุณมีความชัดเจนมากขึ้นเกี่ยวกับสิ่งที่คุณกำลังสร้าง
derabbink

1
@derabbink ทำไมไม่ทำลายชั้นเรียนของคุณให้เล็กลงในกรณีนี้? การใช้เครื่องมือสร้างจะซ่อนความจริงที่ว่าชั้นเรียนกำลังทำอะไรมากเกินไปและกลายเป็นสิ่งที่ไม่อาจปฏิเสธได้
เห็น

1
รุ่งโรจน์สำหรับการสิ้นสุดความบ้าแบบ
Robert Harvey

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

1

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

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

new Builder(e, t).build();                      // ok
new Builder(e, t).stereo(s).build();            // ok
new Builder(e, t).stereo(s).stereo(s).build();  // exception on second call to stereo as stereo is already set 

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


0

จำนวนคลาสตัวสร้างที่แตกต่างกันที่ต้องการคือ O (2 ^ n) โดยที่ n คือจำนวนเซ็ตเตอร์บังคับ

คุณเดาทิศทางที่ถูกต้องสำหรับคำถามนี้แล้ว

หากคุณต้องการได้รับการตรวจสอบเวลารวบรวมคุณจะต้อง(2^n)ประเภท หากคุณต้องการตรวจสอบเวลาทำงานคุณจะต้องมีตัวแปรที่สามารถเก็บ(2^n)สถานะได้ nจำนวนเต็มบิตจะทำ


เพราะสนับสนุน c ++ ไม่ใช่ประเภทพารามิเตอร์แม่แบบ (เช่นค่าจำนวนเต็ม)มันเป็นไปได้สำหรับแม่แบบ C ++ ระดับที่จะ instantiated ลงในO(2^n)ประเภทที่แตกต่างกันโดยใช้รูปแบบคล้ายกับการนี้

อย่างไรก็ตามในภาษาที่ไม่สนับสนุนพารามิเตอร์เทมเพลตที่ไม่ใช่ประเภทคุณไม่สามารถพึ่งพาระบบประเภทเพื่อยกตัวอย่างO(2^n)ประเภทที่แตกต่างกัน


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


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

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