Là ném một ngoại lệ là một mô hình chống ở đây?


33

Tôi vừa có một cuộc thảo luận về lựa chọn thiết kế sau khi xem xét mã. Tôi tự hỏi ý kiến ​​của bạn là gì.

Preferenceslớp này , là một nhóm cho các cặp khóa-giá trị. Giá trị Null là hợp pháp (đó là quan trọng). Chúng tôi hy vọng rằng một số giá trị nhất định có thể chưa được lưu và chúng tôi muốn tự động xử lý các trường hợp này bằng cách khởi tạo chúng với giá trị mặc định được xác định trước khi được yêu cầu.

Giải pháp thảo luận được sử dụng mẫu sau (LƯU Ý: đây không phải là mã thực tế, rõ ràng - nó được đơn giản hóa cho mục đích minh họa):

public class Preferences {
    // null values are legal
    private Map<String, String> valuesFromDatabase;

    private static Map<String, String> defaultValues;

    class KeyNotFoundException extends Exception {
    }

    public String getByKey(String key) {
        try {
            return getValueByKey(key);
        } catch (KeyNotFoundException e) {
            String defaultValue = defaultValues.get(key);
            valuesFromDatabase.put(key, defaultvalue);
            return defaultValue;
        }
    }

    private String getValueByKey(String key) throws KeyNotFoundException {
        if (valuesFromDatabase.containsKey(key)) {
            return valuesFromDatabase.get(key);
        } else {
            throw new KeyNotFoundException();
        }
    }
}

Nó đã bị chỉ trích là một mô hình chống - lạm dụng các ngoại lệ để kiểm soát dòng chảy . KeyNotFoundException- được đưa vào cuộc sống chỉ với một trường hợp sử dụng đó - sẽ không bao giờ được nhìn thấy ngoài phạm vi của lớp này.

Về cơ bản, đó là hai phương pháp chơi tìm nạp với nó chỉ đơn thuần là để giao tiếp với nhau.

Khóa không có trong cơ sở dữ liệu không phải là điều đáng báo động, hoặc ngoại lệ - chúng tôi hy vọng điều này xảy ra bất cứ khi nào cài đặt tùy chọn mới được thêm vào, do đó cơ chế khởi tạo nó một cách duyên dáng với giá trị mặc định nếu cần.

Phản biện là getValueByKey- phương thức riêng tư - như được định nghĩa ngay bây giờ không có cách thông báo tự nhiên nào về phương thức công khai về cả giá trị, và liệu khóa có ở đó không. (Nếu không, nó phải được thêm vào để có thể cập nhật giá trị).

Trả lại nullsẽ là mơ hồ, vì nulllà một giá trị pháp lý hoàn hảo, vì vậy không có ý nghĩa cho dù nó có nghĩa là chìa khóa không có ở đó, hoặc nếu có null.

