ฉันควรเพิ่มรหัสที่ซ้ำซ้อนตอนนี้ในกรณีที่อาจจำเป็นในอนาคตหรือไม่


114

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

ตัวอย่างเช่นฉันกำลังทำงานกับแอปพลิเคชันมือถือที่มีรหัสนี้:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
    }

    //2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
    if(rows.Count == 0)
    {
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

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

อย่างไรก็ตามอาจมีกรณีในอนาคตที่ifจำเป็นต้องใช้คำสั่งที่สองนี้และด้วยเหตุผลที่ทราบ

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

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

มีกฎทั่วไปของกฎหัวแม่มือสำหรับรหัสประเภทนี้หรือไม่ (ฉันจะสนใจที่จะฟังเช่นกันว่านี่จะเป็นการปฏิบัติที่ดีหรือไม่ดีหรือไม่)

หมายเหตุ: อาจถือได้ว่าคล้ายกับคำถามนี้แต่ไม่เหมือนคำถามนั้นฉันต้องการคำตอบสมมติว่าไม่มีกำหนดเวลา

TLDR:ฉันควรจะไปไกลถึงการเพิ่มรหัสซ้ำซ้อนเพื่อให้มันแข็งแกร่งขึ้นในอนาคตหรือไม่



95
ในการพัฒนาซอฟต์แวร์สิ่งที่ไม่ควรเกิดขึ้นตลอดเวลา
Roman Reiner

34
หากคุณรู้ว่าif(rows.Count == 0)จะไม่เกิดขึ้นคุณสามารถยกข้อยกเว้นเมื่อมันเกิดขึ้น - และตรวจสอบว่าทำไมสมมติฐานของคุณผิด
knut

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

9
ทำไมจะrowsเป็นโมฆะ? ไม่มีเหตุผลที่ดีอย่างน้อยใน. NET สำหรับคอลเลกชันที่เป็นโมฆะ ที่ว่างเปล่านั่นเอง แต่ไม่null ฉันจะโยนข้อยกเว้นถ้าrowsเป็นโมฆะเพราะมันหมายความว่ามีการผิดพลาดในตรรกะโดยผู้โทร
Kyralessa

คำตอบ:


176

ในแบบฝึกหัดก่อนอื่นให้ตรวจสอบตรรกะของคุณก่อน แม้ว่าเราจะเห็นว่าคุณมีปัญหาใหญ่กว่าปัญหาตรรกะใด ๆ

เรียกเงื่อนไขแรก A และเงื่อนไขสอง

ก่อนอื่นคุณพูดว่า:

เมื่อดูที่ส่วนที่สองโดยเฉพาะฉันรู้ว่าถ้าส่วนหนึ่งเป็นจริงส่วนที่สองก็จะเป็นจริงเช่นกัน

นั่นคือ: หมายถึง B หรือในแง่พื้นฐานเพิ่มเติม (NOT A) OR B

แล้ว:

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

นั่นคือ: NOT((NOT A) AND B). ใช้กฎของ Demorgan เพื่อให้ได้(NOT B) OR AB ซึ่งหมายถึง A

ดังนั้นหากทั้งสองข้อความของคุณเป็นจริงดังนั้น A หมายถึง B และ B หมายถึง A ซึ่งหมายความว่าพวกเขาจะต้องเท่ากัน

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

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

ลองดูที่การประกาศ:

public static CalendarRow AssignAppointmentToRow(
    Appointment app,    
    List<CalendarRow> rows)

เป็นสาธารณะจึงต้องมีความทนทานต่อข้อมูลที่ไม่ดีจากผู้โทรเข้าโดยพลการ

มันส่งคืนค่าดังนั้นจึงควรมีประโยชน์สำหรับค่าส่งคืนไม่ใช่ผลข้างเคียง

และยังเป็นชื่อของวิธีการที่เป็นคำกริยาแสดงให้เห็นว่ามันมีประโยชน์สำหรับผลข้างเคียงของมัน

สัญญาของพารามิเตอร์รายการคือ:

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

สัญญานี้เป็นคนบ้า ลองนึกภาพการเขียนเอกสารสำหรับสิ่งนี้! ลองนึกภาพกรณีทดสอบการเขียน!

คำแนะนำของฉัน: เริ่มใหม่ API นี้มีอินเทอร์เฟซเครื่องลูกกวาดที่เขียนทับ (การแสดงออกมาจากเรื่องราวเก่าแก่เกี่ยวกับเครื่องทำขนมของ Microsoft ที่ทั้งราคาและตัวเลือกเป็นตัวเลขสองหลักและเป็นเรื่องง่ายมากที่จะพิมพ์ใน "85" ซึ่งเป็นราคาของรายการที่ 75 และคุณจะได้รับ รายการที่ไม่ถูกต้องความจริงสนุก: ใช่ฉันได้ทำจริงโดยไม่ได้ตั้งใจเมื่อฉันพยายามเอาหมากฝรั่งออกจากตู้ขายของอัตโนมัติที่ Microsoft!)

นี่คือวิธีการออกแบบสัญญาที่สมเหตุสมผล:

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

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

ไม่เคยยอมรับ nulls; ลำดับ null เป็นสิ่งที่น่ารังเกียจ ต้องการให้ผู้โทรส่งลำดับที่ว่างเปล่าหากรู้สึกว่าไม่เป็นโมฆะ

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

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

ตอนนี้ฉันได้พูดคุยเกี่ยวกับกรณีเฉพาะของคุณเท่านั้นและคุณถามคำถามทั่วไป ดังนั้นนี่คือคำแนะนำทั่วไปเพิ่มเติม:

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

  • รับเครื่องมือครอบคลุมการทดสอบ ตรวจสอบให้แน่ใจว่าการทดสอบของคุณครอบคลุมรหัสทุกบรรทัด หากมีการเปิดเผยรหัสคุณอาจมีการทดสอบขาดหายไปหรือคุณมีรหัสตาย รหัส Dead เป็นอันตรายอย่างน่าประหลาดใจเพราะปกติแล้วมันไม่ได้ตั้งใจจะตาย! ข้อผิดพลาดด้านความปลอดภัยของ Apple "goto Fail" ที่น่ากลัวอย่างไม่น่าเชื่อเมื่อสองสามปีก่อนเป็นสิ่งที่น่าจดจำ

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

หากดูเหมือนว่าฉันกำลังพูดอยู่: ก่อนอื่นออกแบบรหัสให้ดีและอย่างที่สองทดสอบ heck เพื่อให้แน่ใจว่ามันถูกต้องในวันนี้นั่นคือสิ่งที่ฉันกำลังพูด การทำสิ่งเหล่านั้นจะทำให้การรับมือกับอนาคตง่ายขึ้นมาก ส่วนที่ยากที่สุดเกี่ยวกับอนาคตคือการจัดการกับทุกคนที่เขียนรหัสเครื่องขนม buggy ในอดีต ทำให้ถูกต้องวันนี้และค่าใช้จ่ายจะลดลงในอนาคต


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

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

5
@ รหัสเด็กใช่เอริคเก่งในการอธิบายสิ่งต่าง ๆ ได้อย่างชัดเจนแม้กระทั่งบางหัวข้อที่ซับซ้อน :)
Mason Wheeler

