Một getter nên ném một ngoại lệ nếu đối tượng của nó có trạng thái không hợp lệ?


55

Tôi thường gặp phải vấn đề này, đặc biệt là trong Java, ngay cả khi tôi nghĩ đó là vấn đề OOP chung. Đó là: đưa ra một ngoại lệ cho thấy một vấn đề thiết kế.

Giả sử rằng tôi có một lớp có một String nametrường và một String surnametrường.

Sau đó, nó sử dụng các trường đó để soạn tên đầy đủ của một người để hiển thị nó trên một loại tài liệu, giả sử một hóa đơn.

public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}

Bây giờ tôi muốn tăng cường hành vi của lớp mình bằng cách đưa ra một lỗi nếu displayCompleteNameOnInvoiceđược gọi trước khi tên được gán. Có vẻ là một ý tưởng tốt, phải không?

Tôi có thể thêm một mã tăng ngoại lệ vào getCompleteNamephương thức. Nhưng theo cách này, tôi đang vi phạm hợp đồng 'ngầm' với người dùng lớp; nói chung, các getters không được phép ném ngoại lệ nếu giá trị của chúng không được đặt. Ok, đây không phải là một getter tiêu chuẩn vì nó không trả về một trường đơn lẻ, nhưng theo quan điểm của người dùng, sự khác biệt có thể quá tinh tế để nghĩ về nó.

Hoặc tôi có thể ném ngoại lệ từ bên trong displayCompleteNameOnInvoice. Nhưng để làm như vậy tôi nên kiểm tra trực tiếp namehoặc surnamecác trường và làm như vậy tôi sẽ vi phạm sự trừu tượng được đại diện bởi getCompleteName. Phương pháp này có trách nhiệm kiểm tra và tạo tên đầy đủ. Nó thậm chí có thể quyết định, dựa trên quyết định về dữ liệu khác, trong một số trường hợp, nó là đủ surname.

Vì vậy, khả năng duy nhất dường như thay đổi ngữ nghĩa của phương thức getCompleteNamethành composeCompleteName, điều đó cho thấy một hành vi 'chủ động' hơn và, với nó, khả năng đưa ra một ngoại lệ.

Đây có phải là giải pháp thiết kế tốt hơn? Tôi luôn tìm kiếm sự cân bằng tốt nhất giữa sự đơn giản và chính xác. Có một tài liệu tham khảo thiết kế cho vấn đề này?


8
"nói chung, các getters không được phép đưa ra ngoại lệ nếu giá trị của chúng không được đặt." - Họ phải làm gì sau đó?
Rotem

2
@scriptin Sau đó, theo logic đó, displayCompleteNameOnInvoicechỉ có thể ném một ngoại lệ nếu getCompleteNametrả về null, phải không?
Rotem

2
@Rotem nó có thể. Câu hỏi là về nếu nó nên.
scriptin

22
Về chủ đề này, các lập trình viên của chương trình Sai lầm tin vào tên là một cách đọc rất hay về lý do tại sao 'tên đầu tiên' và 'họ' là những khái niệm rất hẹp.
Lars Viklund

9
Nếu đối tượng của bạn có thể có trạng thái không hợp lệ, bạn đã làm hỏng việc.
dùng253751

Câu trả lời:


151

Không cho phép lớp của bạn được xây dựng mà không gán tên.


9
Đó là một giải pháp triệt để nhưng thú vị. Nhiều lần các lớp học của bạn phải trôi chảy hơn, cho phép các trạng thái trở thành vấn đề chỉ khi và khi một số phương thức nhất định được gọi, nếu không, bạn sẽ kết thúc với một proliferaiton của các lớp nhỏ yêu cầu sử dụng quá nhiều giải thích.
AgostinoX

71
Đây không phải là một bình luận. Nếu anh ta áp dụng lời khuyên trong câu trả lời, anh ta sẽ không còn vấn đề được mô tả nữa. Đó là một câu trả lời.
DeadMG

14
@AgostinoX: Bạn chỉ nên làm cho lớp học của bạn trôi chảy nếu thực sự cần thiết. Nếu không, chúng nên bất biến nhất có thể.
DeadMG

25
@gnat: bạn thực hiện một công việc tuyệt vời ở đây đặt câu hỏi về chất lượng của mọi câu hỏi và mọi câu trả lời trên PSE, nhưng đôi khi bạn đang IMHO hơi quá nitty.
Doc Brown

