Java: Làm thế nào để triển khai trình xây dựng bước mà thứ tự của setters không quan trọng?


10

Chỉnh sửa: Tôi muốn chỉ ra rằng câu hỏi này mô tả một vấn đề lý thuyết và tôi biết rằng tôi có thể sử dụng các đối số hàm tạo cho các tham số bắt buộc hoặc ném ngoại lệ thời gian chạy nếu API được sử dụng không chính xác. Tuy nhiên, tôi đang tìm kiếm một giải pháp không yêu cầu đối số hàm tạo hoặc kiểm tra thời gian chạy.

Hãy tưởng tượng bạn có một Cargiao diện như thế này:

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

Như các ý kiến ​​sẽ đề xuất, Carphải có một EngineTransmissionnhưng là một Stereotùy chọn. Điều đó có nghĩa một Builder có thể build()một Cartrường hợp duy nhất bao giờ nên có một build()phương pháp nếu một EngineTransmissioncó cả hai đã được trao cho các trường hợp xây dựng. Bằng cách đó, trình kiểm tra loại sẽ từ chối biên dịch bất kỳ mã nào cố gắng tạo một Carcá thể mà không có Enginehoặc Transmission.

Điều này gọi cho một Builder Builder . Thông thường, bạn sẽ thực hiện một cái gì đó như thế này:

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

