วิธีการตรวจสอบอินพุตโดยไม่มีข้อยกเว้นหรือความซ้ำซ้อน


11

เมื่อฉันพยายามสร้างส่วนต่อประสานสำหรับโปรแกรมเฉพาะฉันมักจะพยายามหลีกเลี่ยงการโยนข้อยกเว้นที่ขึ้นอยู่กับอินพุตที่ไม่ผ่านการตรวจสอบ

ดังนั้นสิ่งที่มักจะเกิดขึ้นคือฉันคิดว่าโค้ดบางส่วนเช่นนี้ (นี่เป็นเพียงตัวอย่างเพื่อประโยชน์ของตัวอย่างไม่ต้องสนใจฟังก์ชั่นที่ใช้งานตัวอย่างใน 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();
}

ตกลงดังนั้นพูดได้ว่าevenSizeมาจากการป้อนข้อมูลของผู้ใช้ ดังนั้นฉันไม่แน่ใจว่ามันจะเป็นเช่นนั้น แต่ฉันไม่ต้องการเรียกวิธีนี้โดยมีความเป็นไปได้ที่จะมีข้อผิดพลาดเกิดขึ้น ดังนั้นฉันจึงทำฟังก์ชั่นต่อไปนี้:

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

แต่ตอนนี้ฉันมีสองการตรวจสอบที่ดำเนินการตรวจสอบการป้อนข้อมูลเหมือนกัน: การแสดงออกในคำสั่งและการตรวจสอบอย่างชัดเจนในif isEvenทำซ้ำรหัสไม่ดีดังนั้นเราจะทำการ refactor:

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();
}

ตกลงนั่นแก้ไขได้ แต่ตอนนี้เราเข้าสู่สถานการณ์ต่อไปนี้:

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)

ตอนนี้เราได้รับการตรวจสอบซ้ำซ้อน: เรารู้อยู่แล้วว่าค่านี้เป็นเลขคู่ แต่padToEvenWithIsEvenยังคงทำการตรวจสอบพารามิเตอร์ซึ่งจะคืนค่าจริงเสมอเมื่อเราเรียกฟังก์ชันนี้แล้ว

isEvenแน่นอนตอนนี้ไม่ได้มีปัญหา แต่ถ้าการตรวจสอบพารามิเตอร์ยุ่งยากกว่านี้อาจทำให้เกิดค่าใช้จ่ายมากเกินไป นอกจากนั้นการโทรซ้ำซ้อนก็ไม่รู้สึกถูกต้อง

บางครั้งเราสามารถหลีกเลี่ยงปัญหานี้ได้โดยการแนะนำ "ประเภทที่ตรวจสอบแล้ว" หรือโดยการสร้างฟังก์ชั่นที่ปัญหานี้ไม่สามารถเกิดขึ้นได้:

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();
}

แต่สิ่งนี้ต้องใช้ความคิดที่ชาญฉลาดและค่อนข้าง refactor ขนาดใหญ่

มีวิธีการทั่วไป (เพิ่มเติม) ที่เราสามารถหลีกเลี่ยงการโทรซ้ำซ้อนisEvenและดำเนินการตรวจสอบพารามิเตอร์ซ้ำได้หรือไม่? ฉันต้องการวิธีการแก้ปัญหาที่จะไม่โทรpadToEvenด้วยพารามิเตอร์ที่ไม่ถูกต้องจริงเรียกข้อยกเว้น


ไม่มีข้อยกเว้นฉันไม่ได้หมายถึงการเขียนโปรแกรมที่ไม่มีข้อยกเว้นฉันหมายความว่าการป้อนข้อมูลของผู้ใช้ไม่ทำให้เกิดข้อยกเว้นจากการออกแบบในขณะที่ฟังก์ชั่นทั่วไปยังคงมีการตรวจสอบพารามิเตอร์ (ถ้าเพียงเพื่อป้องกันข้อผิดพลาดการเขียนโปรแกรม)


คุณเพียงแค่พยายามลบข้อยกเว้นหรือไม่ คุณสามารถทำได้โดยการตั้งสมมติฐานว่าขนาดที่แท้จริงควรเป็นเท่าไหร่ ตัวอย่างเช่นถ้าคุณผ่านใน 13 ให้ปัดที่ 12 หรือ 14 และเพียงหลีกเลี่ยงการตรวจสอบทั้งหมด หากคุณไม่สามารถตั้งสมมติฐานข้อใดข้อหนึ่งได้แสดงว่าคุณติดอยู่กับข้อยกเว้นเนื่องจากพารามิเตอร์ใช้ไม่ได้และฟังก์ชั่นของคุณไม่สามารถดำเนินการต่อได้
Robert Harvey