getValueByKeysẽ phải trả về một số loại a Tuple<Boolean, String>, bool được đặt thành true nếu khóa đã có sẵn, để chúng ta có thể phân biệt giữa (true, null)(false, null). (Một outtham số có thể được sử dụng trong C #, nhưng đó là Java).

Đây có phải là một thay thế đẹp hơn? Có, bạn sẽ phải định nghĩa một số lớp sử dụng một lần cho hiệu quả của nó Tuple<Boolean, String>, nhưng sau đó chúng ta sẽ loại bỏ KeyNotFoundException, vì vậy loại cân bằng đó không còn nữa. Chúng tôi cũng tránh được việc xử lý một ngoại lệ, mặc dù về mặt thực tế không có ý nghĩa gì - không có cân nhắc về hiệu suất, đó là ứng dụng khách và không giống như sở thích của người dùng sẽ được truy xuất hàng triệu lần mỗi giây.

Một biến thể của phương pháp này có thể sử dụng Guava Optional<String>(Guava đã được sử dụng trong suốt dự án) thay vì một số tùy chỉnh Tuple<Boolean, String>, và sau đó chúng ta có thể phân biệt giữa Optional.<String>absent()và "phù hợp" null. Mặc dù vậy, nó vẫn cảm thấy bị hack, vì những lý do dễ thấy - giới thiệu hai cấp độ "vô hiệu" dường như lạm dụng khái niệm đứng đằng sau việc tạo Optionals ở vị trí đầu tiên.

Một tùy chọn khác là kiểm tra rõ ràng liệu khóa có tồn tại hay không (chỉ thêm một boolean containsKey(String key)phương thức và gọi getValueByKeynếu chúng tôi đã xác nhận rằng nó tồn tại).

Cuối cùng, người ta cũng có thể nội tuyến phương thức riêng tư, nhưng thực tế getByKeycó phần phức tạp hơn mẫu mã của tôi, do đó, nội tuyến sẽ làm cho nó trông khá khó chịu.

Tôi có thể đang chia tóc ở đây, nhưng tôi tò mò không biết bạn sẽ đặt cược gì để gần nhất với thực tiễn tốt nhất trong trường hợp này. Tôi đã không tìm thấy câu trả lời trong hướng dẫn về phong cách của Oracle hoặc của Google.

Việc sử dụng các ngoại lệ như trong mẫu mã có phải là mẫu chống lại không, hoặc có thể chấp nhận được không khi các lựa chọn thay thế cũng không được sạch sẽ? Nếu là nó, trong hoàn cảnh nào nó sẽ ổn? Và ngược lại?


1
@ GlenH7 câu trả lời được chấp nhận (và được bình chọn cao nhất) tổng hợp rằng "bạn nên sử dụng chúng [ngoại lệ] trong trường hợp khi chúng xử lý lỗi dễ dàng hơn với ít sự lộn xộn mã hơn". Tôi đoán tôi đang hỏi ở đây liệu các lựa chọn thay thế mà tôi liệt kê có nên được coi là "ít lộn xộn mã" hay không.
Konrad Morawski

1
Tôi đã rút lại VTC của mình - bạn đang yêu cầu một cái gì đó liên quan, nhưng khác với bản sao tôi đề xuất.

3
Tôi nghĩ rằng câu hỏi trở nên thú vị hơn khi getValueByKeycũng là công khai.
Doc Brown

2
Để tham khảo trong tương lai, bạn đã làm đúng. Điều này sẽ lạc đề về Đánh giá mã vì đó là mã ví dụ.
RubberDuck

1
Chỉ cần kêu vang trong --- đây là Python hoàn toàn thành ngữ, và (tôi nghĩ) sẽ là cách ưa thích để xử lý vấn đề này bằng ngôn ngữ đó. Rõ ràng, mặc dù, các ngôn ngữ khác nhau có quy ước khác nhau.
Patrick Collins

Câu trả lời:


75

Vâng, đồng nghiệp của bạn là đúng: đó là mã xấu. Nếu một lỗi có thể được xử lý cục bộ, thì nó cần được xử lý ngay lập tức. Một ngoại lệ không nên được ném và sau đó xử lý ngay lập tức.

Đây là phiên bản sạch hơn nhiều (phiên bản của bạn getValueByKey()đã bị xóa):

public String getByKey(String key) {
    if (valuesFromDatabase.containsKey(key)) {
        return valuesFromDatabase.get(key);
    } else {
        String defaultValue = defaultValues.get(key);
        valuesFromDatabase.put(key, defaultvalue);
        return defaultValue;
    }
}

Chỉ nên ném ngoại lệ nếu bạn không biết cách khắc phục lỗi cục bộ.


14
Cảm ơn câu trả lời. Đối với hồ sơ, tôi là đồng nghiệp này (tôi không thích mã gốc), tôi chỉ đặt câu hỏi một cách trung lập để nó không được tải :)
Konrad Morawski

59
Dấu hiệu đầu tiên của sự điên rồ: bạn bắt đầu nói chuyện với chính mình.
Robert Harvey

1
@ BЈовић: tốt, trong trường hợp đó tôi nghĩ nó ổn, nhưng nếu bạn cần hai biến thể - một phương thức lấy giá trị mà không tự động xử lý các khóa bị thiếu và một với? Bạn nghĩ gì là sự thay thế sạch nhất (và DRY) sau đó?
Doc Brown

