ข้อยกเว้น: ทำไมต้องโยนเร็ว ทำไมต้องมาสาย?


156

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

ทำไมฉันควรโยน แต่เนิ่นๆและไปช้าถ้าอยู่ในระดับต่ำจะมีข้อยกเว้นตัวชี้โมฆะ? ทำไมฉันถึงต้องจับมันในระดับที่สูงกว่า? มันไม่มีเหตุผลที่ฉันจะจับข้อยกเว้นระดับต่ำในระดับที่สูงขึ้นเช่นชั้นธุรกิจ ดูเหมือนว่าจะละเมิดข้อกังวลของแต่ละชั้น

ลองนึกภาพสถานการณ์ต่อไปนี้:

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

ทำไมโยนเร็วทำไมต้องมาสาย?


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

16
มีบางกรณีที่ NullPointerException (ฉันคิดว่านั่นคือความหมายของ NPE) ที่ควรถูกจับได้ หากเป็นไปได้ควรหลีกเลี่ยงตั้งแต่แรก หากคุณมี NullPointerExceptions แสดงว่าคุณมีรหัสที่เสียหายที่ต้องแก้ไข เป็นไปได้ว่าการแก้ไขนั้นง่ายเกินไป
Phil

6
ได้โปรดพวกก่อนที่จะแนะนำให้ปิดคำถามนี้ซ้ำให้ตรวจสอบว่าคำตอบของคำถามอื่นไม่ตอบคำถามนี้ได้ดีมาก
Doc Brown

1
(Citebot) today.java.net/article/2003/11/20/…หากนี่ไม่ใช่จุดเริ่มต้นของคำพูดโปรดให้ข้อมูลอ้างอิงไปยังแหล่งที่คุณคิดว่าเป็นข้อความต้นฉบับที่น่าจะเป็นไปได้มากที่สุด
ร. ว.

1
เพียงเตือนความทรงจำสำหรับทุกคนถึงคำถามนี้และทำการพัฒนา Android บน Android ต้องมีการตรวจจับและจัดการข้อยกเว้นภายในเครื่อง - ในฟังก์ชั่นเดียวกับที่ติดตั้งครั้งแรก นี่เป็นเพราะข้อยกเว้นไม่แพร่กระจายข้ามตัวจัดการข้อความ - แอปพลิเคชันของคุณจะถูกฆ่าหากเกิดขึ้น ดังนั้นคุณไม่ควรอ้างอิงคำแนะนำนี้เมื่อทำการพัฒนา Android
rwong

คำตอบ:


118

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

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

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

ด้วยการฝึกฝนและประสบการณ์เกี่ยวกับฐานรหัสของคุณมันค่อนข้างง่ายที่จะตัดสินว่าเมื่อใดที่จะเพิ่มบริบทเพิ่มเติมให้กับข้อผิดพลาดและจุดที่เหมาะสมที่สุดที่จะจัดการกับข้อผิดพลาด

จับ→ Rethrow

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

จับ→จัดการ

ทำสิ่งนี้ที่คุณสามารถตัดสินใจขั้นสุดท้ายเกี่ยวกับสิ่งที่เหมาะสม แต่มีขั้นตอนการดำเนินการที่แตกต่างกันผ่านซอฟต์แวร์

จับกลับข้อผิดพลาด→

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


ใช่ฉันรู้แล้วและเป็นปัญหาฉันควรโยนข้อยกเว้น ณ จุดที่เกิดข้อผิดพลาด แต่ทำไมฉันไม่ควรจับ NPE และปล่อยให้มันปีนขึ้นไปบน Stacktrace แทน ฉันมักจะจับ NPE และห่อมันเป็นข้อยกเว้นที่มีความหมาย ฉันยังไม่เห็นข้อได้เปรียบใด ๆ ว่าทำไมฉันควรโยน DAO-Exception ไปยังบริการหรือเลเยอร์ ui ฉันมักจะจับมันที่ชั้นบริการและห่อเป็นข้อยกเว้นบริการด้วยข้อมูลรายละเอียดเพิ่มเติมทำไมการเรียกบริการล้มเหลว
shylynx

8
@Shylynx การจับข้อยกเว้นแล้วทำการ rethrowing ข้อยกเว้นที่มีความหมายมากขึ้นเป็นสิ่งที่ดีที่ต้องทำ สิ่งที่คุณไม่ควรทำคือจับข้อยกเว้นเร็วเกินไปและไม่ต้องทำใหม่อีกครั้ง ข้อผิดพลาดที่คำพูดนั้นเตือนว่าจับข้อยกเว้นเร็วเกินไปแล้วพยายามจัดการมันในเลเยอร์ที่ไม่ถูกต้องในโค้ด
Simon B