@ RobertHarvey นั่น - ในความคิดของฉัน - ตรงกับหลักการของความประหลาดใจน้อยที่สุดเช่นเดียวกับหลักการของความล้มเหลวอย่างรวดเร็ว มันเหมือนกับการคืนค่า null ถ้าพารามิเตอร์อินพุตเป็น null (และจากนั้นลืมจัดการผลลัพธ์อย่างถูกต้องแน่นอนไม่ได้อธิบาย NPE)
Maarten Bodewes

Ergo ข้อยกเว้น ขวา?
Robert Harvey

2
รออะไร? คุณมาที่นี่เพื่อขอคำแนะนำใช่ไหม? สิ่งที่ฉันพูด (ในทางที่ไม่ชัดนัก) ก็คือข้อยกเว้นเหล่านั้นเกิดจากการออกแบบดังนั้นหากคุณต้องการกำจัดพวกเขาคุณอาจต้องมีเหตุผลที่ดี โดยวิธีการที่ฉันเห็นด้วยกับ amon: คุณอาจไม่ควรมีฟังก์ชั่นที่ไม่สามารถรับตัวเลขคี่สำหรับพารามิเตอร์
Robert Harvey

1
@MaartenBodewes: คุณต้องจำไว้padToEvenWithIsEven ว่าไม่ได้ทำการตรวจสอบความถูกต้องของการป้อนข้อมูลของผู้ใช้ มันทำการตรวจสอบความถูกต้องของอินพุตเพื่อป้องกันตัวเองจากข้อผิดพลาดในการเขียนโปรแกรมในรหัสการโทร การตรวจสอบความถูกต้องนี้ต้องขึ้นอยู่กับการวิเคราะห์ค่าใช้จ่าย / ความเสี่ยงซึ่งคุณต้องเสียค่าใช้จ่ายในการตรวจสอบกับความเสี่ยงที่บุคคลที่เขียนรหัสการโทรผ่านในพารามิเตอร์ที่ไม่ถูกต้อง
Bart van Ingen Schenau

คำตอบ:


7

ในกรณีตัวอย่างของคุณทางออกที่ดีที่สุดคือการใช้ฟังก์ชั่นเสริมทั่วไป หากผู้โทรต้องการเพิ่มขนาดให้เท่ากันก็สามารถตรวจสอบได้ด้วยตนเอง

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();
}

หากคุณทำการตรวจสอบความถูกต้องซ้ำกับค่าซ้ำ ๆ หรือต้องการอนุญาตให้ใช้ชุดย่อยของค่าประเภทย่อยได้เพียงอย่างเดียวmicrotypes / Tiny type จะมีประโยชน์ สำหรับยูทิลิตี้ที่ใช้งานทั่วไปเช่นการแพ็ดดิ้งนี่ไม่ใช่ความคิดที่ดี แต่ถ้าคุณค่าของคุณมีบทบาทเฉพาะในโมเดลโดเมนของคุณการใช้ประเภทเฉพาะแทนค่าดั้งเดิมอาจเป็นขั้นตอนที่ยอดเยี่ยม ที่นี่คุณอาจกำหนด:

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;
  }
}

ตอนนี้คุณสามารถประกาศ

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

และไม่ต้องทำการตรวจสอบใด ๆ ภายใน สำหรับการทดสอบห่อหุ้มอย่างง่าย int ภายในวัตถุนั้นอาจมีราคาแพงกว่าในแง่ของประสิทธิภาพเวลาทำงาน แต่การใช้ระบบชนิดเพื่อประโยชน์ของคุณสามารถลดข้อบกพร่องและทำให้การออกแบบของคุณชัดเจนขึ้น

ใช้ประเภทเล็ก ๆ ดังกล่าวยังสามารถเป็นประโยชน์แม้เมื่อพวกเขาดำเนินการตรวจสอบไม่เช่นเพื่อกระจ่างสตริงเป็นตัวแทนจากFirstName LastNameฉันมักจะใช้รูปแบบนี้ในภาษาที่พิมพ์แบบคงที่