3
@RobertHarvey :) một người khác là tác giả của đoạn mã này, một người riêng biệt thực sự, tôi là người đánh giá
Konrad Morawski

1
@Alvaro, sử dụng ngoại lệ thường xuyên không phải là vấn đề. Sử dụng ngoại lệ xấu là một vấn đề, bất kể ngôn ngữ.
kdbanman

11

Tôi sẽ không gọi việc sử dụng Ngoại lệ này là một mô hình chống, chỉ không phải là giải pháp tốt nhất cho vấn đề truyền đạt một kết quả phức tạp.

Giải pháp tốt nhất (giả sử bạn vẫn còn trên Java 7) sẽ sử dụng Tùy chọn của Guava; Tôi không đồng ý rằng việc sử dụng nó trong trường hợp này sẽ là hackish. Dường như với tôi, dựa trên lời giải thích mở rộng của Guava về Tùy chọn , rằng đây là một ví dụ hoàn hảo về thời điểm sử dụng nó. Bạn đang phân biệt giữa "không tìm thấy giá trị" và "giá trị null được tìm thấy".


1
Tôi không nghĩ Optionalcó thể giữ null. Optional.of()chỉ lấy một tham chiếu không null và Optional.fromNullable()coi null là "giá trị không hiện diện".
Navin

1
@Navin nó không thể giữ null, nhưng bạn có thể tùy ý Optional.absent()sử dụng. Và như vậy, Optional.fromNullable(string)sẽ bằng Optional.<String>absent()nếu stringlà null hoặc Optional.of(string)nếu không.
Konrad Morawski

@KonradMorawski Tôi nghĩ vấn đề trong OP là bạn không thể phân biệt giữa chuỗi null và chuỗi không tồn tại mà không đưa ra một ngoại lệ. Optional.absent()bao gồm một trong những kịch bản này. Làm thế nào để bạn đại diện cho người khác?
Navin

@Navin với một thực tế null.
Konrad Morawski

4
@KonradMorawski: Nghe có vẻ là một ý tưởng thực sự tồi tệ. Tôi khó có thể nghĩ ra một cách rõ ràng hơn để nói "phương pháp này không bao giờ trở lại null!" hơn là tuyên bố nó trở lại Optional<...>. . .
ruakh 15/2/2015

8

Vì không có cân nhắc về hiệu suất và đó là một chi tiết triển khai, nên cuối cùng bạn không chọn giải pháp nào. Nhưng tôi phải đồng ý rằng đó là phong cách xấu; chìa khóa vắng mặt là điều mà bạn biết sẽ xảy ra và thậm chí bạn không xử lý nó nhiều hơn một cuộc gọi lên ngăn xếp, đó là nơi mà các ngoại lệ là hữu ích nhất.

Cách tiếp cận tuple là một chút hacky vì trường thứ hai là vô nghĩa khi boolean là sai. Kiểm tra xem khóa tồn tại trước có ngớ ngẩn không vì bản đồ đang tìm kiếm khóa hai lần. (Chà, bạn đã làm điều đó, vì vậy trong trường hợp này ba lần.) OptionalGiải pháp là một sự phù hợp hoàn hảo cho vấn đề. Có vẻ hơi mỉa mai khi lưu trữ nulltrong một Optional, nhưng nếu đó là những gì người dùng muốn làm, bạn không thể nói không.

Theo ghi nhận của Mike trong các bình luận, có một vấn đề với điều này; cả ổi và Java 8 đều không Optionalcho phép lưu trữ nulls. Do đó, bạn cần phải tự cuộn cái của mình - trong khi đơn giản - liên quan đến một số lượng lớn nồi hơi, do đó có thể là quá mức cần thiết cho một cái gì đó sẽ chỉ được sử dụng một lần trong nội bộ. Bạn cũng có thể thay đổi loại bản đồ thành Map<String, Optional<String>>, nhưng việc xử lý Optional<Optional<String>>trở nên khó xử.