9
Nếu chúng có thể là tùy chọn hợp lệ, không có lý do gì để anh ta ném vào vị trí đầu tiên.
DeadMG

75

Câu trả lời được cung cấp bởi DeadMG khá nhiều, nhưng hãy để tôi diễn đạt nó một chút khác biệt: Tránh để các đối tượng có trạng thái không hợp lệ. Một đối tượng nên là "toàn bộ" trong bối cảnh của nhiệm vụ mà nó hoàn thành. Hoặc nó không nên tồn tại.

Vì vậy, nếu bạn muốn lưu loát, hãy sử dụng một cái gì đó giống như Mô hình Builder . Hoặc bất cứ điều gì khác là sự thống nhất riêng biệt của một đối tượng trong xây dựng trái ngược với "thực tế", trong đó đối tượng sau được đảm bảo có trạng thái hợp lệ và là đối tượng thực sự phơi bày các hoạt động được xác định trên trạng thái đó (ví dụ getCompleteName).


7
So if you want fluidity, use something like the builder pattern- tính trôi chảy, hoặc bất biến (trừ khi đó là những gì bạn muốn nói). Nếu một người muốn tạo các đối tượng bất biến, nhưng cần trì hoãn việc thiết lập một số giá trị, thì mẫu cần tuân theo là đặt từng cái một trên một đối tượng xây dựng và chỉ tạo một đối tượng bất biến khi chúng ta đã thu thập tất cả các giá trị cần thiết cho chính xác bang ...
Konrad Morawski

1
@KonradMorawski: Tôi bối rối. Bạn đang làm rõ hoặc mâu thuẫn với tuyên bố của tôi? ;)
back2dos

3
Tôi đã cố gắng bổ sung cho nó ...
Konrad Morawski

40

Đừng có một đối tượng với trạng thái không hợp lệ.

Mục đích của một nhà xây dựng hoặc nhà xây dựng là đặt trạng thái của đối tượng thành một cái gì đó phù hợp và có thể sử dụng được. Không có sự đảm bảo này, các vấn đề sẽ xuất hiện với trạng thái khởi tạo không đúng trong các đối tượng. Vấn đề này trở nên phức tạp nếu bạn đang xử lý đồng thời và một cái gì đó có thể truy cập vào đối tượng trước khi bạn hoàn thành việc thiết lập nó.

Vì vậy, câu hỏi cho phần này là "tại sao bạn cho phép tên hoặc họ là null?" Nếu đó không phải là thứ hợp lệ trong lớp để nó hoạt động bình thường, đừng cho phép nó. Có một nhà xây dựng hoặc nhà xây dựng xác nhận hợp lệ việc tạo đối tượng và nếu nó không đúng, hãy nêu vấn đề tại thời điểm đó. Bạn cũng có thể muốn sử dụng một trong các @NotNullchú thích tồn tại để giúp truyền đạt rằng "điều này không thể là không" và thực thi nó trong mã hóa và phân tích.

Với sự đảm bảo này, việc lập luận về mã trở nên dễ dàng hơn nhiều, nó làm gì và không phải ném ngoại lệ vào những nơi kỳ quặc hoặc đặt kiểm tra quá mức xung quanh các hàm getter.

Getters mà làm nhiều hơn.

Có khá nhiều về chủ đề này. Bạn đã có bao nhiêu Logic trong Gettersnhững gì nên được cho phép bên trong getters và setters? từ đây và các getters và setters thực hiện logic bổ sung trên Stack Overflow. Đây là một vấn đề xuất hiện nhiều lần trong thiết kế lớp.

Cốt lõi của điều này đến từ:

nói chung, các getters không được phép đưa ra ngoại lệ nếu giá trị của chúng không được đặt

Và bạn nói đúng không. Họ không nên. Họ nên trông "ngớ ngẩn" với phần còn lại của thế giới và làm những gì được mong đợi. Đặt quá nhiều logic vào đó dẫn đến các vấn đề mà luật ít ngạc nhiên nhất bị vi phạm. Và hãy đối mặt với điều đó, bạn thực sự không muốn bao bọc các getters try catchbởi vì chúng ta biết chúng ta yêu thích việc đó nói chung như thế nào.