การทำให้บริบทชัดเจนเมื่อได้รับการยกเว้นทำให้ชีวิตของนักพัฒนาในทีมของคุณง่ายขึ้น NPE ต้องการการตรวจสอบเพิ่มเติมเพื่อทำความเข้าใจกับปัญหา
Michael Shaw

4
@shylynx หนึ่งอาจจะถามคำถามที่ว่า "ทำไมคุณมีจุดในรหัสของคุณที่สามารถโยนNullPointerExceptionทำไมไม่ตรวจสอบnullและโยนข้อยกเว้น (อาจจะIllegalArgumentException) เร็วเพื่อให้ผู้ที่โทรมารู้ว่าที่เลวร้ายnullได้ผ่าน?" ฉันเชื่อว่าจะเป็นสิ่งที่ "โยนเร็ว" ส่วนหนึ่งของคำพูดที่จะแนะนำ
jpmc26

2
@jpmc ฉันใช้ NPE เป็นเพียงตัวอย่างเพื่อเน้นความกังวลรอบ ๆ เลเยอร์และข้อยกเว้น ฉันสามารถแทนที่ด้วย IllegalArgumentException ด้วย
shylynx

56

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

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

การตัดข้อยกเว้นในประเภทที่เหมาะสมนั้นเป็นข้อกังวลแบบดั้งเดิม


1
+1 สำหรับการอธิบายอย่างชัดเจนว่าทำไมระดับต่างกันมีความสำคัญ ตัวอย่างที่ยอดเยี่ยมเกี่ยวกับข้อผิดพลาดของระบบไฟล์
Juan Carlos Coto

24

อื่น ๆ ได้สรุปค่อนข้างดีทำไมจะโยนต้น ขอให้ฉันจดจ่อกับเหตุผลที่จะมาสายแทนซึ่งฉันไม่ได้เห็นคำอธิบายที่น่าพอใจสำหรับรสนิยมของฉัน

เหตุใดจึงเป็นข้อยกเว้น

ดูเหมือนจะค่อนข้างสับสนว่าทำไมข้อยกเว้นอยู่ในสถานที่แรก ผมขอแสดงความลับใหญ่ที่นี่: เหตุผลในข้อยกเว้นและการจัดการข้อยกเว้นคือ ... สิ่งที่เป็นนามธรรม

คุณเห็นรหัสเช่นนี้หรือไม่:

static int divide(int dividend, int divisor) throws DivideByZeroException {
    if (divisor == 0)
        throw new DivideByZeroException(); // that's a checked exception indeed

    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    try {
        int res = divide(a, b);
        System.out.println(res);
    } catch (DivideByZeroException e) {
        // checked exception... I'm forced to handle it!
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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

static int divide(int dividend, int divisor) {
    // throws unchecked ArithmeticException for 0 divisor
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt();
    if (b != 0) {
        int res = divide(a, b);
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

หรือคุณสามารถใช้คอมมานโดแบบเต็มในสไตล์ OOP เช่นนี้:

static class Division {
    final int dividend;
    final int divisor;

    private Division(int dividend, int divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public boolean check() {
        return divisor != 0;
    }

    public int eval() {
        return dividend / divisor;
    }

    public static Division with(int dividend, int divisor) {
        return new Division(dividend, divisor);
    }
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Division d = Division.with(a, b);
    if (d.check()) {
        int res = d.eval();
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

อย่างที่คุณเห็นรหัสผู้โทรมีภาระในการตรวจสอบล่วงหน้า แต่ไม่ได้ทำการจัดการข้อยกเว้นใด ๆ หลังจากนั้น หากผู้ArithmeticExceptionที่เคยมาจากการเรียกdivideหรือevalแล้วมันเป็นคุณcheck()ที่มีการทำข้อยกเว้นการจัดการและแก้ไขรหัสของคุณเพราะคุณลืม สำหรับเหตุผลที่คล้ายกันที่การจับเป็นNullPointerExceptionสิ่งที่ต้องทำ

ขณะนี้มีคนบางคนที่กล่าวว่าพวกเขาต้องการที่จะเห็นกรณีพิเศษในลายเซ็นวิธี / ฟังก์ชั่นคืออย่างชัดเจนขยายการส่งออกโดเมน พวกเขาเป็นคนที่ชื่นชอบข้อยกเว้นการตรวจสอบ แน่นอนว่าการเปลี่ยนโดเมนที่ส่งออกควรบังคับให้รหัสผู้โทรโดยตรงสามารถปรับตัวได้ แต่คุณไม่ต้องการข้อยกเว้นสำหรับสิ่งนั้น! นั่นเป็นเหตุผลที่คุณต้องNullable<T> เรียนทั่วไป , เรียนกรณี , ชนิดข้อมูลเกี่ยวกับพีชคณิตและประเภทยูเนี่ยน บางคน OOอาจต้องการกลับมา nullเพราะกรณีข้อผิดพลาดง่าย ๆ เช่นนี้:

static Integer divide(int dividend, int divisor) {
    if (divisor == 0) return null;
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Integer res = divide(a, b);
    if (res != null) {
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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

จะจับ LATE ได้อย่างไร

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

กรณีที่ใช้คือ: การเขียนโปรแกรมกับ Abstractions ทรัพยากร ...

ใช่ตรรกะทางธุรกิจควรตั้งโปรแกรมให้กับ abstractionsไม่ใช่การใช้งานที่เป็นรูปธรรม รหัส "การเดินสาย" IOCระดับบนสุดจะยกตัวอย่างการนำนามธรรมของทรัพยากรไปใช้อย่างเป็นรูปธรรมและส่งต่อไปยังตรรกะทางธุรกิจ ไม่มีอะไรใหม่ที่นี่ แต่การใช้งานที่เป็นรูปธรรมของ abstractions ทรัพยากรเหล่านั้นอาจจะทิ้งข้อยกเว้นเฉพาะในการใช้งานของพวกเขาเองใช่ไหม?

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

"Aha!" คุณอาจพูดว่า: "แต่นั่นเป็นสาเหตุที่เราสามารถยกเว้นคลาสย่อยและสร้างลำดับชั้นของข้อยกเว้น" (ลองดูMr. Spring !) ให้ฉันบอกคุณว่าเป็นความเข้าใจผิด ประการแรกหนังสือที่สมเหตุสมผลทุกเล่มเกี่ยวกับ OOP กล่าวว่าการสืบทอดที่เป็นรูปธรรมนั้นไม่ดี แต่อย่างใดองค์ประกอบหลักของ JVM นี้การจัดการข้อยกเว้นมีการเชื่อมโยงอย่างใกล้ชิดกับการสืบทอดที่เป็นรูปธรรม กระแทกแดกดัน Joshua Bloch ไม่สามารถเขียนหนังสือ Java ที่มีประสิทธิภาพของเขาก่อนที่เขาจะได้รับประสบการณ์กับ JVM ที่ทำงานได้หรือไม่ มันเป็นหนังสือ "บทเรียนที่เรียนรู้" มากกว่าสำหรับคนรุ่นต่อไป ประการที่สองและที่สำคัญกว่านั้นถ้าคุณได้รับข้อยกเว้นระดับสูงคุณจะจัดการกับมันอย่างไรPatientNeedsImmediateAttentionException: เราต้องให้ยาหรือตัดขาของเธอ! คำสั่ง switch เกี่ยวกับ subclasses ที่เป็นไปได้ทั้งหมดเป็นอย่างไร? ความแตกต่างของคุณมีไปสิ่งที่เป็นนามธรรม คุณได้รับประเด็น

ดังนั้นใครสามารถจัดการข้อยกเว้นเฉพาะของทรัพยากรได้ มันจะต้องเป็นคนที่รู้ว่า concretions! ผู้ที่สร้างอินสแตนซ์ของทรัพยากร! รหัส "เดินสาย" แน่นอน! ลองดู:

ตรรกะทางธุรกิจที่เข้ารหัสกับ abstractions ... ไม่มีความผิดพลาดในการจัดการความผิดทางคอนกรีต!

static interface InputResource {
    String fetchData();
}

static interface OutputResource {
    void writeData(String data);
}

static void doMyBusiness(InputResource in, OutputResource out, int times) {
    for (int i = 0; i < times; i++) {
        System.out.println("fetching data");
        String data = in.fetchData();
        System.out.println("outputting data");
        out.writeData(data);
    }
}

ในขณะเดียวกันการใช้งานที่เป็นรูปธรรมอื่น ...

static class ConstantInputResource implements InputResource {
    @Override
    public String fetchData() {
        return "Hello World!";
    }
}

static class FailingInputResourceException extends RuntimeException {
    public FailingInputResourceException(String message) {
        super(message);
    }
}

static class FailingInputResource implements InputResource {
    @Override
    public String fetchData() {
        throw new FailingInputResourceException("I am a complete failure!");
    }
}

static class StandardOutputResource implements OutputResource {
    @Override
    public void writeData(String data) {
        System.out.println("DATA: " + data);
    }
}

และในที่สุดรหัสการเดินสาย ... ใครจัดการข้อยกเว้นที่เป็นรูปธรรม? คนที่รู้เกี่ยวกับพวกเขา!

static void start() {
    InputResource in1 = new FailingInputResource();
    InputResource in2 = new ConstantInputResource();
    OutputResource out = new StandardOutputResource();

    try {
        ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
    }
    catch (FailingInputResourceException e)
    {
        System.out.println(e.getMessage());
        System.out.println("retrying...");
        ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
    }
}

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

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

SUMMA SUMMARUM

ดังนั้นตามการตีความของฉันจับปลายหมายถึงการจับข้อยกเว้นในสถานที่ที่สะดวกที่สุดที่คุณจะไม่ BREAKING นามธรรมอีกต่อไป อย่ารีบเร็วเกินไป! ตรวจจับข้อยกเว้นที่เลเยอร์ที่คุณสร้างข้อยกเว้นที่เป็นรูปธรรมในการขว้างอินสแตนซ์ของ abstractions ทรัพยากรเลเยอร์ที่รู้ถึงข้อสรุปของ abstractions ชั้น "เดินสาย"

HTH การเข้ารหัสที่มีความสุข!


คุณถูกต้องว่ารหัสที่จัดหาอินเทอร์เฟซจะรู้เพิ่มเติมเกี่ยวกับสิ่งที่สามารถผิดพลาดได้มากกว่ารหัสที่ใช้อินเทอร์เฟซ แต่สมมติว่าวิธีการใช้ทรัพยากรสองชนิดที่มีประเภทอินเตอร์เฟสเดียวกันและความล้มเหลว หรือถ้าหนึ่งในทรัพยากรเหล่านั้นภายในเป็นรายละเอียดการใช้งานที่ผู้สร้างไม่รู้จักใช้ทรัพยากรอื่น ๆ ที่ซ้อนกันประเภทเดียวกัน? มีชั้นธุรกิจโยนWrappedFirstResourceExceptionหรือWrappedSecondResourceExceptionและต้องการเลเยอร์ "เดินสาย" เพื่อดูภายในข้อยกเว้นนั้นเพื่อดูสาเหตุของปัญหา ...
supercat

... อาจจะเหนอะ แต่มันก็ดูเหมือนจะดีกว่าสมมติว่าใด ๆยกเว้นจะมีผลการดำเนินงานที่มีFailingInputResource in1ที่จริงแล้วฉันคิดว่าในหลาย ๆ กรณีวิธีการที่ถูกต้องน่าจะให้เลเยอร์การเดินสายผ่านวัตถุการจัดการที่มีข้อยกเว้นและมีเลเยอร์ธุรกิจรวมอยู่ด้วยcatchซึ่งจะเรียกhandleExceptionวิธีการของวัตถุนั้น วิธีการดังกล่าวสามารถสร้างใหม่หรือจัดหาข้อมูลเริ่มต้นหรือวางพร้อมท์ "ยกเลิก / ลองใหม่ / ล้มเหลว" และให้ผู้ประกอบการตัดสินใจว่าจะทำอย่างไรเป็นต้นขึ้นอยู่กับว่าแอปพลิเคชันนั้นต้องการอะไร
supercat

@supercat ฉันเข้าใจสิ่งที่คุณพูด ฉันจะบอกว่าการใช้ทรัพยากรที่เป็นรูปธรรมมีหน้าที่รับผิดชอบในการทราบข้อยกเว้นที่สามารถนำไปใช้ได้ ไม่จำเป็นต้องระบุทุกอย่าง (เรียกว่าพฤติกรรมที่ไม่ได้กำหนด ) แต่ต้องแน่ใจว่าไม่มีความคลุมเครือ นอกจากนี้ยังมีการบันทึกข้อยกเว้นรันไทม์ที่ไม่ได้ทำเครื่องหมายไว้ด้วย หากเอกสารนั้นขัดแย้งกับเอกสารแสดงว่าเป็นข้อผิดพลาด หากคาดว่าจะมีการเรียกใช้รหัสผู้โทรเพื่อทำสิ่งใดก็ตามที่เหมาะสมเกี่ยวกับข้อยกเว้นขั้นต่ำเปล่าคือทรัพยากรห่อหุ้มสิ่งเหล่านั้นในบางUnrecoverableInternalExceptionลักษณะคล้ายกับรหัสข้อผิดพลาด HTTP 500
Daniel Dinnyes

@supercat เกี่ยวกับข้อเสนอแนะของคุณในการจัดการข้อผิดพลาดที่กำหนด: แน่นอน! ในตัวอย่างสุดท้ายของฉันตรรกะการจัดการข้อผิดพลาดเป็นรหัสยากเรียกdoMyBusinessวิธีการคงที่ นี่คือความกะทัดรัดและเป็นไปได้อย่างสมบูรณ์แบบที่จะทำให้มันมีพลังมากขึ้น เช่นHandlerระดับจะได้รับการยกตัวอย่างที่มีทรัพยากรเข้า / ส่งออกบางส่วนและมีวิธีการที่ได้รับการดำเนินการระดับหนึ่งhandle ReusableBusinessLogicInterfaceจากนั้นคุณสามารถรวม / กำหนดค่าเพื่อใช้ตัวจัดการทรัพยากรและตรรกะทางธุรกิจที่แตกต่างกันในเลเยอร์การเดินสาย
Daniel Dinnyes

10

เพื่อตอบคำถามนี้อย่างถูกต้องลองย้อนกลับไปถามคำถามพื้นฐานเพิ่มเติม

ทำไมเราถึงมีข้อยกเว้นตั้งแต่แรก?

เราโยนข้อยกเว้นเพื่อให้ผู้เรียกวิธีการของเรารู้ว่าเราไม่สามารถทำสิ่งที่เราขอให้ทำ ประเภทของข้อยกเว้นอธิบายว่าทำไมเราไม่สามารถทำสิ่งที่เราต้องการจะทำ

ลองดูรหัสบางส่วน:

double MethodA()
{
    return PropertyA - PropertyB.NestedProperty;
}

เห็นได้ชัดว่ารหัสนี้สามารถโยนข้อยกเว้นอ้างอิงPropertyBเป็นค่าว่างได้ มีสองสิ่งที่เราสามารถทำได้ในกรณีนี้เพื่อ "แก้ไข" สำหรับสถานการณ์นี้ เราสามารถ:

  • สร้าง PropertyB โดยอัตโนมัติหากเราไม่มี หรือ
  • ปล่อยให้ลูกโป่งข้อยกเว้นขึ้นกับวิธีการโทร

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

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

ดังนั้นในความเป็นจริงเรากำลังขว้างเร็วเพราะแยกความกังวลออกจากกันดีกว่ามาก ทันทีที่เกิดข้อผิดพลาดเราจะแจ้งให้ผู้พัฒนาต้นน้ำทราบเกี่ยวกับเรื่องนี้

ทำไมเรา "ช้า" เป็นเรื่องที่แตกต่าง เราไม่ต้องการที่จะไปช้าเราต้องการที่จะรีบเร็วเท่าที่เรารู้วิธีจัดการกับปัญหาอย่างถูกต้อง บางครั้งสิ่งนี้จะเป็นสิ่งที่เป็นนามธรรมสิบห้าชั้นในเวลาต่อมาและบางครั้งสิ่งนี้จะเป็นจุดที่สร้าง

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


ฉันคิดว่าคุณกำลังใช้นักพัฒนาต้นน้ำในทางที่ผิด นอกจากนี้คุณยังกล่าวว่ามันเป็นการละเมิดหลักการความรับผิดชอบเพียงอย่างเดียว แต่ในความเป็นจริงแล้วมีการดำเนินการหลายอย่างตามความต้องการและการแคชค่าใช้งานด้วยวิธีนี้ (โดยมีการควบคุมภาวะพร้อมกันที่เหมาะสมแทน)
Daniel Dinnyes

ในตัวอย่างที่คุณได้รับสิ่งที่เกี่ยวกับการตรวจสอบเป็นโมฆะก่อนที่จะดำเนินการลบเช่นนี้if(PropertyB == null) return 0;
user1451111

1
คุณสามารถอธิบายอย่างละเอียดในย่อหน้าสุดท้ายของคุณโดยเฉพาะสิ่งที่คุณหมายถึงโดย ' เลเยอร์ของนามธรรม '
user1451111

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

"ในตัวอย่างที่คุณระบุสิ่งที่เกี่ยวกับการตรวจสอบโมฆะก่อนการดำเนินการลบเช่นนี้ถ้า (PropertyB == null) ส่งคืน 0;" yuck ที่จะบอกวิธีการโทรที่ฉันมีสิ่งที่ถูกต้องเพื่อลบไปและกลับจาก ของหลักสูตรนี้เป็นบริบท แต่มันจะเป็นการปฏิบัติที่ไม่ถูกต้องในการตรวจสอบข้อผิดพลาดของฉันในกรณีส่วนใหญ่
สตีเฟ่น

6

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

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


1
คุณเขียน: โยนเร็ว ๆ นี้ ... จับเร็ว ๆ นี้ ... ! ทำไม? นั่นเป็นวิธีที่ตรงกันข้ามอย่างสิ้นเชิงในทางตรงกันข้ามกับ "โยนเร็วจับช้า"
shylynx

1
@Shylynx ฉันไม่รู้ว่ามาจากไหน "โยนเร็วจับช้า" แต่คุณค่ามันน่าสงสัย จับอะไร "สาย" หมายถึงอะไรกันแน่? มันทำให้รู้สึกถึงข้อยกเว้น (ถ้าเลย) ขึ้นอยู่กับปัญหา สิ่งเดียวที่ชัดเจนคือคุณต้องการตรวจสอบปัญหา (และโยน) ให้เร็วที่สุดเท่าที่จะทำได้
Doval

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

@Hurkyl: ปัญหาที่เกิดขึ้นกับ "catch ล่าช้า" คือหากมีข้อยกเว้นเกิดขึ้นในเลเยอร์ที่ไม่รู้อะไรเลยมันอาจเป็นเรื่องยากสำหรับโค้ดที่อาจอยู่ในสถานะที่จะทำบางสิ่งบางอย่างเกี่ยวกับสถานการณ์ที่จะรู้ว่าสิ่งต่างๆ ที่คาดหวัง ตัวอย่างง่ายๆสมมติว่าถ้า parser สำหรับไฟล์เอกสารผู้ใช้จำเป็นต้องโหลด CODEC จากดิสก์และเกิดข้อผิดพลาดดิสก์ขณะอ่านรหัสที่เรียก parser อาจทำหน้าที่ไม่เหมาะสมหากคิดว่ามีข้อผิดพลาดของดิสก์ขณะอ่านผู้ใช้ เอกสาร.
supercat

4

กฎธุรกิจที่ถูกต้องคือ 'หากซอฟต์แวร์ระดับต่ำกว่าไม่สามารถคำนวณค่าได้แล้ว ... '

สิ่งนี้สามารถแสดงออกได้ในระดับที่สูงกว่ามิฉะนั้นซอฟต์แวร์ระดับล่างพยายามเปลี่ยนพฤติกรรมตามความถูกต้องของตัวเองซึ่งจะจบลงด้วยปมเท่านั้น


2

ก่อนอื่นข้อยกเว้นสำหรับสถานการณ์พิเศษ ในตัวอย่างของคุณไม่สามารถคำนวณตัวเลขได้หากไม่มีข้อมูลดิบเพราะไม่สามารถโหลดได้

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

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

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

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

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

ไปได้ไกลกว่าที่คุณสามารถทำได้ (ในโลกอุดมคติ) โดยไม่ต้องละทิ้งtry/ catchรหัสจาก UI แทนที่จะใช้ตัวจัดการข้อยกเว้นส่วนกลางที่สามารถเข้าใจข้อยกเว้นที่อาจถูกโยนโดยเลเยอร์ที่ต่ำกว่าให้เขียนลงในบันทึกบางส่วนแล้วห่อลงในวัตถุที่มีข้อผิดพลาดซึ่งมีข้อมูลที่มีความหมาย (และอาจเป็นภาษาท้องถิ่น) ที่มีความหมาย วัตถุเหล่านั้นสามารถนำเสนอต่อผู้ใช้ในรูปแบบใด ๆ ที่คุณต้องการได้อย่างง่ายดาย (กล่องข้อความการแจ้งเตือนขนมปังปิ้งข้อความและอื่น ๆ )


1

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

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.