Mô hình xây dựng: Khi nào thất bại?


45

Khi triển khai Mô hình Trình tạo, tôi thường cảm thấy bối rối khi phải để việc xây dựng thất bại và tôi thậm chí còn xoay sở để có những lập trường khác nhau về vấn đề này cứ sau vài ngày.

Đầu tiên một số giải thích:

  • Với việc thất bại sớm tôi có nghĩa là việc xây dựng một đối tượng sẽ thất bại ngay khi một tham số không hợp lệ được truyền vào. Vì vậy, bên trong SomeObjectBuilder.
  • Với việc thất bại muộn tôi có nghĩa là việc xây dựng một đối tượng chỉ có thể thất bại trong build()cuộc gọi mà gọi ngầm là một nhà xây dựng của đối tượng sẽ được xây dựng.

Sau đó, một số đối số:

  • Có lợi cho việc thất bại muộn: Một lớp xây dựng không nên nhiều hơn một lớp chỉ giữ các giá trị. Hơn nữa, nó dẫn đến việc sao chép mã ít hơn.
  • Có lợi cho việc thất bại sớm: Cách tiếp cận chung trong lập trình phần mềm là bạn muốn phát hiện các vấn đề càng sớm càng tốt và do đó, vị trí hợp lý nhất để kiểm tra sẽ là trong lớp xây dựng, 'setters' và cuối cùng là trong phương thức xây dựng.

Sự đồng thuận chung về điều này là gì?


8
Tôi không thấy bất kỳ lợi thế nào để thất bại muộn. Điều mà ai đó nói rằng một lớp xây dựng "nên" không được ưu tiên so với thiết kế tốt và việc bắt lỗi sớm luôn tốt hơn bắt lỗi muộn.
Doval

3
Một cách khác để xem xét điều này là người xây dựng có thể không biết dữ liệu hợp lệ là gì. Thất bại sớm trong trường hợp này là về thất bại ngay khi bạn biết có lỗi. Không thất bại sớm, sẽ là người xây dựng trả lại một nullđối tượng khi có vấn đề build().
Chris

Nếu bạn không thêm một cách để đưa ra cảnh báo và cung cấp phương tiện để khắc phục trong trình xây dựng thì không có lý do gì để thất bại muộn.
Đánh dấu

Câu trả lời:


34

Hãy xem xét các tùy chọn, nơi chúng ta có thể đặt mã xác thực:

  1. Bên trong các setters trong xây dựng.
  2. Bên trong build()phương pháp.
  3. Bên trong thực thể được xây dựng: nó sẽ được gọi trong build()phương thức khi thực thể được tạo.

Tùy chọn 1 cho phép chúng tôi phát hiện các vấn đề sớm hơn, nhưng có thể có các trường hợp phức tạp khi chúng tôi có thể xác thực đầu vào chỉ có bối cảnh đầy đủ, do đó, thực hiện ít nhất một phần xác thực trong build()phương thức. Do đó, việc chọn tùy chọn 1 sẽ dẫn đến mã không nhất quán với một phần xác thực được thực hiện ở một nơi và một phần khác được thực hiện ở nơi khác.

Tùy chọn 2 không tệ hơn đáng kể so với tùy chọn 1, bởi vì, thông thường, setters trong trình xây dựng được gọi ngay trước khi build(), đặc biệt, trong các giao diện trôi chảy. Vì vậy, vẫn có thể phát hiện sớm một vấn đề đủ sớm trong hầu hết các trường hợp. Tuy nhiên, nếu trình xây dựng không phải là cách duy nhất để tạo một đối tượng, nó sẽ dẫn đến sự trùng lặp mã xác thực, bởi vì bạn sẽ cần phải có nó ở mọi nơi mà bạn tạo một đối tượng. Giải pháp hợp lý nhất trong trường hợp này sẽ là đặt xác nhận càng gần đối tượng được tạo càng tốt, nghĩa là bên trong nó. Và đây là tùy chọn 3 .

Từ quan điểm RẮN, việc xác thực trong trình xây dựng cũng vi phạm SRP: lớp trình xây dựng đã có trách nhiệm tổng hợp dữ liệu để xây dựng một đối tượng. Xác nhận là thiết lập hợp đồng trên trạng thái nội bộ của chính nó, trách nhiệm mới là kiểm tra trạng thái của một đối tượng khác.