3
@supercat แน่นอน แต่มันก็ยากที่จะให้เหตุผล ขั้นแรกคุณอาจต้องการดูสิ่งที่ไม่ได้แก้ไขสถานะโกลบอลดังนั้นหลีกเลี่ยงปัญหาการเกิดพร้อมกันและการคอรัปชั่น เมื่อสิ่งนี้ไม่สมเหตุสมผลคุณจะต้องแยกทั้งสองอย่างออกมา - ซึ่งจะช่วยให้คุณเห็นสถานที่ที่ชัดเจนพร้อมกันซึ่งเป็นปัญหา (และถือว่าเป็นอันตรายพิเศษ) และสถานที่ที่จัดการ นี่เป็นหนึ่งในแนวคิดหลักในกระดาษ OOP ดั้งเดิมและเป็นหัวใจของประโยชน์ของนักแสดง ไม่มีกฎทางศาสนา - เพียง แต่ต้องการแยกทั้งสองเมื่อมันสมเหตุสมผล มันมักจะทำ
Luaan

5
นี่คือโพสต์ที่มีประโยชน์มากสำหรับทุกคน!
Adrian Buzea

89

สิ่งที่คุณกำลังทำอยู่ในรหัสที่คุณกำลังแสดงข้างต้นไม่ในอนาคตมากการตรวจสอบมากที่สุดเท่าที่มันเป็นเข้ารหัสป้องกัน

