חוזה (תכנות)

תכנות לפי חוזה (באנגלית: Design by Contract, בקיצור: DbC) היא שיטת עיצוב תוכנה, המתבססת על הגדרת מפרטים פורמליים, מדויקים וניתנים לאימות עבור ממשקים של רכיבי תוכנה. בשיטת עבודה זו, רכיבי התוכנה הם טיפוסי נתונים אבסטרקטיים, הדורשים קיום של תנאים מוקדמים (preconditions), תנאים מאוחרים (postconditions), וקבועים (invariants). מפרטים אלו נקראים "חוזים", כמטפורה לתנאים ולמחויבויות הקיימים במסגרת חוזה עסקי.
על פי שיטה זו, רכיבי התוכנה משתפים פעולה זה עם זה על בסיס "מחויבויות" ו"רווחים" הדדיים. בדומה לעולם העסקי, מוגדרים "ספק" ו"לקוח" המסכימים ביניהם על "חוזה" המגדיר דברים כמו:
הספק חייב לספק מוצר או שירות מסוים ("המחויבות"), וזכותו לצפות כי הלקוח שילם עבור המוצר ("הרווח").
הלקוח חייב לשלם עבור המוצר ("המחויבות"), וזכותו לקבל את המוצר ("הרווח").
שני הצדדים חייבים לעמוד במחויבויות מסוימות, כדוגמת תקנות וחוקים, התקפים על פני כל החוזים.
באופן דומה, בתכנות מונחה-עצמים, אם שגרה במחלקה מספקת פונקציונליות מסוימת, היא יכולה:
לצפות שתנאי מסוים ימולא כאשר מודול לקוח כלשהו קורא לה: זהו התנאי המוקדם שנדרש על ידי השגרה – המחייב את הלקוח, והוא מהווה רווח עבור הספק (המתודה עצמה), בכך שהוא משחרר אותה מהצורך לטפל במקרים שחורגים מעבר לתנאי המוקדם.
להבטיח שמשהו יתקיים בזמן היציאה: התנאי המאוחר של המתודה – זוהי ההתחייבות של הספק, והרווח עבור הלקוח (הרווח העיקרי שיוצא לו מקריאה למתודה זו).
לשמר מצב מסוים: ההנחה היא שתנאי כלשהו מתקיים בזמן הכניסה למתודה, ומובטח שהוא מתקיים גם ביציאה ממנה – זהו הקבוע של המחלקה.
החוזה הוא פורמליזציה של מחויבויות ורווחים אלו. ניתן לסכם גישה זו על ידי "שלוש שאלות" שהמתכנת צריך לחזור ולשאול את עצמו לגבי החוזה:
לְמה החוזה מצפה?
מה מבטיח החוזה?
מה החוזה משמר?
שפות תכנות רבות מספקות תמיכה מובנית לבדיקת טענות נכונות (assertions), המאפשרות לבדוק את העמידה בתנאים של החוזה. עם זאת, גישת ה"תכנות לפי חוזה" רואה בחוזים חשיבות מכרעת לתקינות התוכנה, כך שהם אמורים להיות חלק כבר בתהליך התכנון. למעשה, התומכים בתכנות לפי חוזים, כותבים את ה-assertions לפני המימוש העיקרי של התוכנה. (ראו: פיתוח מונחה-בדיקות).
רעיון ה"חוזה" יורד עד לרמת המתודה/פרוצדורה. החוזה עבור מתודה מסוימת יתאר בדרך כלל את הפרטים הבאים:
ערכים וטיפוסי נתונים חוקיים ולא חוקיים עבור הקלטים, והמשמעויות שלהם.
ערכים וטיפוסי נתונים של הפלטים (ערך מוחזר), והמשמעויות שלהם.
שגיאות ותנאים שבהם יכולות להתרחש חריגות, והמשמעויות שלהם.
תופעות לוואי.
תנאים מוקדמים (preconditions).
תנאים מאוחרים (postcondition).
קבועים (invariants).
הבטחות לגבי ביצועים (במקרים נדירים יותר), כגון: זמן ריצה, שימוש בזיכרון, ושטח דיסק.
בשימוש בירושה, המחלקה היורשת מורשית להרפות את התנאים המוקדמים (אבל לא להקשות אותם), והיא רשאית להקשיח את התנאים המאוחרים (אבל לא להחליש אותם).
כל היחסים בין מחלקות הם בין מחלקות "לקוח" לבין מחלקות "ספק". מחלקת לקוח מחויבת לבצע את הקריאות לשירותים של הספק כך שהמצב של הספק לא יופר על ידי קריאת הלקוח. עם זאת, הספק מחויב לספק "מצב החזרה" ונתונים שאינם מפרים את דרישות המצב של הלקוח. לדוגמה, חוצץ הנתונים (buffer) של הספק יכול לדרוש שהנתונים אכן ימצאו בחוצץ בזמן קריאה לשירות מחיקה של נתונים. בהמשך, הספק מבטיח ללקוח שכאשר שירות המחיקה מסיים את העבודה, הנתונים אכן ימחקו מהחוצץ. חוזים אחרים מגדירים את "קבועי המחלקה". קבוע המחלקה מבטיח (למחלקה המקומית) כי בסיום הריצה של כל אחד מהשירותים, המצב של המחלקה יישמר במסגרת טווח מוגדר מראש של מצבים.
כאשר משתמשים בחוזים, הספק לא אמור לנסות לוודא שתנאי החוזה מתממשים; הרעיון הכללי הוא שבמקרה של חריגה מתנאי החוזה, הקוד אמור "להכשל חזק", ווידוא החוזה אמור להיות רק "רשת הביטחון". ה"כישלון החזק" בשיטת התכנות לפי חוזים מקל על ניפוי שגיאות כאשר ההתנהגות הצפויה של כל שגרה מפורטת בצורה ברורה. זהו הבדל בולט לעומת שיטה דומה הנקראת defensive programming ("תכנות מתגונן"), בה הספק אחראי על ההחלטה מה לעשות כאשר אחד מהתנאים המוקדמים מופר. בדרך כלל, הספק זורק חריגה (exception) על מנת להודיע ללקוח שאחד מהתנאים המוקדמים אינו מתקיים. בשני המקרים - "תכנות לפי חוזה" ו"תכנות מתגונן" - הלקוח אמור להחליט כיצד הוא מגיב למצב כזה. תכנות לפי חוזה מקל על עבודת הספק.
תכנות לפי חוזה מסייע גם בשימוש חוזר (reuse) בקוד, מאחר שהחוזה עבור כל יחידת קוד מתועד במלואו. החוזים של מודול מסוים יכולים להחשב כסוג של דוקומנטציה של ההתנהגות עבור מודול זה.