Một sự thỏa hiệp hợp lý có thể là giữ ngoại lệ, nhưng thừa nhận vai trò của nó như là một "lợi nhuận thay thế". Tạo một ngoại lệ được kiểm tra mới với dấu vết ngăn xếp bị vô hiệu hóa và ném một thể hiện được tạo trước của nó mà bạn có thể giữ trong một biến tĩnh. Cái này rẻ - có thể rẻ như một chi nhánh địa phương - và bạn không thể quên xử lý nó, vì vậy việc thiếu dấu vết ngăn xếp không phải là vấn đề.


1
Chà, trong thực tế, nó chỉ Map<String, String>ở cấp bảng cơ sở dữ liệu, vì valuescột phải là một loại. Mặc dù vậy, có một lớp DAO bao bọc loại mạnh từng cặp khóa-giá trị. Nhưng tôi đã bỏ qua điều này cho rõ ràng. (Tất nhiên bạn có thể có một bảng một hàng với n cột thay thế, nhưng sau đó thêm từng giá trị mới yêu cầu cập nhật lược đồ bảng, do đó loại bỏ sự phức tạp liên quan đến việc phải phân tích chúng đi kèm với chi phí giới thiệu sự phức tạp khác ở nơi khác).
Konrad Morawski

Điểm tốt về tuple. Thật vậy, những gì sẽ (false, "foo")có nghĩa là gì?
Konrad Morawski

Ít nhất là với Tùy chọn của Guava, cố gắng đưa null vào kết quả trong một ngoại lệ. Bạn sẽ phải sử dụng Options.absent ().
Mike Partridge

@MikePartridge Điểm tốt. Một cách giải quyết xấu xí là thay đổi bản đồ Map<String, Optional<String>>và sau đó bạn sẽ kết thúc Optional<Optional<String>>. Một giải pháp ít gây nhầm lẫn hơn là cuộn Tùy chọn của riêng bạn cho phép nullhoặc loại dữ liệu đại số tương tự không tuân theo nullnhưng có 3 trạng thái - vắng mặt, không hoặc hiện tại. Cả hai đều đơn giản để thực hiện, mặc dù số lượng nồi hơi liên quan là cao cho một cái gì đó sẽ chỉ được sử dụng trong nội bộ.
Doval 12/2/2015

2
"Vì không có cân nhắc về hiệu suất và đó là chi tiết triển khai, nên cuối cùng bạn không chọn giải pháp nào" - Ngoại trừ việc bạn đang sử dụng C # bằng một ngoại lệ sẽ gây khó chịu cho bất kỳ ai đang cố gắng gỡ lỗi với "Phá vỡ khi có ngoại lệ "Được bật cho các ngoại lệ CLR.
Stephen

5

Có lớp Preferences này, là một nhóm cho các cặp khóa-giá trị. Giá trị Null là hợp pháp (đó là quan trọng). Chúng tôi hy vọng rằng các giá trị nhất định có thể chưa được lưu và chúng tôi muốn tự động xử lý các trường hợp này bằng cách khởi tạo chúng với giá trị mặc định được xác định trước khi được yêu cầu.

Vấn đề là chính xác điều này. Nhưng bạn đã tự đăng giải pháp:

Một biến thể của phương pháp này có thể sử dụng Tùy chọn của Guava (Guava đã được sử dụng trong toàn dự án) thay vì một số Tuple tùy chỉnh, và sau đó chúng ta có thể phân biệt giữa null.absent () và "thích hợp" null . Mặc dù vậy, nó vẫn cảm thấy bị hack, vì những lý do dễ thấy - giới thiệu hai cấp độ "vô hiệu" dường như lạm dụng khái niệm đứng đằng sau việc tạo Tùy chọn ở vị trí đầu tiên.

Tuy nhiên, không sử dụng null hoặc Optional . Bạn có thể và chỉ nên sử dụng Optional. Đối với cảm giác "hackish" của bạn, chỉ cần sử dụng nó lồng nhau, vì vậy bạn kết thúc với Optional<Optional<String>>nó làm cho nó rõ ràng rằng có thể có một khóa trong cơ sở dữ liệu (lớp tùy chọn đầu tiên) và nó có thể chứa một giá trị được xác định trước (lớp tùy chọn thứ hai).

Cách tiếp cận này tốt hơn so với sử dụng các ngoại lệ và nó khá dễ hiểu miễn Optionallà không mới đối với bạn.