ฟังก์ชันที่สองของคุณไม่ได้ส่งข้อยกเว้นในบางกรณีหรือไม่
Robert Harvey

1
@RobertHarvey ใช่เช่นในกรณีของพอยน์เตอร์พอยน์เตอร์ แต่ตอนนี้การตรวจสอบหลัก - ว่าเป็นเลขคู่ - ถูกบังคับให้ออกจากฟังก์ชั่นไปเป็นความรับผิดชอบของผู้โทร จากนั้นพวกเขาสามารถจัดการกับข้อยกเว้นในลักษณะที่เห็นสมควร ฉันคิดว่าคำตอบเพ่งความสนใจไปที่การกำจัดข้อยกเว้นทั้งหมดน้อยกว่าการกำจัดการทำสำเนารหัสในรหัสการตรวจสอบ
amon

1
คำตอบที่ดี แน่นอนใน Java บทลงโทษสำหรับการสร้างคลาสข้อมูลนั้นค่อนข้างสูง (มีรหัสเพิ่มเติมมากมาย) แต่นั่นเป็นปัญหาของภาษามากกว่าความคิดที่คุณนำเสนอ ฉันเดาว่าคุณจะไม่ใช้โซลูชันนี้สำหรับทุกกรณีการใช้งานที่ปัญหาของฉันจะเกิดขึ้น แต่เป็นวิธีที่ดีในการป้องกันการใช้ข้อมูลพื้นฐานมากเกินไปหรือสำหรับกรณีขอบซึ่งค่าใช้จ่ายของการตรวจสอบพารามิเตอร์นั้นยากมาก
Maarten Bodewes

1
@MaartenBodewes หากคุณต้องการตรวจสอบความถูกต้องคุณไม่สามารถกำจัดสิ่งที่ยกเว้นได้ ทางเลือกคือการใช้ฟังก์ชั่นสแตติกที่ส่งกลับวัตถุที่ตรวจสอบหรือโมฆะเมื่อความล้มเหลว แต่ที่ไม่มีประโยชน์โดยทั่วไป แต่ด้วยการย้ายการตรวจสอบความถูกต้องออกจากฟังก์ชันไปยังตัวสร้างเราสามารถเรียกฟังก์ชันนี้ได้โดยไม่เกิดข้อผิดพลาดในการตรวจสอบความถูกต้อง ที่ให้ผู้โทรควบคุมทั้งหมด เช่น:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
อมร

1
@MaartenBodewes การตรวจสอบซ้ำจะเกิดขึ้นตลอดเวลา ตัวอย่างเช่นก่อนที่จะพยายามปิดประตูคุณอาจตรวจสอบว่ามันปิดไปแล้ว แต่ประตูนั้นจะไม่อนุญาตให้ปิดอีกครั้งหากเป็นประตูอยู่แล้ว ไม่มีทางออกจากสิ่งนี้ยกเว้นในกรณีที่คุณไว้ใจได้ว่าผู้โทรไม่ได้ทำงานที่ไม่ถูกต้อง ในกรณีของคุณคุณอาจมีการunsafePadStringToEvenดำเนินการที่ไม่ได้ทำการตรวจสอบใด ๆ แต่ดูเหมือนว่าเป็นความคิดที่ไม่ดีเพียงเพื่อหลีกเลี่ยงการตรวจสอบ
plalx

9

ในฐานะที่เป็นส่วนขยายของคำตอบ @ amon เราสามารถรวมเขาเข้าEvenIntegerกับชุมชนการเขียนโปรแกรมที่ใช้งานได้อาจเรียกว่า "Smart Constructor" - ฟังก์ชั่นที่หุ้มตัวสร้างใบ้และตรวจสอบให้แน่ใจว่าวัตถุนั้นอยู่ในสถานะที่ถูกต้อง คลาสคอนสตรัคหรือในภาษาที่ไม่ใช่คลาสโมดูล / แพคเกจส่วนตัวเพื่อให้แน่ใจว่ามีการใช้ตัวสร้างสมาร์ทเท่านั้น) เคล็ดลับคือการส่งคืนOptional(หรือเทียบเท่า) เพื่อให้ฟังก์ชั่นแต่งได้มากขึ้น

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;
    }
}

จากนั้นเราสามารถใช้Optionalวิธีมาตรฐานเพื่อเขียนตรรกะอินพุตได้อย่างง่ายดาย:

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

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