ทั้งสองifงบทดสอบสิ่งต่าง ๆ ทั้งคู่เป็นการทดสอบที่เหมาะสมขึ้นอยู่กับความต้องการของคุณ

ส่วนที่ 1 ทดสอบและแก้ไขnullวัตถุ หมายเหตุด้านข้าง:การสร้างรายการไม่ได้สร้างรายการย่อย (เช่นCalendarRow)

ส่วนที่ 2 ทดสอบและแก้ไขข้อผิดพลาดของผู้ใช้และ / หรือการนำไปใช้งาน เพียงเพราะคุณมีList<CalendarRow>ไม่ได้หมายความว่าคุณมีรายการใด ๆ ในรายการ ผู้ใช้และผู้พัฒนาระบบจะทำสิ่งที่คุณนึกไม่ออกเพราะพวกเขาได้รับอนุญาตให้ทำไม่ว่าจะเหมาะสมหรือไม่ก็ตาม


1
ที่จริงฉันกำลังใช้ 'เคล็ดลับผู้ใช้งี่เง่า' เพื่อหมายถึงการป้อนข้อมูล ใช่คุณไม่ควรเชื่อถืออินพุต แม้จากนอกห้องเรียน ตรวจสอบ! หากคุณคิดว่านี่เป็นเพียงข้อกังวลในอนาคตฉันยินดีที่จะแฮ็คคุณวันนี้
candied_orange

1
@CandiedOrange นั่นเป็นเจตนา แต่ถ้อยคำไม่ได้สื่อถึงอารมณ์ขันที่พยายามทำ ฉันเปลี่ยนถ้อยคำ
Adam Zuckerman

3
เพียงคำถามสั้น ๆ ที่นี่หากเป็นข้อผิดพลาดในการใช้งาน / ข้อมูลที่ไม่ดีฉันไม่ควรทำผิดพลาดแทนที่จะลองแก้ไขข้อผิดพลาด
KidCode

4
@KidCode เมื่อใดก็ตามที่คุณพยายามกู้คืน "แก้ไขข้อผิดพลาด" คุณต้องทำสองสิ่งกลับสู่สถานะดีที่รู้จักและไม่สูญเสียข้อมูลที่มีค่าอย่างเงียบ ๆ การปฏิบัติตามกฎดังกล่าวในกรณีนี้จะทำให้เกิดคำถาม: รายการแถวศูนย์มีค่าที่ป้อนเข้าหรือไม่
candied_orange

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

35

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

โดยส่วนตัวแล้วฉันจะไม่รำคาญที่จะสร้างรหัสแสดงหัวข้อย่อยสำหรับรหัสในอนาคต ฉันไม่ทราบอนาคตและเช่นนี้รหัส "กระสุน bullet กระสุนในอนาคต" ใด ๆ ดังกล่าวนั้นถึงวาระที่จะล้มเหลวอย่างน่าสังเวชเมื่ออนาคตมาถึง

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


4
ฉันชอบจุดที่คุณไม่ทราบว่าอนาคตจะเป็นอย่างไร ตอนนี้สิ่งที่ฉันทำจริง ๆ คือการเดาว่าอะไรจะเป็นปัญหาแล้วลองเดาอีกครั้ง
KidCode

