ทำไมฉันต้องใช้สัญญารหัส


26

ฉันเพิ่งสะดุดกับกรอบของ Microsoft สำหรับสัญญารหัส

ฉันอ่านเอกสารนิดหน่อยและพบว่าตัวเองถามอยู่ตลอดเวลา: "ทำไมฉันถึงต้องการทำสิ่งนี้เพราะมันไม่ได้และไม่สามารถทำการวิเคราะห์แบบสแตติกได้"

ตอนนี้ฉันมีรูปแบบการเขียนโปรแกรมป้องกันอยู่แล้วโดยมีข้อยกเว้นป้องกันเช่นนี้:

if(var == null) { throw new NullArgumentException(); }

ฉันยังใช้ NullObject Pattern มากและไม่ค่อยมีปัญหาใด ๆ เพิ่มการทดสอบหน่วยเข้ากับมันและคุณก็พร้อมแล้ว

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

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



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

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

@ เหยี่ยว: แปลกประสบการณ์ของฉันแตกต่างอย่างสิ้นเชิง การวิเคราะห์แบบสแตติกทำงานได้อย่างไม่มีที่ติ แต่ค่อนข้างดีและฉันไม่ค่อยเห็นคำเตือนที่ไม่จำเป็นแม้ว่าจะทำงานที่ระดับเตือน 4 (ระดับสูงสุด)
Arseni Mourzenko

ฉันเดาว่าฉันจะต้องลองเพื่อดูว่ามันทำงานอย่างไร
เหยี่ยว

คำตอบ:


42

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

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

สัญญากับยาม

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

สัญญากับการทดสอบหน่วย

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

รูปแบบสัญญากับวัตถุ Null

อย่างน้อยตอนนี้ก็อยู่ใน ball ball เดียวกัน ภาษาอย่าง Scala และ Haskell ประสบความสำเร็จอย่างสูงในการกำจัดการอ้างอิงแบบ null ทั้งหมดจากโปรแกรม (แม้ว่า Scala อนุญาตให้เป็นโมฆะอย่างเป็นทางการของการประชุมคือไม่เคยใช้มัน)

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

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

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

แต่...

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


6
นั่นเป็นคำตอบแรกที่ยอดเยี่ยมสำหรับโปรแกรมเมอร์ ดีใจที่มีส่วนร่วมในชุมชน

13

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

ดังนั้นที่นี่มาประโยชน์แรก:

ประโยชน์ที่ 1: การวิเคราะห์แบบคงที่

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

ประโยชน์ที่ 2: เรียงลำดับเอกสารที่เป็นปัจจุบันเสมอ

สัญญารหัสนอกจากนี้ยังมีการจัดเรียงของเอกสารซึ่งเป็นเสมอถึงวันที่ หากความคิดเห็น XML ของวิธีการSetProductPrice(int newPrice)บอกว่าnewPriceควรเหนือกว่าหรือเท่ากับศูนย์คุณอาจหวังว่าเอกสารนั้นเป็นข้อมูลล่าสุด แต่คุณอาจค้นพบว่ามีคนเปลี่ยนวิธีการดังกล่าวเพื่อให้เกิดการnewPrice = 0โยนArgumentOutOfRangeExceptionแต่ไม่เคยเปลี่ยนแปลงเอกสารที่เกี่ยวข้อง เมื่อพิจารณาถึงความสัมพันธ์ระหว่างสัญญาโค้ดและโค้ดเองคุณไม่มีปัญหาเกี่ยวกับเอกสารที่ไม่ซิงค์กัน

การเรียงลำดับของเอกสารที่จัดทำโดยสัญญาโค้ดนั้นมีค่าเช่นกันซึ่งบ่อยครั้งที่ความคิดเห็น XML ไม่สามารถอธิบายค่าที่ยอมรับได้ดี ฉันสงสัยหลายครั้งว่าnullหรือstring.Emptyหรือ\r\nเป็นค่าที่ได้รับอนุญาตสำหรับวิธีการและความคิดเห็น XML เงียบในที่!

โดยสรุปหากไม่มีสัญญารหัสโค้ดจำนวนมากก็เป็นเช่นนั้น:

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

ด้วยสัญญารหัสมันจะกลายเป็น:

อาร์กิวเมนต์หัวเรื่องสามารถเป็นสตริงที่ไม่ใช่ค่า null ที่มีความยาว 0..500 จำนวนเต็มซึ่งตามมาเป็นค่าบวกซึ่งสามารถเป็นศูนย์ได้เฉพาะเมื่อสตริงว่างเปล่า ในที่สุดฉันจะคืนIDefinitionวัตถุไม่เป็นโมฆะ

ประโยชน์ที่ 3: สัญญาการเชื่อมต่อ

ประโยชน์ประการที่สามคือสัญญาที่ให้อำนาจกับอินเตอร์เฟส สมมติว่าคุณมีสิ่งที่ชอบ:

public interface ICommittable
{
    public ICollection<AtomicChange> PendingChanges { get; }

    public void CommitChanges();

    ...
}

คุณจะใช้การยืนยันและข้อยกเว้นCommitChangesเพียงอย่างเดียวPendingChangesได้อย่างไรรับประกันได้ว่าสามารถเรียกได้เฉพาะเมื่อไม่ว่างเปล่า คุณจะรับประกันได้อย่างไรว่าPendingChangesไม่เคยมีnull?

ประโยชน์ที่ 4: บังคับใช้ผลลัพธ์ของวิธีการ

ในที่สุดประโยชน์ที่สี่คือการได้Contract.Ensureผลลัพธ์ จะเกิดอะไรขึ้นหากเมื่อเขียนวิธีที่ส่งกลับจำนวนเต็มฉันต้องการตรวจสอบให้แน่ใจว่าค่านั้นไม่ด้อยกว่าหรือเท่ากับศูนย์ รวมถึงห้าปีต่อมาหลังจากประสบกับความเปลี่ยนแปลงมากมายจากนักพัฒนามากมาย? ทันทีที่วิธีหนึ่งมีหลายจุดคืนค่าAsserts กลายเป็นฝันร้ายในการบำรุงรักษา


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

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


3

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

สัญญามีการตรวจสอบนอกวิธีการ

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

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

หากคุณมีวิธีการป้องกันสัญญาที่เรียกว่าหลายแห่งสิ่งนี้จะเป็นประโยชน์อย่างแท้จริง


2

สองสิ่งจริง ๆ :

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