Cũng có những tình huống bạn phải sử dụng một getFoophương thức, chẳng hạn như khung công tác JavaBeans và khi bạn có thứ gì đó từ EL gọi để mong có được một bean (để <% bar.foo %>thực sự gọi getFoo()trong lớp - hãy đặt sang một bên 'bean nên thực hiện thành phần hoặc nên có được để lại cho xem không? 'bởi vì người ta có thể dễ dàng đưa ra các tình huống trong đó người này hay người kia rõ ràng có thể là câu trả lời đúng)

Cũng nhận ra rằng một trình getter đã cho có thể được chỉ định trong giao diện hoặc là một phần của API công khai được hiển thị trước đó cho lớp đang được cấu trúc lại (một phiên bản trước đó có 'CompleteName' đã được trả về và việc tái cấu trúc bị phá vỡ nó thành hai lĩnh vực).

Vào cuối ngày...

Làm điều dễ nhất để lý do về. Bạn sẽ dành nhiều thời gian hơn để duy trì mã này hơn là bạn sẽ dành thời gian để thiết kế nó (mặc dù bạn càng dành ít thời gian để thiết kế, bạn sẽ càng dành nhiều thời gian để duy trì). Lựa chọn lý tưởng là thiết kế nó theo cách mà nó sẽ không mất nhiều thời gian để duy trì, nhưng đừng ngồi đó suy nghĩ về nó trong nhiều ngày - trừ khi đây thực sự là một lựa chọn thiết kế sẽ có nhiều ngày có ý nghĩa sau này .

Cố gắng gắn bó với sự thuần khiết về ngữ nghĩa của người private T foo; T getFoo() { return foo; }nhận được sẽ khiến bạn gặp rắc rối vào một lúc nào đó. Đôi khi mã không phù hợp với mô hình đó và các mâu thuẫn mà người ta trải qua để cố gắng giữ nó theo cách đó chỉ không có ý nghĩa ... và cuối cùng làm cho việc thiết kế khó khăn hơn.

Đôi khi chấp nhận rằng những lý tưởng của thiết kế không thể được nhận ra theo cách bạn muốn chúng trong mã. Hướng dẫn là hướng dẫn - không phải xiềng xích.


2
Rõ ràng ví dụ của tôi chỉ là một cái cớ để cải thiện phong cách mã hóa bằng cách suy luận về nó, không phải là một vấn đề ngăn chặn. Nhưng tôi khá bối rối bởi tầm quan trọng mà bạn và những người khác dường như dành cho các nhà xây dựng. Theo lý thuyết họ giữ lời hứa. Trong thực tế, chúng trở nên cồng kềnh với sự kế thừa, không thể sử dụng được khi bạn trích xuất một giao diện từ một lớp, không được javabeans và các khung công tác khác như mùa xuân chấp nhận nếu tôi không sai. Dưới cái ô của một ý tưởng 'đẳng cấp' sống nhiều loại đối tượng khác nhau. Một đối tượng giá trị có thể có một hàm tạo xác nhận hợp lệ, nhưng đây chỉ là một loại.
AgostinoX

2
Có thể suy luận về mã là chìa khóa để có thể viết mã bằng API hoặc có thể duy trì mã. Nếu một lớp có một constructor cho phép nó được trong tình trạng không hợp lệ nó làm cho nó nhiều khó khăn hơn để suy nghĩ thấu đáo "tốt, điều này làm gì?" bởi vì bây giờ bạn phải xem xét nhiều tình huống hơn người thiết kế của lớp được xem xét hoặc dự định. Tải khái niệm thêm này đi kèm với hình phạt của nhiều lỗi hơn và thời gian viết mã lâu hơn. Mặt khác, nếu bạn có thể nhìn vào mã và nói "tên và họ sẽ không bao giờ là null", việc viết mã bằng cách sử dụng chúng sẽ trở nên dễ dàng hơn nhiều.

2
Không đầy đủ không có nghĩa là không hợp lệ. Một hóa đơn không có biên nhận có thể được tiến hành và API công khai mà nó xuất trình sẽ phản ánh rằng đó là trạng thái hợp lệ. Nếu surnamehoặc namelà null là trạng thái hợp lệ cho đối tượng, thì xử lý nó cho phù hợp. Nhưng nếu đó không phải là trạng thái hợp lệ - khiến các phương thức trong lớp đưa ra các ngoại lệ, thì nó nên được ngăn chặn ở trạng thái đó từ thời điểm nó được khởi tạo. Bạn có thể vượt qua các đối tượng không hoàn chỉnh, nhưng vượt qua những đối tượng không hợp lệ là vấn đề. Nếu một tên họ null là ngoại lệ, hãy cố gắng ngăn chặn nó sớm hơn sau này.