3
@ รหัสเด็ก: สังเกตดี ความคิดของคุณที่นี่จริงๆฉลาดกว่าคำตอบมากมายที่นี่รวมถึงคำตอบที่คุณยอมรับ
JacquesB

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

23

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

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

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

สำหรับความคิดเพิ่มเติมอ่านเรียงความสั้น ๆ นี้: อย่าตอกย้ำโปรแกรมของคุณในตำแหน่งตั้งตรง


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

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

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

อาจเป็นการดึงดูดให้เขียนฟังก์ชันดังนี้:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        n = 1; // Replace the negative input with an input that will work
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

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

แต่จะเป็นการดีกว่าถ้าคุณเขียนเช็คเช่นนี้:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        throw new ArgumentException("Can't have negative inputs to Fibonacci");
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

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


1
c # ไม่มีข้อยกเว้นบางประการเพื่อระบุอาร์กิวเมนต์ที่ไม่ถูกต้องหรืออาร์กิวเมนต์ที่ไม่อยู่ในช่วงหรือไม่
JDługosz

@ JDługoszใช่แล้ว! C # มีArgumentExceptionและ Java มีIllegalArgumentException
Kevin

คำถามคือการใช้ c # นี่คือC ++ (ลิงก์ไปยังสรุป)เพื่อความสมบูรณ์
JDługosz

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

2
@ Luaan: นั่นไม่ใช่ความรับผิดชอบของฟังก์ชั่นตัวอย่าง
whatsisname

11

คุณควรเพิ่มรหัสซ้ำซ้อนหรือไม่ เลขที่

แต่สิ่งที่คุณอธิบายไม่ได้รหัสซ้ำซ้อน

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

โดยส่วนตัวแล้วฉันเป็นผู้เชื่อที่ยิ่งใหญ่ในวิธีการนี้ แต่ก็เหมือนกับทุกสิ่งที่คุณต้องระวัง ใช้ตัวอย่างเช่น, C ++ std::vector::operator[]'s วางการใช้งานโหมดดีบักของ VS สักครู่ฟังก์ชันนี้จะไม่ทำการตรวจสอบขอบเขต หากคุณร้องขอองค์ประกอบที่ไม่มีอยู่ผลลัพธ์จะไม่ถูกกำหนด ขึ้นอยู่กับผู้ใช้ในการจัดทำดัชนีเวกเตอร์ที่ถูกต้อง นี่เป็นการพิจารณาโดยเจตนา: คุณสามารถ "เลือก" เพื่อ จำกัด ขอบเขตการตรวจสอบโดยเพิ่มที่ callsite แต่หากการoperator[]ติดตั้งใช้งานแล้วคุณจะไม่สามารถ "ยกเลิก" ในฐานะที่เป็นฟังก์ชั่นระดับต่ำค่อนข้างเหมาะสม

แต่ถ้าคุณกำลังเขียนAddEmployee(string name)ฟังก์ชั่นสำหรับอินเทอร์เฟซระดับสูงกว่าฉันคาดหวังอย่างเต็มที่ว่าฟังก์ชั่นนี้จะทำให้เกิดข้อยกเว้นอย่างน้อยถ้าคุณได้ให้ค่าว่างเปล่าnameรวมถึงเงื่อนไขเบื้องต้นนี้ คุณอาจไม่ได้ให้ข้อมูลผู้ใช้ที่ไม่ได้รับอนุญาตให้กับฟังก์ชั่นนี้ในวันนี้ แต่การทำให้มัน "ปลอดภัย" ในลักษณะนี้หมายความว่าการละเมิดเงื่อนไขที่ปรากฏขึ้นในอนาคตสามารถวินิจฉัยได้ง่าย ตรวจจับข้อบกพร่อง นี่ไม่ใช่ความซ้ำซ้อน: มันเป็นความขยัน

