Làm thế nào để thực hiện xác nhận đầu vào mà không có ngoại lệ hoặc dự phòng


11

Khi tôi đang cố gắng tạo giao diện cho một chương trình cụ thể, tôi thường cố gắng tránh đưa ra các ngoại lệ phụ thuộc vào đầu vào không được xác thực.

Vì vậy, điều thường xảy ra là tôi đã nghĩ về một đoạn mã như thế này (đây chỉ là một ví dụ vì lợi ích của một ví dụ, đừng bận tâm đến chức năng mà nó thực hiện, ví dụ trong Java):

public static String padToEvenOriginal(int evenSize, String string) {
    if (evenSize % 2 == 1) {
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, vì vậy nói rằng evenSizethực sự có nguồn gốc từ đầu vào của người dùng. Vì vậy, tôi không chắc chắn rằng nó là thậm chí. Nhưng tôi không muốn gọi phương thức này với khả năng một ngoại lệ được đưa ra. Vì vậy, tôi thực hiện các chức năng sau đây:

public static boolean isEven(int evenSize) {
    return evenSize % 2 == 0;
}

nhưng bây giờ tôi đã có hai kiểm tra thực hiện xác nhận đầu vào giống nhau: biểu thức trong ifcâu lệnh và kiểm tra rõ ràng isEven. Mã trùng lặp, không đẹp, vì vậy hãy tái cấu trúc:

public static String padToEvenWithIsEven(int evenSize, String string) {
    if (!isEven(evenSize)) { // to avoid duplicate code
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, điều đó đã giải quyết nó, nhưng bây giờ chúng ta gặp phải tình huống sau:

String test = "123";
int size;
do {
    size = getSizeFromInput();
} while (!isEven(size)); // checks if it is even
String evenTest = padToEvenWithIsEven(size, test);
System.out.println(evenTest); // checks if it is even (redundant)

bây giờ chúng tôi đã có một kiểm tra dự phòng: chúng tôi đã biết rằng giá trị là chẵn, nhưng padToEvenWithIsEvenvẫn thực hiện kiểm tra tham số, sẽ luôn trả về giá trị đúng, như chúng tôi đã gọi hàm này.

Bây giờ isEventất nhiên không gây ra vấn đề gì, nhưng nếu việc kiểm tra tham số trở nên cồng kềnh hơn thì điều này có thể phải chịu quá nhiều chi phí. Bên cạnh đó, thực hiện một cuộc gọi dự phòng đơn giản là không cảm thấy đúng.

Đôi khi chúng ta có thể giải quyết vấn đề này bằng cách giới thiệu "loại được xác thực" hoặc bằng cách tạo một hàm trong đó vấn đề này không thể xảy ra:

public static String padToEvenSmarter(int numberOfBigrams, String string) {
    int size = numberOfBigrams * 2;
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append('x');
    }
    return sb.toString();
}

nhưng điều này đòi hỏi một số suy nghĩ thông minh và một công cụ tái cấu trúc khá lớn.

Có một cách chung (hơn) trong đó chúng ta có thể tránh các cuộc gọi dự phòng isEvenvà thực hiện kiểm tra tham số kép không? Tôi muốn giải pháp không thực sự gọi padToEvenvới một tham số không hợp lệ, gây ra ngoại lệ.


Không có ngoại lệ, tôi không có nghĩa là lập trình không có ngoại lệ, ý tôi là đầu vào của người dùng không kích hoạt ngoại lệ theo thiết kế, trong khi bản thân hàm chung vẫn chứa kiểm tra tham số (nếu chỉ để bảo vệ chống lại lỗi lập trình).


Bạn chỉ đang cố gắng để loại bỏ ngoại lệ? Bạn có thể làm điều đó bằng cách đưa ra một giả định về kích thước thực tế. Ví dụ: nếu bạn vượt qua trong 13, hãy đệm đến 12 hoặc 14 và chỉ cần tránh kiểm tra hoàn toàn. Nếu bạn không thể đưa ra một trong những giả định đó, thì bạn bị mắc kẹt với ngoại lệ vì tham số này không sử dụng được và chức năng của bạn không thể tiếp tục.
Robert Harvey

@RobertHarvey Điều đó - theo tôi - đi thẳng vào nguyên tắc ít bất ngờ nhất cũng như chống lại nguyên tắc thất bại nhanh. Nó giống như trả về null nếu tham số đầu vào là null (và sau đó quên xử lý kết quả chính xác, tất nhiên, xin chào NPE không giải thích được).
Maarten Bodewes

Ergo, ngoại lệ. Đúng?
Robert Harvey

2
Đợi đã, cái gì? Bạn đã đến đây để được tư vấn, phải không? Điều tôi đang nói (theo cách rõ ràng của tôi không quá tinh tế), đó là những ngoại lệ đó là do thiết kế, vì vậy nếu bạn muốn loại bỏ chúng, có lẽ bạn nên có một lý do chính đáng. Nhân tiện, tôi đồng ý với amon: có lẽ bạn không nên có một hàm không thể chấp nhận số lẻ cho một tham số.
Robert Harvey

1
@MaartenBodewes: Bạn phải nhớ rằng padToEvenWithIsEven không thực hiện xác nhận đầu vào của người dùng. Nó thực hiện kiểm tra tính hợp lệ trên đầu vào của nó để bảo vệ chính nó khỏi các lỗi lập trình trong mã gọi. Việc xác thực này cần bao nhiêu tùy thuộc vào phân tích chi phí / rủi ro khi bạn đặt chi phí kiểm tra đối với rủi ro mà người viết mã gọi vượt qua trong tham số sai.
Bart van Ingen Schenau

Câu trả lời:


7

Trong trường hợp ví dụ của bạn, giải pháp tốt nhất là sử dụng chức năng đệm chung hơn; nếu người gọi muốn đệm đến một kích thước chẵn thì họ có thể tự kiểm tra.

public static String padString(int size, String string) {
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

Nếu bạn liên tục thực hiện xác nhận tương tự trên một giá trị hoặc muốn chỉ cho phép một tập hợp con các giá trị của một loại, thì microtypes / loại nhỏ có thể hữu ích. Đối với các tiện ích cho mục đích chung như đệm, đây không phải là một ý tưởng hay, nhưng nếu giá trị của bạn đóng một số vai trò cụ thể trong mô hình miền của bạn, sử dụng loại chuyên dụng thay vì giá trị nguyên thủy có thể là một bước tiến tuyệt vời. Ở đây, bạn có thể định nghĩa:

final class EvenInteger {
  public final int value;

  public EvenInteger(int value) {
    if (!(value % 2 == 0))
      throw new IllegalArgumentException("EvenInteger(" + value + ") is not even");
    this.value = value;
  }
}

Bây giờ bạn có thể khai báo

public static String padStringToEven(EvenInteger evenSize, String string)
    ...

và không phải thực hiện bất kỳ xác nhận bên trong. Đối với một thử nghiệm đơn giản như vậy, việc bọc một int bên trong một đối tượng có thể sẽ tốn kém hơn về hiệu suất thời gian chạy, nhưng sử dụng hệ thống loại để lợi thế của bạn có thể giảm lỗi và làm rõ thiết kế của bạn.

Sử dụng các loại nhỏ như vậy thậm chí có thể hữu ích ngay cả khi chúng không thực hiện xác nhận, ví dụ: để phân tán một chuỗi đại diện cho a FirstNametừ a LastName. Tôi thường xuyên sử dụng mô hình này trong các ngôn ngữ gõ tĩnh.


Không phải chức năng thứ hai của bạn vẫn ném một ngoại lệ trong một số trường hợp?
Robert Harvey

1
@RobertHarvey Có, ví dụ trong trường hợp con trỏ null. Nhưng bây giờ kiểm tra chính - rằng số chẵn - bị buộc ra khỏi chức năng thuộc trách nhiệm của người gọi. Sau đó, họ có thể đối phó với ngoại lệ theo cách họ thấy phù hợp. Tôi nghĩ rằng câu trả lời ít tập trung vào việc loại bỏ tất cả các ngoại lệ hơn là loại bỏ sự trùng lặp mã trong mã xác nhận.
amon

1
Câu trả lời chính xác. Tất nhiên trong Java, hình phạt cho việc tạo các lớp dữ liệu là khá cao (rất nhiều mã bổ sung) nhưng đó là vấn đề của ngôn ngữ nhiều hơn ý tưởng bạn đã trình bày. Tôi đoán rằng bạn sẽ không sử dụng giải pháp này cho tất cả các trường hợp sử dụng khi xảy ra sự cố của tôi, nhưng đó là một giải pháp tốt để chống lại việc lạm dụng nguyên thủy hoặc cho các trường hợp cạnh trong đó chi phí kiểm tra tham số đặc biệt khó khăn.
Maarten Bodewes

1
@MaartenBodewes Nếu bạn muốn xác thực, bạn không thể loại bỏ thứ gì đó như ngoại lệ. Vâng, thay thế là sử dụng một hàm tĩnh trả về một đối tượng được xác thực hoặc null khi thất bại, nhưng về cơ bản điều đó không có lợi thế. Nhưng bằng cách di chuyển xác thực ra khỏi hàm vào hàm tạo, giờ đây chúng ta có thể gọi hàm mà không có khả năng xảy ra lỗi xác thực. Điều đó cung cấp cho người gọi tất cả các điều khiển. Ví dụ:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
amon

1
@MaartenBodewes Xác thực trùng lặp xảy ra mọi lúc. Ví dụ, trước khi thử đóng một cánh cửa, bạn có thể kiểm tra xem nó đã đóng chưa, nhưng bản thân cánh cửa cũng sẽ không cho phép đóng lại nếu nó đã được đóng lại. Không có cách nào thoát khỏi điều này, ngoại trừ nếu bạn luôn tin tưởng rằng người gọi không thực hiện bất kỳ hoạt động không hợp lệ nào. Trong trường hợp của bạn, bạn có thể có một unsafePadStringToEvenhoạt động không thực hiện bất kỳ kiểm tra nào, nhưng có vẻ như đó là một ý tưởng tồi chỉ để tránh xác nhận.
plalx

9

Là một phần mở rộng cho câu trả lời của @ amon, chúng ta có thể kết hợp câu trả lời của mình EvenIntegervới cộng đồng lập trình chức năng có thể gọi là "Trình xây dựng thông minh" - một hàm bao bọc hàm tạo câm và đảm bảo rằng đối tượng ở trạng thái hợp lệ (chúng ta tạo ra câm lớp constructor hoặc mô-đun / gói ngôn ngữ không dựa trên lớp riêng để đảm bảo chỉ các hàm tạo thông minh được sử dụng). Mẹo là trả về một Optional(hoặc tương đương) để làm cho hàm có thể ghép lại nhiều hơn.

public final class EvenInteger {
    private final int value;

    private EvenInteger(value) {
        this.value = value;
    }

    public static Optional<EvenInteger> of(final int value) {
        if (value % 2 == 0) {
            return Optional.of(new EvenInteger(value));
        }
        return Optional.empty();
    }

    public int getValue() {
        return this.value;
    }
}

Sau đó chúng ta có thể dễ dàng sử dụng các Optionalphương thức tiêu chuẩn để viết logic đầu vào:

class GetEvenInput {
    public Optional<EvenInteger> askOnce() {
        int size = getSizeFromInput();
        return EvenInteger.of(size);
    }

    public EvenInteger keepAsking() {
        return askOnce().orElseGet(() -> keepAsking());
    }
}

Bạn cũng có thể viết keepAsking()theo kiểu Java thành ngữ hơn với vòng lặp do-while.

Optional<EvenInteger> result;
do {
    result = askOnce();
} while (!result.isPresent());

return result.get();

Sau đó, trong phần còn lại của mã của bạn, bạn có thể phụ thuộc vào EvenIntegerchứng nhận rằng nó thực sự sẽ đồng đều và kiểm tra thậm chí của chúng tôi chỉ được viết một lần, trong EvenInteger::of.


Tùy chọn nói chung thể hiện dữ liệu không hợp lệ khá tốt. Điều này tương tự với rất nhiều phương thức TryPude, trả về cả dữ liệu và cờ biểu thị tính hợp lệ đầu vào. Với kiểm tra thời gian tuân thủ.
Basilevs

1

Thực hiện xác nhận hai lần là một vấn đề nếu kết quả xác thực là giống nhau VÀ việc xác thực đang được thực hiện trong cùng một lớp. Đó không phải là ví dụ của bạn. Trong mã được cấu trúc lại của bạn, kiểm tra isEven đầu tiên được thực hiện là xác thực đầu vào, thất bại dẫn đến đầu vào mới được yêu cầu. Kiểm tra thứ hai hoàn toàn độc lập với kiểm tra thứ nhất, vì nó nằm trên một phương thức công khai padToEvenWithEven có thể được gọi từ bên ngoài lớp có kết quả khác (ngoại lệ).

Vấn đề của bạn tương tự như vấn đề vô tình mã giống hệt bị nhầm lẫn cho không khô. Bạn đang nhầm lẫn việc thực hiện, với thiết kế. Chúng không giống nhau và chỉ vì bạn có một hoặc một tá dòng giống nhau, không có nghĩa là chúng đang phục vụ cùng một mục đích và có thể mãi mãi được xem là có thể thay thế cho nhau. Ngoài ra, có lẽ lớp học của bạn làm quá nhiều, nhưng bỏ qua vì đây có lẽ chỉ là một ví dụ đồ chơi ...

Nếu đây là vấn đề về hiệu năng, bạn có thể giải quyết vấn đề bằng cách tạo một phương thức riêng tư, không thực hiện bất kỳ xác nhận nào, mà padEEWWWEEE công khai của bạn đã gọi sau khi thực hiện xác thực và phương thức khác của bạn sẽ gọi thay thế. Nếu đó không phải là vấn đề về hiệu năng, thì hãy để các phương thức khác nhau của bạn thực hiện kiểm tra mà chúng yêu cầu để thực hiện các nhiệm vụ được giao.


OP tuyên bố rằng việc xác thực đầu vào được thực hiện để ngăn chặn chức năng ném. Vì vậy, kiểm tra là hoàn toàn phụ thuộc và cố ý như nhau.
D Drmmr

@DDrmmr: không, họ không phụ thuộc. Hàm ném vì đó là một phần của hợp đồng. Như tôi đã nói, câu trả lời là tạo ra một phương thức riêng thực hiện mọi thứ trừ việc ném ngoại lệ và sau đó gọi phương thức chung gọi phương thức riêng. Phương thức công khai giữ lại kiểm tra, trong đó nó phục vụ một mục đích khác - để xử lý đầu vào không có giá trị.
jmoreno
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.