Do đó, theo quan điểm của tôi, không chỉ tốt hơn là thất bại muộn từ quan điểm thiết kế, mà còn tốt hơn là thất bại bên trong thực thể được xây dựng, thay vì trong chính người xây dựng.

CẬP NHẬT: nhận xét này nhắc nhở tôi về một khả năng nữa, khi xác nhận bên trong trình xây dựng (tùy chọn 1 hoặc 2) có ý nghĩa. Sẽ có ý nghĩa nếu người xây dựng có hợp đồng riêng của mình trên các đối tượng mà nó đang tạo. Ví dụ: giả sử rằng chúng ta có một trình xây dựng xây dựng một chuỗi với nội dung cụ thể, giả sử, danh sách các phạm vi số 1-2,3-4,5-6. Trình xây dựng này có thể có một phương thức như thế nào addRange(int min, int max). Chuỗi kết quả không biết gì về những con số này, cũng không cần phải biết. Trình xây dựng tự xác định định dạng của chuỗi và các ràng buộc trên các số. Do đó, phương thức addRange(int,int)phải xác thực các số đầu vào và đưa ra một ngoại lệ nếu max nhỏ hơn min.

Điều đó nói rằng, quy tắc chung sẽ chỉ xác nhận các hợp đồng được xác định bởi chính người xây dựng.


Tôi nghĩ rằng đáng chú ý là trong khi Phương án 1 có thể dẫn đến thời gian kiểm tra "không nhất quán", nó vẫn có thể được xem là nhất quán nếu mọi thứ "càng sớm càng tốt". Thật dễ dàng hơn một chút để làm cho "càng sớm càng tốt" rõ ràng hơn nếu biến thể của trình xây dựng, StepBuilder, được sử dụng thay thế.
Joshua Taylor

Nếu trình xây dựng URI ném ngoại lệ nếu chuỗi null được thông qua, đây có phải là vi phạm RẮN không? Rác
Gusdor

@Gusdor có, nếu nó tự ném một ngoại lệ. Tuy nhiên, từ quan điểm của người dùng, tất cả các tùy chọn trông giống như một ngoại lệ được ném bởi một người xây dựng.
Ivan Gammel

Vậy tại sao không có một xác thực () được gọi bởi build ()? Bằng cách đó, có rất ít sự trùng lặp, tính nhất quán và không vi phạm SRP. Nó cũng cho phép xác thực dữ liệu mà không cần cố gắng xây dựng và xác thực gần với việc tạo.
StellarVortex

@StellarVortex trong trường hợp này, nó sẽ được xác thực hai lần - một lần trong builder.build () và, nếu dữ liệu hợp lệ và chúng tôi tiến hành xây dựng đối tượng, trong hàm tạo đó.
Ivan Gammel

34

Cho rằng bạn sử dụng Java, hãy xem xét hướng dẫn chi tiết và có thẩm quyền do Joshua Bloch cung cấp trong bài viết Tạo và hủy các đối tượng Java (phông chữ đậm trong trích dẫn dưới đây là của tôi):

Giống như một constructor, một người xây dựng có thể áp đặt các bất biến lên các tham số của nó. Phương thức xây dựng có thể kiểm tra các bất biến này. Điều quan trọng là chúng phải được kiểm tra sau khi sao chép các tham số từ trình tạo vào đối tượng và chúng được kiểm tra trên các trường đối tượng chứ không phải các trường của trình tạo (Mục 39). Nếu bất kỳ bất biến nào bị vi phạm, phương thức xây dựng sẽ ném IllegalStateException(Mục 60). Phương pháp chi tiết của ngoại lệ sẽ chỉ ra bất biến nào bị vi phạm (Mục 63).

Một cách khác để áp đặt các bất biến liên quan đến nhiều tham số là các phương thức setter lấy toàn bộ các nhóm tham số mà một số bất biến phải giữ. Nếu bất biến không thỏa mãn, phương thức setter sẽ ném một IllegalArgumentException. Điều này có lợi thế là phát hiện lỗi bất biến ngay khi các tham số không hợp lệ được thông qua, thay vì chờ bản dựng được gọi.