Ngoài ra, xin lưu ý rằng Optionalcó một số chức năng thoải mái, để bạn không phải tự mình làm tất cả công việc. Bao gồm các:

  • static static <T> Optional<T> fromNullable(T nullableReference)để chuyển đổi cơ sở dữ liệu của bạn thành các Optionalloại
  • abstract T or(T defaultValue) để lấy giá trị khóa của lớp tùy chọn bên trong hoặc (nếu không có) lấy giá trị khóa mặc định của bạn

1
Optional<Optional<String>>Trông có vẻ xấu xa, mặc dù nó có một số nét quyến rũ tôi phải thừa nhận :)
Konrad Morawski

Bạn có thể giải thích thêm tại sao nó trông xấu xa? Nó thực sự là cùng một mô hình nhưList<List<String>> . Tất nhiên, bạn có thể (nếu ngôn ngữ cho phép) tạo một loại mới giống như DBConfigKeybao bọc Optional<Optional<String>>và làm cho nó dễ đọc hơn nếu bạn thích điều đó.
valenterry

2
@valenterry Nó rất khác. Danh sách các danh sách là một cách lưu trữ hợp pháp một số loại dữ liệu; một tùy chọn của một tùy chọn là một cách không điển hình để chỉ ra hai loại vấn đề mà dữ liệu có thể có.
Ypnypn

Một tùy chọn của một tùy chọn giống như một danh sách các danh sách trong đó mỗi danh sách có một hoặc không có thành phần. Về cơ bản là giống nhau. Chỉ vì nó không được sử dụng thường xuyên trong Java không có nghĩa là nó vốn đã xấu.
valenterry 12/2/2015

1
@valenterry ác theo nghĩa là Jon Skeet nghĩa là; Vì vậy, không hoàn toàn xấu, nhưng một chút xấu xa, "mã thông minh". Tôi đoán nó chỉ hơi baroque, một tùy chọn của một tùy chọn. Nó không rõ ràng mức độ tùy chọn đại diện cho những gì. Tôi sẽ mất gấp đôi nếu tôi gặp nó trong mã của ai đó. Bạn có thể đúng rằng nó cảm thấy kỳ lạ chỉ vì nó bất thường, không phổ biến. Có lẽ không có gì lạ mắt đối với một lập trình viên ngôn ngữ chức năng, với các đơn vị của họ và tất cả. Mặc dù tôi sẽ không tự mình đi, tôi vẫn trả lời câu hỏi của bạn bởi vì nó thú vị
Konrad Morawski

5

Tôi biết rằng tôi đến bữa tiệc muộn, nhưng dù sao thì trường hợp sử dụng của bạn cũng giống như cách JavaProperties cho phép một người xác định một tập các thuộc tính mặc định, điều này sẽ được kiểm tra nếu không có khóa tương ứng được tải bởi cá thể.

Nhìn vào cách triển khai được thực hiện cho Properties.getProperty(String)(từ Java 7):

Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;

Thực sự không cần phải "lạm dụng ngoại lệ để kiểm soát dòng chảy", như bạn đã trích dẫn.

Một cách tiếp cận tương tự, nhưng ngắn gọn hơn, đối với câu trả lời của @ BЈовић cũng có thể là:

public String getByKey(String key) {
    if (!valuesFromDatabase.containsKey(key)) {
        valuesFromDatabase.put(key, defaultValues.get(key));
    }
    return valuesFromDatabase.get(key);
}

4

Mặc dù tôi nghĩ rằng câu trả lời của @ BЈовић là tốt trong trường hợp getValueByKey không cần thiết ở nơi nào khác, tôi không nghĩ giải pháp của bạn là xấu trong trường hợp chương trình của bạn chứa cả hai trường hợp sử dụng:

  • truy xuất bằng khóa với tính năng tự động tạo trong trường hợp khóa không tồn tại trước đó và

  • Truy xuất mà không có chủ nghĩa tự động đó, không thay đổi bất cứ điều gì trong cơ sở dữ liệu, kho lưu trữ hoặc bản đồ chính (nghĩ về việc nuôi getValueByKeyong công khai, không riêng tư)

