Được rồi, do đó, cách tiếp cận bước xây dựng bước truyền thống sẽ giống như thế này.
Thật không may, bởi vì chúng tôi đang trộn các phương pháp chung chung và không chung chung, chúng tôi phải redeclare rất nhiều phương pháp. Tôi không nghĩ rằng có một cách hay về việc này.
Ý tưởng cơ bản chỉ là: xác định từng bước trên một giao diện, sau đó triển khai tất cả các bước trên lớp riêng tư. Chúng ta có thể làm điều đó với các giao diện chung bằng cách kế thừa từ các kiểu thô của chúng. Nó xấu xí, nhưng nó hoạt động.
public interface NumberStep {
NumberStep withNumber(int number);
}
public interface NeitherDoneStep extends NumberStep {
@Override NeitherDoneStep withNumber(int number);
<T> TypeDoneStep<T> withTyped(T type);
<S> ListDoneStep<S> withList(List<S> list);
}
public interface TypeDoneStep<T> extends NumberStep {
@Override TypeDoneStep<T> withNumber(int number);
TypeDoneStep<T> withTyped(T type);
<S> BothDoneStep<T, S> withList(List<S> list);
}
public interface ListDoneStep<S> extends NumberStep {
@Override ListDoneStep<S> withNumber(int number);
<T> BothDoneStep<T, S> withTyped(T type);
ListDoneStep<S> withList(List<S> list);
}
public interface BothDoneStep<T, S> extends NumberStep {
@Override BothDoneStep<T, S> withNumber(int number);
BothDoneStep<T, S> withTyped(T type);
BothDoneStep<T, S> withList(List<S> list);
SomeObject<T, S> create();
}
@SuppressWarnings({"rawtypes","unchecked"})
private static final class BuilderImpl implements NeitherDoneStep, TypeDoneStep, ListDoneStep, BothDoneStep {
private final int number;
private final Object typed;
private final List list;
private BuilderImpl(int number, Object typed, List list) {
this.number = number;
this.typed = typed;
this.list = list;
}
@Override
public BuilderImpl withNumber(int number) {
return new BuilderImpl(number, this.typed, this.list);
}
@Override
public BuilderImpl withTyped(Object typed) {
// we could return 'this' at the risk of heap pollution
return new BuilderImpl(this.number, typed, this.list);
}
@Override
public BuilderImpl withList(List list) {
// we could return 'this' at the risk of heap pollution
return new BuilderImpl(this.number, this.typed, list);
}
@Override
public SomeObject create() {
return new SomeObject(number, typed, list);
}
}
// static factory
public static NeitherDoneStep builder() {
return new BuilderImpl(0, null, null);
}
Vì chúng tôi không muốn mọi người truy cập vào việc triển khai xấu, chúng tôi làm cho nó riêng tư và làm cho mọi người trải qua phương pháp static
.
Nếu nó hoạt động khá nhiều giống như ý tưởng của riêng bạn:
SomeObject<String, Integer> works =
SomeObject.builder()
.withNumber(4)
.withList(new ArrayList<Integer>())
.withTyped("something")
.create();
// we could return 'this' at the risk of heap pollution
này là cái gì vậy? Được rồi, vì vậy có một vấn đề nói chung ở đây, và nó là như thế này:
NeitherDoneStep step = SomeObject.builder();
BothDoneStep<String, Integer> both =
step.withTyped("abc")
.withList(Arrays.asList(123));
// setting 'typed' to an Integer when
// we already set it to a String
step.withTyped(123);
SomeObject<String, Integer> oops = both.create();
Nếu chúng ta không tạo ra các bản sao, bây giờ chúng ta sẽ phải 123
giả mạo xung quanh như một String
.
(Nếu bạn chỉ sử dụng những người xây dựng như các thiết lập thông thạo các cuộc gọi, điều này không thể xảy ra.)
Mặc dù chúng tôi không cần phải tạo một bản sao cho withNumber
, tôi chỉ cần đi thêm bước và làm cho người xây dựng không thay đổi được. Chúng tôi đang tạo ra nhiều đối tượng hơn chúng ta phải nhưng không thực sự là một giải pháp tốt nữa. Nếu tất cả mọi người sẽ sử dụng trình xây dựng theo cách chính xác, thì chúng tôi có thể biến nó thành có thể thay đổi và return this
.
Vì chúng tôi quan tâm đến các giải pháp chung mới, đây là triển khai trình xây dựng trong một lớp duy nhất.
Sự khác biệt ở đây là chúng tôi không giữ lại các loại typed
và list
nếu chúng tôi gọi một trong hai người định cư của họ lần thứ hai. Điều này không thực sự là một nhược điểm cho mỗi lần, nó chỉ khác nhau tôi đoán. Nó có nghĩa là chúng ta có thể làm điều này:
SomeObject<Long, String> =
SomeObject.builder()
.withType(new Integer(1))
.withList(Arrays.asList("abc","def"))
.withType(new Long(1L)) // <-- changing T here
.create();
public static class OneBuilder<T, S> {
private final int number;
private final T typed;
private final List<S> list;
private OneBuilder(int number, T typed, List<S> list) {
this.number = number;
this.typed = typed;
this.list = list;
}
public OneBuilder<T, S> withNumber(int number) {
return new OneBuilder<T, S>(number, this.typed, this.list);
}
public <TR> OneBuilder<TR, S> withTyped(TR typed) {
// we could return 'this' at the risk of heap pollution
return new OneBuilder<TR, S>(this.number, typed, this.list);
}
public <SR> OneBuilder<T, SR> withList(List<SR> list) {
// we could return 'this' at the risk of heap pollution
return new OneBuilder<T, SR>(this.number, this.typed, list);
}
public SomeObject<T, S> create() {
return new SomeObject<T, S>(number, typed, list);
}
}
// As a side note,
// we could return e.g. <?, ?> here if we wanted to restrict
// the return type of create() in the case that somebody
// calls it immediately.
// The type arguments we specify here are just whatever
// we want create() to return before withTyped(...) and
// withList(...) are each called at least once.
public static OneBuilder<Object, Object> builder() {
return new OneBuilder<Object, Object>(0, null, null);
}
Cùng một điều về việc tạo bản sao và ô nhiễm heap.
Bây giờ chúng tôi đang nhận được thực sự cuốn tiểu thuyết. Ý tưởng ở đây là chúng ta có thể "vô hiệu hóa" từng phương pháp bằng cách gây ra lỗi chuyển đổi chụp.
Đó là một chút phức tạp để giải thích, nhưng ý tưởng cơ bản là:
- Mỗi phương pháp bằng cách nào đó phụ thuộc vào một biến kiểu đó được khai báo trên lớp.
- "Vô hiệu hóa" phương thức đó bằng cách đặt loại trả về biến kiểu thành
?
.
- Điều này gây ra lỗi chuyển đổi chụp nếu chúng tôi cố gắng gọi phương thức trên giá trị trả về đó.
Sự khác biệt giữa ví dụ này và ví dụ trước là nếu chúng ta cố gắng gọi một setter lần thứ hai, chúng tôi sẽ nhận được một lỗi biên dịch:
SomeObject<Long, String> =
SomeObject.builder()
.withType(new Integer(1))
.withList(Arrays.asList("abc","def"))
.withType(new Long(1L)) // <-- compiler error here
.create();
Do đó, chúng tôi chỉ có thể gọi mỗi setter Một lần.
Hai nhược điểm lớn ở đây là rằng bạn:
- không thể gọi setters lần thứ hai cho hợp pháp lý do
- và thể setters gọi lần thứ hai với
null
chữ.
Tôi nghĩ rằng đó là một bằng chứng khá thú vị về khái niệm, ngay cả khi đó là một chút không thực tế.
public static class OneBuilder<T, S, TCAP, SCAP> {
private final int number;
private final T typed;
private final List<S> list;
private OneBuilder(int number, T typed, List<S> list) {
this.number = number;
this.typed = typed;
this.list = list;
}
public OneBuilder<T, S, TCAP, SCAP> withNumber(int number) {
return new OneBuilder<T, S, TCAP, SCAP>(number, this.typed, this.list);
}
public <TR extends TCAP> OneBuilder<TR, S, ?, SCAP> withTyped(TR typed) {
// we could return 'this' at the risk of heap pollution
return new OneBuilder<TR, S, TCAP, SCAP>(this.number, typed, this.list);
}
public <SR extends SCAP> OneBuilder<T, SR, TCAP, ?> withList(List<SR> list) {
// we could return 'this' at the risk of heap pollution
return new OneBuilder<T, SR, TCAP, SCAP>(this.number, this.typed, list);
}
public SomeObject<T, S> create() {
return new SomeObject<T, S>(number, typed, list);
}
}
// Same thing as the previous example,
// we could return <?, ?, Object, Object> if we wanted
// to restrict the return type of create() in the case
// that someone called it immediately.
// (The type arguments to TCAP and SCAP should stay
// Object because they are the initial bound of TR and SR.)
public static OneBuilder<Object, Object, Object, Object> builder() {
return new OneBuilder<Object, Object, Object, Object>(0, null, null);
}
Một lần nữa, điều tương tự về việc tạo bản sao và ô nhiễm heap.
Dù sao, tôi hy vọng điều này sẽ cung cấp cho bạn một số ý tưởng để đánh răng vào. :)
Nếu bạn thường quan tâm đến điều này, tôi khuyên bạn nên học code generation with annotation processing, bởi vì bạn có thể tạo ra những thứ như thế này dễ dàng hơn nhiều so với viết chúng bằng tay. Như chúng ta đã nói trong phần bình luận, việc viết những thứ như thế này bằng tay trở nên không thực tế một cách nhanh chóng.
* "unmaintainable nếu chúng tôi mong đợi nhiều thông số chung hơn chỉ hai" * Nếu bạn muốn giữ lại thứ tự tùy ý (trong ví dụ của bạn, bạn có thể làm cả hai 'withTyped (...). WithList (...)' * và * 'withList (...). withTyped (...)') sau đó vấn đề trở nên thực sự khó khăn bởi vì bạn kết thúc với một cái gì đó giống như 'n!' lớp, trong đó 'n' là số tham số kiểu. Nếu bạn thực hiện một cách tiếp cận xây dựng bước truyền thống hơn thì nó đơn giản hơn một chút. – Radiodef
@Radiodef Tôi nghĩ rằng 2^n lớp: tại bất kỳ điểm nào, mọi loại chung có thể ở một trong hai trạng thái: đã được xác định hoặc chưa được xác định. Nhưng có, đây là một nhược điểm nghiêm trọng của việc thực hiện "thủ công" đó. Đó là lý do tại sao tôi tự hỏi nếu một giải pháp tốt hơn tồn tại; có lẽ tận dụng các ràng buộc chung trong một số cách thông minh. –