ถ้าฉันต้องคิดกฎทั่วไป (แม้ว่าตามกฎทั่วไปฉันพยายามหลีกเลี่ยงสิ่งเหล่านั้น) ฉันจะบอกว่าฟังก์ชั่นที่ตอบสนองต่อไปนี้:

  • ใช้ชีวิตในภาษาระดับสูงพิเศษ (พูด JavaScript มากกว่า C)
  • ตั้งอยู่ที่ขอบเขตของอินเตอร์เฟส
  • ไม่สำคัญต่อประสิทธิภาพ
  • ยอมรับอินพุตของผู้ใช้โดยตรง

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

หัวข้อนี้มีการสำรวจเพิ่มเติมเกี่ยวกับ Wikipedia ( https://en.wikipedia.org/wiki/Defensive_programming )


9

สองในสิบบัญญัติบัญญัติที่เกี่ยวข้องที่นี่:

  • ท่านจะไม่คิดว่าอินพุตนั้นถูกต้อง

  • ท่านจะไม่สร้างรหัสเพื่อใช้ในอนาคต

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

การตรวจสอบความว่างเปล่าไม่เกี่ยวกับการใช้ในอนาคต มันเกี่ยวข้องกับ Commandment # 1: อย่าสันนิษฐานว่าอินพุตจะถูกต้อง อย่าถือว่าฟังก์ชั่นของคุณจะได้รับการป้อนข้อมูลบางส่วน ฟังก์ชั่นควรตอบสนองในลักษณะที่เป็นตรรกะไม่ว่าจะเป็นการปลอมและทำข้อมูลให้ยุ่ง


5
บัญญัติเหล่านี้อยู่ที่ไหน คุณมีลิงค์หรือไม่? ฉันอยากรู้อยากเห็นเพราะฉันได้ทำงานในหลาย ๆ โปรแกรมที่สมัครสมาชิกที่สองของบัญญัติเหล่านั้นและหลายคนที่ไม่ได้ ผู้ที่สมัครเป็นสมาชิกของบัญญัติมักThou shall not make code for future useจะพบปัญหาด้านการบำรุงรักษาโดยเร็วแม้ว่าจะมีเหตุผลเชิงตรรกะของบัญญัติก็ตาม ฉันพบว่าในการเขียนโค้ดในชีวิตจริงพระบัญญัตินั้นมีผลบังคับใช้กับรหัสที่คุณควบคุมรายการคุณลักษณะและกำหนดเวลาเท่านั้นและทำให้แน่ใจว่าคุณไม่จำเป็นต้องใช้รหัสค้นหาในอนาคตเพื่อเข้าถึงพวกเขา ...
Cort Ammon

1
สามารถพิสูจน์ได้เล็กน้อย: หากใครสามารถประเมินความน่าจะเป็นของการใช้ในอนาคตและมูลค่าที่คาดการณ์ของรหัส "การใช้ในอนาคต" และผลิตภัณฑ์ของทั้งสองนั้นสูงกว่าค่าใช้จ่ายในการเพิ่มรหัส "การใช้ในอนาคต" เพิ่มรหัส ฉันคิดว่าบัญญัติปรากฏขึ้นในสถานการณ์ที่นักพัฒนา (หรือผู้จัดการ) ถูกบังคับให้ยอมรับว่าทักษะการประเมินของพวกเขาไม่น่าเชื่อถือเท่าที่พวกเขาต้องการดังนั้นมาตรการป้องกันที่พวกเขาเลือกที่จะไม่ประเมินงานในอนาคตเลย
Cort Ammon

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

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

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

7

คำจำกัดความของ 'รหัสซ้ำซ้อน' และ 'YAGNI' มักจะขึ้นอยู่กับว่าฉันมองไปไกลแค่ไหน

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

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

อย่างไรก็ตามหากคุณเป็นเหมือนฉันฉันคาดหวังว่าคุณจะพิมพ์มันออกมาเป็น 'โดยค่าเริ่มต้น' และมันก็ไม่ได้นำคุณไปอีกแล้ว


6

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

/** ...
*   Precondition: rows is null or nonempty
*/
public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    Assert( rows==null || rows.Count > 0 )
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

[สมมติว่านี่คือ C # Assert อาจไม่ใช่เครื่องมือที่ดีที่สุดสำหรับงานเนื่องจากไม่ได้รวบรวมในโค้ดที่นำออกใช้ แต่นั่นเป็นการถกเถียงกันในอีกวัน]

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


5

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

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

ประมาณความน่าจะเป็นที่จะต้องใช้รหัสเพิ่มเติม จากนั้นทำคณิตศาสตร์

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


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

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

3

คำถามหลักที่นี่คือ "จะเกิดอะไรขึ้นถ้าคุณทำ / ไม่"

ดังที่คนอื่น ๆ ได้ชี้ให้เห็นว่าการเขียนโปรแกรมป้องกันแบบนี้ดี แต่มันก็เป็นอันตรายเช่นกัน

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

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

การตัดสินใจทั้งหมดที่ "ไม่จำเป็น" ของคุณควรทำโดยคำนึงถึงสถานที่นั้น


2

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

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

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

if (rows == null) throw new ArgumentNullException(nameof(rows));

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

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

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

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

นี่ไม่ได้หมายความว่าทุกอย่างที่ไม่คาดคิดจะทำให้แอปพลิเคชันของคุณทำงานขัดข้อง หากคุณใช้ข้อยกเว้นได้ดีพวกเขาจะสร้างจุดจัดการข้อผิดพลาดที่ปลอดภัยซึ่งคุณสามารถกู้คืนสถานะแอปพลิเคชันที่เสถียรและอนุญาตให้ผู้ใช้ดำเนินการต่อในสิ่งที่พวกเขากำลังทำอยู่ (ในขณะที่หลีกเลี่ยงการสูญหายของข้อมูล คุณจะเห็นโอกาสในการแก้ไขปัญหา ("ไม่พบหมายเลขคำสั่ง 2212 คุณหมายถึง 2212b หรือไม่") หรือให้การควบคุมผู้ใช้ ("ข้อผิดพลาดในการเชื่อมต่อกับฐานข้อมูลลองอีกครั้ง") แม้ว่าจะไม่มีตัวเลือกดังกล่าวอย่างน้อยก็จะให้โอกาสคุณที่จะไม่มีสิ่งใดเสียหาย - ฉันเริ่มเห็นคุณค่าโค้ดที่ใช้usingและtry... finallyมากกว่าtry...catchมันเปิดโอกาสให้คุณมากมายในการรักษาค่าคงที่แม้ภายใต้เงื่อนไขพิเศษ

ผู้ใช้ไม่ควรสูญเสียข้อมูลและทำงาน สิ่งนี้ยังต้องมีความสมดุลกับต้นทุนการพัฒนา ฯลฯ แต่เป็นแนวทางทั่วไปที่ค่อนข้างดี (หากผู้ใช้ตัดสินใจว่าจะซื้อซอฟต์แวร์ของคุณหรือไม่ - ซอฟต์แวร์ภายในมักไม่มีความหรูหราดังกล่าว) แม้แต่แอปพลิเคชันทั้งหมดที่ทำงานล้มเหลวก็ยิ่งมีปัญหาน้อยลงหากผู้ใช้สามารถรีสตาร์ทและกลับไปที่สิ่งที่พวกเขาทำ นี่คือความทนทานที่แท้จริง - Word บันทึกงานของคุณตลอดเวลาโดยไม่ทำให้เอกสารของคุณเสียหายบนดิสก์และให้ทางเลือกแก่คุณการคืนค่าการเปลี่ยนแปลงเหล่านั้นหลังจากที่เริ่ม Word ใหม่หลังจากเกิดความผิดพลาด มันดีกว่าไม่มีบั๊กในตอนแรกไหม? อาจจะไม่ - แต่อย่าลืมว่างานที่ใช้จับข้อผิดพลาดที่หายากอาจใช้เวลาดีกว่าทุกที่ แต่มันก็ยังดีกว่าทางเลือกอื่น ๆ - เช่นเอกสารที่เสียหายบนดิสก์งานทั้งหมดตั้งแต่การบันทึกครั้งสุดท้ายที่หายไปเอกสารถูกแทนที่โดยอัตโนมัติด้วยการเปลี่ยนแปลงก่อนที่จะเกิดความผิดพลาดซึ่งเพิ่งจะเป็น Ctrl + A และ Delete


1

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

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

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

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


1

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

คุณระบุถึงความคาดหวังในปัจจุบันของคุณเกี่ยวกับrowsอาร์กิวเมนต์: อาจเป็นโมฆะหรือมีอย่างน้อยหนึ่งรายการ ดังนั้นคำถามคือ: ควรเขียนรหัสเพื่อจัดการกับกรณีที่สามที่ไม่คาดคิดที่rowsมีรายการศูนย์หรือไม่

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

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

ดังนั้นรหัสคุณควรมีลักษณะเช่นนี้:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    if (rows != null && rows.Count == 0) throw new ArgumentException("Rows is empty."); 

    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

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


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


1

ฉันควรเพิ่มรหัสที่ซ้ำซ้อนตอนนี้ในกรณีที่อาจจำเป็นในอนาคตหรือไม่

คุณไม่ควรเพิ่มรหัสซ้ำซ้อนได้ตลอดเวลา

คุณไม่ควรเพิ่มรหัสที่จำเป็นในอนาคตเท่านั้น

คุณควรตรวจสอบให้แน่ใจว่ารหัสของคุณทำงานได้ดีไม่ว่าจะเกิดอะไรขึ้น

คำจำกัดความของ "ประพฤติดี" ขึ้นอยู่กับความต้องการของคุณ เทคนิคหนึ่งที่ฉันชอบใช้คือข้อยกเว้น "หวาดระแวง" หากฉันแน่ใจ 100% ว่ากรณีใด ๆ ไม่สามารถเกิดขึ้นได้ฉันยังคงตั้งข้อยกเว้น แต่ฉันทำในลักษณะที่ a) บอกทุกคนอย่างชัดเจนว่าฉันไม่เคยคาดหวังว่าสิ่งนี้จะเกิดขึ้นและ b) แสดงอย่างชัดเจนและบันทึกไว้และ ดังนั้นจึงไม่นำไปสู่การคืบคลานการทุจริตในภายหลัง

ตัวอย่างรหัสเทียม:

file = File.open(">", "bla")  or raise "Paranoia: cannot open file 'bla'"

file.write("xytz") or raise "Paranoia: disk full?"

file.close()  or raise "Paranoia: huh?!?!?"

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

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

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


-1

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

ก่อนส่วนหัวของวิธีการ:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)