Nếu đây là tình huống của bạn và miễn là hiệu suất đạt được chấp nhận được, tôi nghĩ giải pháp đề xuất của bạn là hoàn toàn ổn. Nó có ưu điểm là tránh trùng lặp mã truy xuất, nó không dựa vào các khung của bên thứ ba và nó khá đơn giản (ít nhất là trong mắt tôi).

Trong thực tế, trong một tình huống như vậy, phụ thuộc vào bối cảnh nếu một khóa bị thiếu có phải là tình huống "ngoại lệ" hay không. Đối với một bối cảnh nơi nó là, một cái gì đó như getValueByKeylà cần thiết. Đối với bối cảnh nơi tạo khóa tự động được mong đợi, việc cung cấp một phương thức sử dụng lại chức năng đã có sẵn, nuốt ngoại lệ và cung cấp một hành vi lỗi khác, hoàn toàn có ý nghĩa. Điều này có thể được hiểu là một phần mở rộng hoặc "trang trí" củagetValueByKey , không nhiều như một chức năng trong đó "ngoại lệ bị lạm dụng cho luồng kiểm soát".

Tất nhiên, có một cách thay thế thứ ba: tạo phương thức thứ ba, riêng tư trả về Tuple<Boolean, String>, như bạn đã đề xuất và sử dụng lại phương thức này cả trong getValueByKeygetByKey. Đối với một trường hợp phức tạp hơn có thể thực sự là sự thay thế tốt hơn, nhưng đối với một trường hợp đơn giản như được trình bày ở đây, điều này đã khiến IMHO có mùi quá mức, và tôi nghi ngờ rằng mã sẽ thực sự dễ bảo trì hơn theo cách đó. Tôi ở đây với câu trả lời cao nhất ở đây bởi Karl Bielefeldt :

"Bạn không nên cảm thấy tồi tệ khi sử dụng các ngoại lệ khi nó đơn giản hóa mã của bạn".


3

Optionallà giải pháp chính xác. Tuy nhiên, nếu bạn thích, có một giải pháp thay thế có ít cảm giác "hai null" hơn, hãy xem xét một trọng điểm.

Xác định một chuỗi tĩnh riêng keyNotFoundSentinel, có giá trị new String(""). *

Bây giờ phương pháp riêng tư có thể return keyNotFoundSentinelhơn throw new KeyNotFoundException(). Phương thức công khai có thể kiểm tra giá trị đó

String rval = getValueByKey(key);
if (rval == keyNotFoundSentinel) { // reference equality, not string.equals
    ... // generate default value
} else {
    return rval;
}

Trong thực tế, nulllà một trường hợp đặc biệt của một trọng điểm. Đó là một giá trị sentinel được xác định bởi ngôn ngữ, với một vài hành vi đặc biệt (nghĩa là hành vi được xác định rõ nếu bạn gọi một phương thức trên null). Nó thật sự hữu ích khi có một trọng điểm như thế mà gần như ngôn ngữ sử dụng nó.

* Chúng tôi cố tình sử dụng new String("")thay vì chỉ đơn thuần ""để ngăn Java "nội bộ" chuỗi, điều này sẽ cung cấp cho nó cùng tham chiếu như bất kỳ chuỗi trống nào khác. Vì đã thực hiện bước bổ sung này, chúng tôi được đảm bảo rằng Chuỗi được gọi bằng keyNotFoundSentinelmột thể hiện duy nhất, mà chúng tôi cần đảm bảo nó không bao giờ có thể xuất hiện trong chính bản đồ.


Đây, IMHO, là câu trả lời tốt nhất.
fgp

2

Tìm hiểu từ khung đã học từ tất cả các điểm đau của Java:

.NET cung cấp hai giải pháp tinh tế hơn cho vấn đề này, được minh họa bằng:

Cái sau rất dễ viết bằng Java, cái trước chỉ cần một lớp trình trợ giúp "tham chiếu mạnh".


Một thay thế thân thiện hiệp phương sai cho Dictionarymô hình sẽ được T TryGetValue(TKey, out bool).
supercat
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.