Lưu ý theo giải thích của biên tập viên trong bài viết này, "các mục" trong trích dẫn ở trên đề cập đến các quy tắc được trình bày trong Java hiệu quả, Ấn bản thứ hai .

Bài viết không đi sâu vào giải thích lý do tại sao điều này được khuyến nghị, nhưng nếu bạn nghĩ về nó, những lý do khá rõ ràng. Mẹo chung để hiểu điều này được cung cấp ngay trong bài viết, trong phần giải thích cách khái niệm trình xây dựng được kết nối với hàm tạo - và các bất biến lớp được dự kiến ​​sẽ được kiểm tra trong hàm tạo, không phải trong bất kỳ mã nào khác có thể đi trước / chuẩn bị lời gọi của nó.

Để hiểu rõ hơn về lý do tại sao việc kiểm tra các bất biến trước khi gọi một bản dựng sẽ là sai, hãy xem xét một ví dụ phổ biến về CarBuilder . Các phương thức của trình tạo có thể được gọi theo thứ tự tùy ý và kết quả là, người ta không thể thực sự biết liệu tham số cụ thể có hợp lệ cho đến khi xây dựng hay không.

Hãy xem xét rằng chiếc xe thể thao không thể có nhiều hơn 2 chỗ ngồi, làm sao người ta có thể biết liệu setSeats(4)có ổn hay không? Nó chỉ ở bản dựng khi người ta có thể biết chắc chắn liệu có setSportsCar()được gọi hay không, nghĩa là có ném TooManySeatsExceptionhay không.


3
+1 để đề xuất loại ngoại lệ nào cần ném, chính xác là những gì tôi đang tìm kiếm.
Xantix

Không chắc chắn tôi có được sự thay thế. Nó dường như được nói hoàn toàn khi bất biến chỉ có thể được xác nhận trong các nhóm. Trình xây dựng chấp nhận các thuộc tính đơn khi chúng không liên quan đến bất kỳ thuộc tính nào khác và chỉ chấp nhận các nhóm thuộc tính khi nhóm có bất biến. Trong trường hợp này, thuộc tính đơn lẻ có nên ném ngoại lệ trước khi xây dựng không?
Didier A.

19

Theo tôi, các giá trị không hợp lệ không hợp lệ vì chúng không được chấp nhận. Nói cách khác, nếu bạn chỉ chấp nhận các số dương và một số âm được truyền vào, thì không cần phải đợi cho đến khi build()được gọi. Tôi sẽ không xem xét các loại vấn đề mà bạn sẽ "mong đợi" xảy ra, vì đó là điều kiện tiên quyết để gọi phương thức bắt đầu. Nói cách khác, bạn có thể sẽ không phụ thuộc vào việc không thiết lập các tham số nhất định. Nhiều khả năng bạn sẽ cho rằng các tham số là chính xác hoặc bạn sẽ tự kiểm tra.

Tuy nhiên, đối với các vấn đề phức tạp hơn mà không dễ xác thực có thể tốt hơn nên được biết khi bạn gọi build(). Một ví dụ điển hình cho việc này có thể là sử dụng thông tin kết nối bạn cung cấp để thiết lập kết nối tới cơ sở dữ liệu. Trong trường hợp này, về mặt kỹ thuật bạn có thể kiểm tra các điều kiện như vậy, nó không còn trực quan nữa và nó chỉ làm phức tạp mã của bạn. Như tôi thấy, đây cũng là những loại vấn đề có thể thực sự xảy ra và bạn không thể thực sự lường trước cho đến khi bạn thử nó. Đó là sự khác biệt giữa việc kết hợp một chuỗi với một biểu thức chính quy để xem liệu nó có thể được phân tích cú pháp như một int và chỉ đơn giản là cố gắng phân tích nó, xử lý bất kỳ trường hợp ngoại lệ tiềm ẩn nào có thể xảy ra do hậu quả.

Tôi thường không thích ném ngoại lệ khi cài đặt tham số vì điều đó có nghĩa là phải bắt bất kỳ ngoại lệ nào, vì vậy tôi có xu hướng ủng hộ xác nhận build(). Vì vậy, vì lý do này, tôi thích sử dụng RuntimeException hơn một lần nữa, lỗi trong các tham số được truyền thường không nên xảy ra.