นอกจากนี้คุณยังสามารถเขียนkeepAsking()ในสไตล์ Java ที่มีสำนวนมากขึ้นด้วยการวนซ้ำในขณะที่

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

return result.get();

จากนั้นในส่วนที่เหลือของรหัสของคุณคุณสามารถขึ้นอยู่กับการมีความเชื่อมั่นว่าจริงๆจะยิ่งและการตรวจสอบของเราแม้จะเป็นเพียงคนเดียวที่เคยเขียนครั้งเดียวในEvenIntegerEvenInteger::of


ตัวเลือกโดยทั่วไปหมายถึงข้อมูลที่อาจไม่ถูกต้องค่อนข้างดี นี่คล้ายกับวิธีทดลองมากมายของ TryParse ซึ่งส่งคืนทั้งข้อมูลและการตั้งค่าสถานะเพื่อระบุความถูกต้องของอินพุต ด้วยการตรวจสอบเวลา complie
Basilevs

1

การตรวจสอบความถูกต้องสองครั้งเป็นปัญหาหากผลลัพธ์ของการตรวจสอบเหมือนกันและการตรวจสอบความถูกต้องจะทำในชั้นเรียนเดียวกัน นั่นไม่ใช่ตัวอย่างของคุณ ในรหัส refactored ของคุณการตรวจสอบ isEven ครั้งแรกที่ทำคือการตรวจสอบความถูกต้องของอินพุตผลลัพธ์ความล้มเหลวในอินพุตใหม่ที่ถูกร้องขอ การตรวจสอบครั้งที่สองเป็นอิสระอย่างสมบูรณ์จากการตรวจสอบครั้งแรกเนื่องจากเป็นวิธีการสาธารณะ padToEvenWithEven ซึ่งสามารถเรียกได้จากนอกชั้นเรียนและมีผลที่แตกต่างกัน (ยกเว้น)

ปัญหาของคุณคล้ายกับปัญหาของรหัสที่เหมือนกันโดยไม่ตั้งใจซึ่งทำให้สับสน คุณกำลังสับสนในการนำไปใช้กับการออกแบบ พวกเขาไม่เหมือนกันและเพียงเพราะคุณมีหนึ่งหรือสิบบรรทัดที่เหมือนกันไม่ได้หมายความว่าพวกเขาจะให้บริการวัตถุประสงค์เดียวกันและสามารถพิจารณาเปลี่ยนได้ตลอดไป นอกจากนี้อาจให้ชั้นเรียนของคุณทำมากเกินไป แต่ข้ามไปเพราะนี่อาจเป็นเพียงตัวอย่างของเล่น ...

หากนี่เป็นปัญหาด้านประสิทธิภาพคุณสามารถแก้ไขปัญหาได้ด้วยการสร้างวิธีการส่วนตัวซึ่งไม่ได้ทำการตรวจสอบใด ๆ ซึ่ง padToEvenWithEven สาธารณะของคุณเรียกว่าหลังจากทำการตรวจสอบแล้วและวิธีอื่นของคุณจะเรียกแทน หากไม่ใช่ปัญหาด้านประสิทธิภาพให้วิธีการต่าง ๆ ของคุณทำการตรวจสอบว่าพวกเขาต้องการทำงานที่ได้รับมอบหมายหรือไม่


OP ระบุว่ามีการตรวจสอบความถูกต้องของอินพุตเพื่อป้องกันไม่ให้เกิดการโยนฟังก์ชัน ดังนั้นการตรวจสอบทั้งหมดขึ้นอยู่กับความตั้งใจและความตั้งใจเดียวกัน
D Drmmr

@DDrmmr: ไม่พวกเขาไม่ต้องพึ่งพา ฟังก์ชั่นพ่นเพราะนั่นเป็นส่วนหนึ่งของสัญญา อย่างที่ฉันพูดคำตอบคือการสร้างวิธีการส่วนตัวทำทุกอย่างยกเว้นการยกเว้นข้อยกเว้นจากนั้นให้วิธีสาธารณะเรียกวิธีส่วนตัว วิธีสาธารณะคงไว้ซึ่งการตรวจสอบซึ่งทำหน้าที่มีวัตถุประสงค์ที่แตกต่าง - เพื่อจัดการอินพุตที่ไม่ผ่านการตรวจสอบ
jmoreno
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.