2
Một bài đăng blog có liên quan để đọc: Thiết kế khởi tạo đối tượng và lưu ý bốn cách tiếp cận để khởi tạo một biến thể hiện. Một trong số đó là cho phép nó ở trạng thái không hợp lệ, mặc dù lưu ý rằng nó ném ngoại lệ. Nếu bạn đang cố gắng tránh điều này, cách tiếp cận đó không phải là một cách hợp lệ và bạn sẽ cần phải làm việc với các cách tiếp cận khác - liên quan đến việc đảm bảo một đối tượng hợp lệ mọi lúc. Từ blog: "Các đối tượng có thể có trạng thái không hợp lệ sẽ khó sử dụng và khó hiểu hơn so với những đối tượng luôn hợp lệ." và đó là chìa khóa.

5
"Làm điều dễ nhất để lý luận về." Điều đó. Đó
Ben

5

Tôi không phải là một người Java, nhưng điều này dường như tuân thủ cả hai ràng buộc mà bạn đã trình bày.

getCompleteNamekhông ném ngoại lệ nếu tên không được khởi tạo và displayCompleteNameOnInvoicekhông.

public String getCompleteName()
{
    if (name == null || surname == null)
    {
        return null;
    }
    return name + " " + surname;
}

public void displayCompleteNameOnInvoice() 
{
    String completeName = getCompleteName();
    if (completeName == null)
    {
        //throw an exception.
    }
    //do something with it....
}

Để mở rộng lý do tại sao đây là một giải pháp chính xác: Bằng cách này, người gọi getCompleteName()không cần quan tâm nếu tài sản thực sự được lưu trữ hoặc nếu nó được tính toán nhanh chóng.
Bart van Ingen Schenau

18
Để mở rộng lý do tại sao giải pháp này là điên rồ, bạn phải kiểm tra tính vô hiệu trên mỗi cuộc gọi đến getCompleteName, điều này thật đáng ghét.
DeadMG

Không, @DeadMG, bạn chỉ phải kiểm tra nó trong trường hợp nên ném ngoại lệ nếu getCompleteName trả về null. Đó là cách nó nên được. Giải pháp này là chính xác.
Dawood ibn Kareem

1
@DeadMG Có, nhưng chúng tôi không biết những gì KHÁC sử dụng getCompleteName()trong phần còn lại của lớp học, hoặc thậm chí trong các phần khác của chương trình. Trong một số trường hợp, trả lại nullcó thể chính xác những gì được yêu cầu. Nhưng trong khi có MỘT phương thức có thông số kỹ thuật là "ném ngoại lệ trong trường hợp này", thì đó chính xác là những gì chúng ta nên mã hóa.
Dawood ibn Kareem

4
Có thể có những tình huống đây là giải pháp tốt nhất, nhưng tôi không thể tưởng tượng được. Đối với hầu hết các trường hợp, điều này có vẻ quá phức tạp: Một phương thức ném với dữ liệu không đầy đủ, một phương thức khác trả về null. Tôi e rằng điều này có thể được sử dụng không chính xác bởi người gọi.
sleske

4

Có vẻ như không ai trả lời câu hỏi của bạn.
Không giống như những gì mọi người muốn tin, "tránh các đối tượng không hợp lệ" thường không phải là một giải pháp thực tế.

Câu trả lời là:

Có nó nên.

Ví dụ dễ dàng: FileStream.Lengthtrong C # /. NET , sẽ ném NotSupportedExceptionnếu tệp không thể tìm kiếm được.


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
maple_shaft

1

Bạn có toàn bộ điều kiểm tra tên lộn ngược.

getCompleteName()không cần phải biết chức năng nào sẽ được sử dụng. Vì vậy, không có một namekhi gọi displayCompleteNameOnInvoice()getCompleteName()trách nhiệm.

Nhưng, namelà một điều kiện tiên quyết rõ ràng cho displayCompleteNameOnInvoice(). Vì vậy, cần có trách nhiệm kiểm tra trạng thái (Hoặc nên ủy thác trách nhiệm kiểm tra cho phương pháp khác, nếu bạn thích).

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.