Có một chuỗi của các tầng lớp người xây dựng khác nhau ( Builder, BuilderWithEngine, CompleteBuilder), mà add một đòi hỏi phương pháp setter sau khác, với lớp cuối cùng có chứa tất cả các phương pháp setter tùy chọn là tốt.
Điều này có nghĩa là người dùng của trình xây dựng bước này bị giới hạn theo thứ tự mà tác giả đã tạo ra các setters bắt buộc có sẵn . Dưới đây là một ví dụ về các cách sử dụng có thể (lưu ý rằng tất cả chúng đều được sắp xếp nghiêm ngặt: engine(e)đầu tiên, tiếp theo transmission(t)và cuối cùng là tùy chọn 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();

Tuy nhiên, có rất nhiều tình huống trong đó điều này không lý tưởng cho người dùng của nhà xây dựng, đặc biệt là nếu nhà xây dựng không chỉ setters, mà còn các trình bổ sung hoặc nếu người dùng không thể kiểm soát thứ tự mà các thuộc tính nhất định cho nhà xây dựng sẽ khả dụng.

Giải pháp duy nhất tôi có thể nghĩ ra là rất phức tạp: Đối với mọi sự kết hợp của các thuộc tính bắt buộc đã được đặt hoặc chưa được đặt, tôi đã tạo một lớp trình tạo chuyên dụng để biết các setters bắt buộc tiềm năng nào khác cần được gọi trước khi đến trạng thái nơi build()phương thức nên có sẵn và mỗi một trong số các setters đó trả về một kiểu trình xây dựng hoàn chỉnh hơn, một bước gần hơn để chứa một build()phương thức.
Tôi đã thêm mã bên dưới, nhưng bạn có thể nói rằng tôi đang sử dụng hệ thống loại để tạo một FSM cho phép bạn tạo một Builder, có thể biến thành một BuilderWithEnginehoặc BuilderWithTransmission, sau đó cả hai có thể được chuyển thành một CompleteBuilder, trong đó thực hiệnbuild()phương pháp. Setters tùy chọn có thể được gọi trên bất kỳ trường hợp xây dựng nào. nhập mô tả hình ảnh ở đây

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

Như bạn có thể nói, điều này không có quy mô tốt, vì số lượng các lớp xây dựng khác nhau được yêu cầu sẽ là O (2 ^ n) trong đó n là số lượng setters bắt buộc.

Do đó câu hỏi của tôi: Điều này có thể được thực hiện thanh lịch hơn?

(Tôi đang tìm kiếm một câu trả lời hoạt động với Java, mặc dù Scala cũng sẽ được chấp nhận)


1
Điều gì ngăn cản bạn chỉ sử dụng một container IoC để đứng lên tất cả các phụ thuộc này? Ngoài ra, tôi không rõ tại sao, nếu đơn hàng không quan trọng như bạn đã khẳng định, bạn không thể sử dụng các phương thức setter thông thường trả về this?
Robert Harvey

Việc gọi .engine(e)hai lần cho một người xây dựng có nghĩa là gì?
Erik Eidt

3
Nếu bạn muốn xác minh nó một cách tĩnh mà không cần viết một lớp theo cách thủ công cho mọi kết hợp, có lẽ bạn phải sử dụng các công cụ cấp độ cổ như macro hoặc lập trình siêu mẫu. Theo hiểu biết của tôi, Java không đủ sức biểu cảm và nỗ lực có thể sẽ không xứng đáng với các giải pháp được xác minh động trong các ngôn ngữ khác.
Karl Bielefeldt

1
Robert: Mục tiêu là để trình kiểm tra loại thực thi thực tế là cả động cơ và hộp số đều là bắt buộc; bằng cách này, bạn thậm chí không thể gọi build()nếu bạn chưa gọi engine(e)transmission(t)trước đó.
derabbink

Erik: Bạn có thể muốn bắt đầu với một Enginetriển khai mặc định và sau đó ghi đè lên nó bằng một triển khai cụ thể hơn. Nhưng rất có thể điều này sẽ có ý nghĩa hơn nếu engine(e)không phải là một setter, mà là một adder : addEngine(e). Điều này sẽ hữu ích cho một Carnhà chế tạo có thể sản xuất xe hơi lai với nhiều động cơ / động cơ. Vì đây là một ví dụ giả định, tôi đã không đi sâu vào chi tiết về lý do tại sao bạn có thể muốn làm điều này - cho ngắn gọn.
derabbink

Câu trả lời:


3

Bạn dường như có hai yêu cầu khác nhau, dựa trên các cuộc gọi phương thức bạn cung cấp.

  1. Chỉ có một động cơ (bắt buộc), chỉ một truyền (bắt buộc) và chỉ một âm thanh nổi (tùy chọn).
  2. Một hoặc nhiều động cơ (bắt buộc), một hoặc nhiều truyền (bắt buộc) và một hoặc nhiều âm thanh nổi (tùy chọn).

Tôi nghĩ vấn đề đầu tiên ở đây là bạn không biết bạn muốn lớp học làm gì. Một phần trong đó là không biết bạn muốn đối tượng được xây dựng trông như thế nào.

Một chiếc xe chỉ có thể có một động cơ và một hộp số. Ngay cả những chiếc xe hybrid cũng chỉ có một động cơ (có lẽ là a GasAndElectricEngine)

Tôi sẽ giải quyết cả hai triển khai:

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

Nếu một động cơ và truyền được yêu cầu, thì chúng nên ở trong hàm tạo.

Nếu bạn không biết động cơ hoặc truyền động nào là bắt buộc, thì chưa đặt một động cơ nào; đó là một dấu hiệu cho thấy bạn đang tạo trình xây dựng quá xa ngăn xếp.


2

Tại sao không sử dụng mẫu đối tượng null? Loại bỏ trình xây dựng này, mã thanh lịch nhất bạn có thể viết là mã bạn thực sự không phải viết.

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

    //...

}

Đây là những gì tôi nghĩ ngay lập tức. mặc dù tôi sẽ không có ba hàm tạo tham số, chỉ có hai hàm tạo tham số có các phần tử bắt buộc và sau đó là một setter cho âm thanh nổi vì nó là tùy chọn.
Encaitar

1
Trong một ví dụ đơn giản (giả định) như thế Car, điều này sẽ có ý nghĩa vì số lượng đối số c'tor là rất nhỏ. Tuy nhiên, ngay khi bạn xử lý bất cứ điều gì phức tạp vừa phải (> = 4 đối số bắt buộc), toàn bộ điều sẽ trở nên khó xử lý hơn / ít đọc hơn ("Động cơ hoặc truyền dẫn có trước không?"). Đó là lý do tại sao bạn sẽ sử dụng trình xây dựng: API buộc bạn phải rõ ràng hơn về những gì bạn đang xây dựng.
derabbink