Tuy nhiên, đây là một thực hành tốt nhất hơn bất cứ điều gì. Tôi hy vọng trả lời câu hỏi của bạn.


11

Theo như tôi biết, thực tế chung (không chắc có đồng thuận hay không) sẽ thất bại ngay khi bạn có thể phát hiện ra lỗi một cách khả thi. Điều này cũng khiến việc vô tình lạm dụng API của bạn trở nên khó khăn hơn.

Nếu đó là một thuộc tính tầm thường có thể được kiểm tra trên đầu vào, chẳng hạn như dung lượng hoặc độ dài không âm, thì tốt nhất bạn nên thất bại ngay lập tức. Giữ lỗi làm tăng khoảng cách giữa lỗi và phản hồi, điều này khiến việc tìm ra nguồn gốc của vấn đề trở nên khó khăn hơn.

Nếu bạn gặp bất hạnh khi ở trong tình huống mà tính hợp lệ của một thuộc tính phụ thuộc vào người khác, thì bạn có hai lựa chọn:

  • Yêu cầu cả hai (hoặc nhiều) thuộc tính được cung cấp đồng thời (nghĩa là gọi phương thức đơn).
  • Kiểm tra tính hợp lệ ngay khi bạn biết không có thêm thay đổi nào được gửi đến: khi nào build()được gọi như vậy.

Như với hầu hết mọi thứ, đây là một quyết định được đưa ra trong một bối cảnh. Nếu bối cảnh làm cho nó lúng túng hoặc phức tạp để thất bại sớm, một sự đánh đổi có thể được thực hiện để trì hoãn kiểm tra đến lần sau, nhưng không nhanh chóng nên được mặc định.


Vì vậy, để tóm tắt, bạn đang nói rằng có hợp lý để xác nhận càng sớm càng tốt mọi thứ có thể được bao phủ trong một loại đối tượng / nguyên thủy? Giống như unsigned, @NonNullv.v.
skiwi

2
@skiwi Khá nhiều, đúng vậy. Kiểm tra tên miền, kiểm tra null, loại đó. Tôi sẽ không ủng hộ việc đặt nhiều hơn thế nữa: các nhà xây dựng nói chung là những điều đơn giản.
JvR

1
Có thể đáng lưu ý rằng nếu tính hợp lệ của một tham số phụ thuộc vào giá trị của một tham số khác, thì người ta chỉ có thể từ chối một cách hợp pháp một giá trị tham số nếu người ta biết rằng tham số kia "thực sự" được thiết lập . Nếu được phép đặt giá trị tham số nhiều lần [với cài đặt cuối cùng được ưu tiên], thì trong một số trường hợp, cách tự nhiên nhất để thiết lập một đối tượng có thể là đặt tham số Xthành giá trị không hợp lệ với giá trị hiện tại là Y, nhưng trước khi gọi build()được đặt Ythành một giá trị sẽ làm cho Xhợp lệ.
supercat

Nếu ví dụ một là xây dựng một Shapevà những người xây dựng có WithLeftWithRighttài sản, và một mong muốn để điều chỉnh một người thợ xây để xây dựng một đối tượng ở một nơi khác nhau, đòi hỏi mà WithRightđược gọi đầu tiên khi di chuyển một đối tượng bên phải, và WithLeftkhi di chuyển nó sang trái, sẽ làm tăng thêm sự phức tạp không cần thiết so với việc cho phép WithLeftđặt cạnh trái sang bên phải của cạnh phải cũ với điều kiện phải WithRightsửa cạnh phải trước khi buildđược gọi.
supercat

0

Nguyên tắc cơ bản là "thất bại sớm".

Quy tắc nâng cao hơn một chút là "thất bại càng sớm càng tốt".

Nếu một tài sản thực chất không hợp lệ ...

CarBuilder.numberOfWheels( -1 ). ...  

... Sau đó bạn từ chối nó ngay lập tức.

Các trường hợp khác có thể cần các giá trị được kiểm tra kết hợp và có thể được đặt tốt hơn trong phương thức build ():

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
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.