מה הופך מימוש לקשה לקריאה

24/06/2024

לא משנה אם אנחנו מדברים על JavaScript, על פייתון, על פרל או על ראסט, יש כמה מאפיינים שיכולים להפוך מימוש של אלגוריתם לקשה במיוחד לקריאה. אנטון זייאנוב ריכז מימושים של UUID7 ב 32 שפות בקישור כאן: https://antonz.org/uuidv7/

ואני חשבתי שזו הזדמנות מצוינת לחפש את הקושי.

1. מה אנחנו בונים

המבנה של UUID7 כולל 48 ביטים של תווית זמן, אחרי זה 4 ביטים של גירסה (הקבוע 7), אחריהם 12 ביטים אקראיים, שני ביטים שהם 10 - שבגלל שאנחנו מייצגים את הערך כמחרוזת ותו במחרוזת הוא 4 ביטים יכולים להיות כל אחד מהערכים 8, 9, a, b ואז עוד 62 ביטים אקראיים. אני מסכים זה קצת מסורבל אבל מה שחשוב זה איך מתרגמים אותו לקוד.

2. מה הופך את המימוש לקשה לקריאה

זה לא משנה על איזה שפת תכנות אנחנו מדברים, ואני ממליץ לכם להעיף מבט בכל המימושים אצל אנטון, הקושי בקריאה הוא תמיד באותם מקומות.

האתגר הראשון הוא העתקת המערך. שימו לב לקטע הבא ב JavaScript:

# timestamp
value[0] = (timestamp >> 40) & 0xFF
value[1] = (timestamp >> 32) & 0xFF
value[2] = (timestamp >> 24) & 0xFF
value[3] = (timestamp >> 16) & 0xFF
value[4] = (timestamp >> 8) & 0xFF
value[5] = timestamp & 0xFF

למי שמכיר את האופרטור >> אין בעיה לראות כאן שמעתיקים כל פעם קטע אחר מתוך timestamp ל value, אבל האמת שאם את כל הבלוק הזה היינו מעבירים לפונקציה עם שם כמו ArrayCopy הוא היה יותר קריא. נשווה עם Java:

System.arraycopy(timestamp.array(), 2, value, 0, 6);

אפילו בלי להכיר Java ברור שהמטרה של השורה היא להעתיק דברים ממערך אחד לאחר, ובאופן כללי אפשר לסכם שקוד שנותן שמות לפעולות יהיה יותר קל להבנה מקוד שפשוט כותב אותן.

אתגר שני הוא השמות של הפונקציות המובנות בשפה. השורה הזאת ב PHP ממחישה את הסיפור:

// current timestamp in ms
$timestamp = intval(microtime(true) * 1000);

הקוד לא מספר מה היחידה ולכן צריך הערה שתסביר שמדובר במילי שניות. אני משווה את זה לסי שארפ:

long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

וברור שבגירסת הסי שארפ ההערה מיותרת.

עוד אחד מאותו סוג הוא יצירת הערכים האקראיים. ככה זה נראה בקוטלין:

private val random = SecureRandom()

וב Ruby:

value = SecureRandom.random_bytes(16).bytes

וב Elixir:

value = :crypto.strong_rand_bytes(16) |> :binary.bin_to_list()

לעומת Julia:

value = rand(UInt8, 16)

או PHP:

$value = random_bytes(16);

או אפילו go:

_, err := rand.Read(value[:])

שימו לב איך בשלושת הראשונים ברור שמדובר במספרים אקראיים חזקים המתאימים לשימוש באבטחת מידע רק לפי השם של הפונקציה, ובשלושת האחרונים הייתי נזהר לכתוב כזה קוד בלי לבדוק כמה פעמים בתיעוד.

סיפור אחרון הוא שמות למשתנים גם בחישובי ביניים. הדבר הזה מאוד בולט בקלוז'ר:

(concat
  ;; timestamp
  (map byte (.toByteArray (biginteger (System/currentTimeMillis))))
  ;; version
  [(bit-or (bit-and (first rand-array) 0x0F) 0x70)]
  [(nth rand-array 1)]
  ;; variant
  [(bit-or (bit-and (nth rand-array 2) 0x3F) 0x80)]
  (drop 3 rand-array))))

אם היינו מפרידים את החישוב של כל אחד מהחלקים לאיזה let בתחילת הפונקציה היינו מקבלים concat יותר קריא בלי הצורך בהערות:

(concat
    timestamp
    version
    rand_a
    variant
    rand_b)

סך הכל קריאת המימוש הזה ב 32 שפות תכנות רק הבהירה לי שהדרך לקוד קריא לא קשורה כל כך לתחביר השפה אלא יותר לאופן בו אנחנו משתמשים בה. בחירת שמות טובים, חלוקה ברורה לפונקציות והפרדה לרמות אבסטרקציה תמיד יעזרו לנו לבנות קוד שיהיה קל יותר לאחרים לקרוא.