1
@derabbink Tại sao không phá vỡ lớp học của bạn trong những trường nhỏ hơn trong trường hợp này? Sử dụng một trình xây dựng sẽ chỉ che giấu sự thật rằng lớp đang làm quá nhiều và đã trở nên không thể nhận ra.
Phát hiện

1
Kudos cho kết thúc sự điên rồ mô hình.
Robert Harvey

@ Phát hiện một số lớp chỉ chứa rất nhiều dữ liệu. Ví dụ: nếu bạn muốn tạo một lớp nhật ký truy cập chứa tất cả các thông tin liên quan về yêu cầu HTTP và xuất dữ liệu sang định dạng CSV hoặc JSON. Sẽ có rất nhiều dữ liệu và nếu bạn muốn thực thi một số trường có mặt trong thời gian biên dịch, bạn sẽ cần một mẫu trình xây dựng với hàm tạo danh sách arg rất dài, trông không đẹp.
ssgao

1

Thứ nhất, trừ khi bạn có nhiều thời gian hơn bất kỳ cửa hàng nào tôi từng làm việc, có lẽ không đáng để cho phép bất kỳ đơn đặt hàng hoạt động nào hoặc chỉ sống với thực tế là bạn có thể chỉ định nhiều hơn một đài phát thanh. Lưu ý rằng bạn đang nói về mã, không phải đầu vào của người dùng, do đó bạn có thể có các xác nhận sẽ thất bại trong quá trình kiểm tra đơn vị của mình thay vì một giây trước khi biên dịch.

Tuy nhiên, nếu ràng buộc của bạn, như được đưa ra trong các ý kiến, rằng bạn phải có một động cơ và truyền, sau đó thực thi điều này bằng cách đặt tất cả các thuộc tính bắt buộc là hàm tạo của trình tạo.

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 

Nếu đó chỉ là âm thanh nổi là tùy chọn, thì việc thực hiện bước cuối cùng bằng cách sử dụng các lớp con của trình xây dựng là có thể, nhưng ngoài ra, việc đạt được lỗi trong thời gian biên dịch thay vì trong thử nghiệm có lẽ không đáng để nỗ lực.


0

số lượng các lớp xây dựng khác nhau được yêu cầu sẽ là O (2 ^ n) trong đó n là số lượng setters bắt buộc.

Bạn đã đoán đúng hướng cho câu hỏi này.

Nếu bạn muốn kiểm tra thời gian biên dịch, bạn sẽ cần (2^n)các loại. Nếu bạn muốn kiểm tra thời gian chạy, bạn sẽ cần một biến có thể lưu trữ (2^n)trạng thái; một nsố nguyên -bit sẽ làm.


Vì C ++ hỗ trợ tham số mẫu không phải kiểu (ví dụ: giá trị nguyên) , nên mẫu lớp C ++ có thể được khởi tạo thành O(2^n)các loại khác nhau, sử dụng sơ đồ tương tự như thế này .

Tuy nhiên, trong các ngôn ngữ không hỗ trợ các tham số mẫu không phải loại, bạn không thể dựa vào hệ thống loại để khởi tạo O(2^n)các loại khác nhau.


Cơ hội tiếp theo là với các chú thích Java (và các thuộc tính C #). Các siêu dữ liệu bổ sung này có thể được sử dụng để kích hoạt hành vi do người dùng xác định tại thời gian biên dịch, khi bộ xử lý chú thích được sử dụng. Tuy nhiên, nó sẽ là quá nhiều công việc để bạn thực hiện những điều này. Nếu bạn đang sử dụng các khung cung cấp chức năng này cho bạn, hãy sử dụng nó. Nếu không, hãy kiểm tra cơ hội tiếp theo.


Cuối cùng, lưu ý rằng việc lưu trữ O(2^n)các trạng thái khác nhau dưới dạng một biến trong thời gian chạy (theo nghĩa đen là một số nguyên có ít nhất nbit rộng) là rất dễ dàng. Đây là lý do tại sao tất cả các câu trả lời được đánh giá cao nhất đều khuyên bạn nên thực hiện kiểm tra này vào thời gian chạy, bởi vì nỗ lực cần thiết để thực hiện kiểm tra thời gian biên dịch là quá nhiều, khi so sánh với mức tăng tiềm năng.

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.