กำหนดนัดหมายเพื่อสิ่งแถว? ควรจะชัดเจนทันทีจากรายการพารามิเตอร์ ไม่มีความรู้ใด ๆ ต่อไปผมจะคาดหวัง params (Appointment app, CalendarRow row)วิธีการที่จะมีลักษณะเช่นนี้

ถัดไป "ตรวจสอบอินพุต":

//1. Is rows equal to null? - This will be the case if this is the first appointment.
if (rows == null) {
    rows = new List<CalendarRow> ();
}

//2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
if(rows.Count == 0)
{
    rows.Add (new CalendarRow (0));
    rows [0].Appointments.Add (app);
}

นี่คือพล่าม

  1. ตรวจสอบ) ผู้เรียกเมธอดควรตรวจสอบให้แน่ใจว่าไม่ได้ผ่านค่าที่ไม่ได้กำหนดค่าเริ่มต้นไว้ภายในเมธอด มันเป็นความรับผิดชอบของโปรแกรมเมอร์ (ที่จะลอง) ไม่ให้โง่
  2. ตรวจสอบ) หากฉันไม่ได้คำนึงถึงว่าการผ่านrowsเข้าสู่วิธีการนั้นอาจผิด (ดูความคิดเห็นด้านบน) ดังนั้นจึงไม่ควรรับผิดชอบวิธีการที่เรียกว่าAssignAppointmentToRowจัดการแถวด้วยวิธีอื่นนอกเหนือจากการกำหนดนัดหมายที่ไหนสักแห่ง

