1.10 - ממשק תוכנית ושגיאות הניתנים לבדיקה - הרצאה
מפרידים את הפענוח מן המסוף¶
עד עכשיו רבות מהדוגמאות השתמשו ב־expect כדי לעצור כאשר הקלט אינו מתאים לניסוי. בכלי אמיתי קלט שגוי הוא לעיתים קרובות מצב צפוי. נרצה להסביר אותו למשתמש בלי להציג פאניקה פנימית, ונרצה לבדוק את ההיגיון בלי להפעיל תהליך חדש בכל בדיקה.
נבנה כלי שמקבל אורך רשומה ומספר רשומות, ומחשב את גודל הנתונים. נפריד את החישוב לספרייה קטנה ואת קריאת הארגומנטים לתוכנית פקודה. צרו פרויקט Cargo רגיל והוסיפו קובץ src/lib.rs לצד src/main.rs.
הכניסו לספרייה:
#[derive(Debug, PartialEq)]
pub enum SizeError {
ZeroWidth,
Overflow,
TooLarge,
}
pub fn payload_size(count: usize, width: usize) -> Result<usize, SizeError> {
if width == 0 {
return Err(SizeError::ZeroWidth);
}
let size = count.checked_mul(width).ok_or(SizeError::Overflow)?;
if size > 1024 * 1024 {
return Err(SizeError::TooLarge);
}
Ok(size)
}
המילה pub הופכת את הממשק לזמין מחוץ למודול שלו. המשתמש אינו צריך לדעת כיצד אנחנו בודקים את הגודל, אבל הוא צריך לדעת אילו שגיאות אפשר לקבל. ההפרדה בין Overflow ל־TooLarge שומרת על ההבדל בין מגבלת הייצוג למגבלת המוצר.
התוכנית מתרגמת בין קלט משתמש לחוזה הספרייה¶
בדוגמה נניח ששם החבילה הוא size_tool. החליפו את src/main.rs:
use std::process::ExitCode;
use size_tool::payload_size;
fn run() -> Result<(), String> {
let mut args = std::env::args().skip(1);
let count_text = args.next().ok_or("usage: size_tool COUNT WIDTH")?;
let width_text = args.next().ok_or("usage: size_tool COUNT WIDTH")?;
if args.next().is_some() {
return Err("too many arguments".into());
}
let count: usize = count_text.parse().map_err(|_| "invalid count")?;
let width: usize = width_text.parse().map_err(|_| "invalid width")?;
let size = payload_size(count, width)
.map_err(|error| format!("size rejected: {error:?}"))?;
println!("{size}");
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("{message}");
ExitCode::FAILURE
}
}
}
התוכנית מחליטה איך להציג שגיאה ואיזה exit status להחזיר. הספרייה מחליטה אילו גדלים חוקיים. אין סיבה שהספרייה תדפיס למסוף בכל כשל, משום שהיא עשויה להיקרא בעתיד משירות, מבדיקה או מממשק גרפי.
המרת השגיאה למחרוזת נמצאת כאן בגבול החיצוני. בתוך הספרייה שמרנו טיפוס מובנה שאפשר להתאים עליו דפוסים. בפרקים הבאים נלמד גם ליישם Display ו־Error ולשמור שרשרת מקורות. אין צורך להכניס את כל המנגנון כדי להתחיל להפריד אחריות נכון.
בודקים את החוזה במקום את ההדפסה¶
הוסיפו בסוף src/lib.rs את המודול הבא:
#[cfg(test)]
mod tests {
use super::{payload_size, SizeError};
#[test]
fn accepts_empty_payload() {
assert_eq!(payload_size(0, 8), Ok(0));
}
#[test]
fn rejects_zero_width() {
assert_eq!(payload_size(10, 0), Err(SizeError::ZeroWidth));
}
#[test]
fn distinguishes_overflow_from_limit() {
assert_eq!(payload_size(usize::MAX, 2), Err(SizeError::Overflow));
assert_eq!(payload_size(1024 * 1024 + 1, 1), Err(SizeError::TooLarge));
}
}
הריצו cargo test. הבדיקות קוראות לאותה פונקציה שהבינארי משתמש בה, אבל אינן צריכות להקליד ארגומנטים או לפרש טקסט שיוצא למסוף. לכן הן ממוקדות בחוזה החישוב ויכולות לאתר שינוי התנהגות גם אם ניסוח הודעת המשתמש השתנה.
אין חובה שכל בדיקה תכיל בדיוק assert אחד. השאלה היא האם הבדיקה מתארת התנהגות קוהרנטית והאם כשל שלה ניתן להבנה. הבדיקה האחרונה משווה שני מסלולי שגיאה כדי להדגים את ההבחנה ביניהם. כאשר התרחישים גדלים נפריד אותם כדי לקבל אבחון ממוקד יותר.
נראות שומרת אינווריאנטים¶
פונקציה ציבורית היא התחייבות לקוראים. שדות ציבוריים מאפשרים להם לבנות ולשנות מצבים בלי לעבור דרך המתודות שלנו. כאשר נבנה טיפוס שמייצג גודל מאומת, ייתכן שנרצה לשמור את השדה פרטי ולהציע רק constructor שבודק את הטווח. כך המשתמש אינו יכול ליצור ערך לא תקין בלי שהקומפיילר יבחין בכך.
המטרה אינה להסתיר כל פרט באמצעות שכבות. אם תפקידו של מבנה הוא פשוט להעביר נתונים ואין בו אינווריאנט נוסף, שדות גלויים יכולים להיות מתאימים. ההחלטה צריכה לנבוע מן החוזה שאנחנו רוצים לשמור, ולא מכלל סגנון קשיח שמחייב getter לכל מספר.
שגיאה היא חלק ממסלול רגיל¶
הריצו את הכלי עם שני מספרים תקינים, עם ארגומנט חסר, עם מילה במקום מספר ועם גודל גדול מדי. בדקו גם את קוד היציאה באמצעות echo $? במסוף. בכל מקרה התוכנית צריכה להחזיר תוצאה או שגיאה שהוגדרה מראש, בלי להסתמך על פאניקה כמנגנון טיפול בקלט צפוי.
שימו לב שהדפסה ל־stdout עצמה יכולה להיכשל, למשל כאשר הצד השני של צינור נסגר. בפרק ממשקי המערכת נפריד גם את השגיאות האלה ונכתוב פלט דרך ממשק Write שמחזיר Result. כאן הגבלנו את הניסוי לפענוח ולחישוב כדי להשאיר את החוזה קטן וברור.
בפרויקט הפרק נחבר את הכלים שבנינו: פענוח טקסט מושאל, טיפוסי תוצאה, בעלות באוסף וממשק בדיק. השאלות על חיי הנתונים והשגיאות יישארו גלויות גם כשהקוד יהיה ארוך יותר מתוכנית של עשר שורות.