แต่แนวคิดทั้งหมดของการกำหนดนัดหมายที่ไหนสักแห่งที่แปลก (ยกเว้นกรณีนี้เป็นส่วนหนึ่งของรหัส GUI) ดูเหมือนว่ารหัสของคุณจะมี (หรืออย่างน้อยก็พยายาม) โครงสร้างข้อมูลที่ชัดเจนซึ่งเป็นตัวแทนของปฏิทิน (นั่นคือList<CalendarRows><- ซึ่งควรถูกกำหนดเป็นCalendarบางแห่งหากคุณต้องการCalendar calendarใช้วิธีนี้ ถ้าคุณไปทางนี้ฉันคาดว่าcalendarจะได้รับการเติมล่วงหน้าด้วยช่องที่คุณวาง (กำหนด) การนัดหมายหลังจากนั้น (เช่นcalendar[month][day] = appointmentจะเป็นรหัสที่เหมาะสม) แต่จากนั้นคุณสามารถเลือกที่จะทิ้งโครงสร้างปฏิทินจากตรรกะหลักทั้งหมดและเพียงแค่มีList<Appointment>ที่Appointmentวัตถุมีแอตทริบิวต์date. และถ้าคุณต้องการแสดงปฏิทินใน GUI คุณสามารถสร้างโครงสร้าง 'ปฏิทินที่ชัดเจน' นี้ก่อนแสดงผล

ฉันไม่ทราบรายละเอียดของแอปของคุณดังนั้นสิ่งนี้บางอย่างอาจไม่เหมาะกับคุณ แต่การตรวจสอบทั้งสองนี้ (ส่วนใหญ่เป็นที่สอง) บอกฉันว่ามีบางอย่างผิดปกติกับการแยกข้อกังวลในรหัสของคุณ


-2

เพื่อความง่ายลองสมมติว่าในที่สุดคุณจะต้องใช้รหัสนี้ใน N วัน (ไม่ใช่ในภายหลังหรือก่อนหน้า) หรือไม่ต้องการเลย

pseudocode:

let C_now   = cost of implementing the piece of code now
let C_later = ... later
let C_maint = cost of maintaining the piece of code one day
              (note that it can be negative)
let P_need  = probability that you will need the code after N days

if C_now + C_maint * N < P_need*C_later then implement the code else omit it.

ปัจจัยสำหรับC_maint:

  • มันปรับปรุงรหัสโดยทั่วไปหรือไม่ทำให้การจัดทำเอกสารด้วยตนเองง่ายขึ้นและง่ายต่อการทดสอบ? ถ้าใช่C_maintคาดหวังเชิงลบ
  • มันทำให้โค้ดมีขนาดใหญ่ขึ้น (ซึ่งยากต่อการอ่าน, รวบรวมได้นานกว่า, ใช้การทดสอบ ฯลฯ )?
  • มีการปรับโครงสร้าง / การออกแบบใหม่ค้างอยู่หรือไม่? C_maintถ้าใช่ชน Nกรณีนี้ต้องใช้สูตรที่ซับซ้อนมากขึ้นกับตัวแปรครั้งมากขึ้นเช่น

ดังนั้นสิ่งใหญ่ที่เพิ่งจะลดน้ำหนักโค้ดและอาจจำเป็นต้องใช้ใน 2 ปีที่มีความน่าจะเป็นต่ำควรจะถูกทิ้งไว้ แต่สิ่งเล็ก ๆ น้อย ๆ ที่ยังยืนยันประโยชน์ที่เป็นประโยชน์และ 50% ที่จะต้องใช้ใน 3 เดือน .


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