הבוליאני הרע

פרמטר בוליאני, דבר תמים, האמנם?

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

אותו בוליאני תמים מרמז שכנראה שבפנים יש פיצול, איזשהו תנאי, אחרת - לשם מה נולד אותו משתנה בוליאני?
המצב מחריף כשיש מספר משתנים בוליאנים - במידה ויש תלות ביניהם, אנחנו מתחילים להכנס לאוטוסטרדה של פיצולים.

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

מה עושים?
דיי פשוט - אם אפשר לפצל את הפונקציה ולהשתמש במספר פונקציות שונות או בהעמסת פונקציות, מה טוב.
אם מתאימה כאן הוספת שכבה נוספה של אבסטרקציה - לפעמים טוב יותר.



































פשוט, לא?

יש סיכוי שחלקכם לא יאהב את הפונקציה CommitAndUpload, כיוון שהיא מכילה את המילה - And, דבר שיכול להיות Code Smell לפונקציה שעושה שני דברים שונים.

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

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

במידה ואנחנו נתקלים במצב שכזה נעשה את המינימום ובקריאה לאותה פונקציה נשתמש ב-named parameter, דבר שיקל את קריאות הקוד.












תהנו,
עידן

איך לכתוב בדיקות יחידה בצורה נכונה

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


גם מאמרים באינטרנט וגם אנשים לרוב מדברים על - Arrange-Act-Assert , שיטת שלושת השלבים למבנה של בדיקת יחידה.

אבל מה מעבר לזה? מה חשוב בקוד שכזה (חוץ משהבדיקה תעבוד)?

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

לשם כך צריך לענות על שאלה פשוטה - מה העיקר ומה הטפל?
את הקוד שחשוב לראות נחשוף בגאווה ואת כל השאר נחביא על ידי הוצאת פונקציות או על ידי שימוש ב-TestInitialize או ClassInitialize.

במידה ומודול הבדיקות מכיל הרבה פרמוטציות של בדיקה דומה (דבר דיי נפוץ), נשתדל לעשות את המבחנים דומים ככל האפשר כדי לאפשר כניסה חלקה למי שהולך לקרוא את הקוד (אם הם דומים לחלוטין, נשתמש ב-Data Driven Test, מוזמנים לקרוא על כך אך זה לא הנושא של הפוסט).

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

זכרו תמיד - הקריאות (Readability) היא מעל (כמעט) הכל במקרה של בדיקות. מותר לשכפל קוד ומותר לקצר שמות משתנים כדי להכניס אותם לקונטקסט מצומצם יותר - העיקר שיהיה קריא!

כן כן, שמעתם טוב - מותר לשכפל קוד.

והנה ההסבר: במידה והמבחן נכשל, הדבר החשוב ביותר הוא להבין מהי הבעיה במהירות המירבית. כל ניסיון למנוע שכפול קוד דרך קוד משותף או Design מסובך יוביל את המתכנת לניווט בין מחלקות ומודולים שונים ויוציא אותו לגמרי מהקונטקסט שלשמו הוא נקרא - להבין מהר ובקלות מה הבדיקה עושה ומה לא עובד.
מה גם שכתיבה של קוד מלא תלויות מסובך ומורכב תדרוש תחזוקה ותקשה בעתיד על Refactoring.
בנוסף, תשתית משותפת שתנסה להיות כללית מספיק כדי להתאים למודולים ותסריטים שונים עלולה להכיל לא מעט באגים.

ההמלצה שלי היא לשתף רק מה שהכרחי דרך TestClassBase ולהשתדל לגרום למודולים שונים במערכת להיות בלתי תלויים בקוד הבדיקות שלהם גם במידה ויש חפיפה קלה בין המודולים. 
נא להפעיל שיקול דעת :)

בחזרה לעניין, נסו להבין מה הקוד זה עושה:






















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

ועכשיו תראו איזה קסם.
ככה הקובץ שלי נראה אחרי Refactor קל:




















כל מה שחשוב מול העיניים - שיטת המשלוח, ארץ המקור וארץ היעד.

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

במבט חטוף אפשר להבין בקלות מה קורה כאן ומה ספיציפית הבדיקה הזו באה לבדוק.

לאיפה כל הקוד המגעיל נעלם? הנה הוא.
סביר להניח שאף אחד לא יפתח את ה-Region ברוב המקרים, כי הוא לא כל כך מעניין בקונטקסט של הבדיקה, אבל הנה מה שמסתתר בפנים - אותו קוד מהבדיקה המקורית:






























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

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

על הדרך תרוויחו את זה שתתאהבו במבחנים שאתם כותבים, תאמינו לי :)

נספח קצר על שם המבחן - אני אישית מעדיף את המבנה הזה:

ModuleName_ActionDescription_ExpectedResult

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

זהו,
עידן

סעיפי משמר + להכשל מהר

הפוסט של היום יעסוק ב-"סעיפי משמר", או באנגלית Guard Clauses.

מהם בעצם אותם "סעיפי משמר"?
כשאנחנו נכנסים לפונקציה ניתן לומר שבמרבית המקרים אנחנו נכנסים עם פרמטרים.
כדי לשמור על ודאות גבוהה ויציבות, כדאי להכשל מהר (Fail Fast) במידה והקלט שלנו לא תקין.
דבר זה מתבצע על ידי כתיבת מספר תנאי בסיס בכניסה לפונקציה וזריקת שגיאה מתאימה (האופציה המועדפת עליי היא ArgumentException ונגזרותיה).

קל להסביר למה זה נחוץ בהוכחה על דרך השלילה - במידה ואחד מהפרמטרים אותם אנחנו מקבלים לא תקין (לדוגמה ערך Null), אנו עלולים לגלות זאת בשלב מאוחר יותר - במקרה הטוב נהיה במצב לא יציב של חוסר ודאות ובמקרה הרע יתכן והתבצע כבר קוד לא מספיק הגנתי שביצע פעולה כלשהי למרות הקלט המקולקל ויצר באג.

למה להכשל מהר?
ראשית, אם אנחנו יודעים שיש איזושהי בעיה - למה לחכות? שוב דבר טוב לא יקרה אם נחכה.
שנית, שיקולי יישור (איידנטציה) - הקוד הרבה יותר קריא, הרבה יותר מסודר והרבה יותר ברור כשהוא מישורי ולא הררי מדי.

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

הנה דוגמה קטנה:



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

 תחליטו בעצמכם מה יותר נקי ;)

עידן

פוסט ראשון

אז שלום,

אני עידן (לינקדאין), מהנדס תוכנה.

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

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

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

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

תדירות העדכונים כאן (בתקווה...) תהיה פעם בשבוע.
אם לא אצליח - לא נורא :)

אשתדל לכתוב בשפה ברורה, קלילה עם דוגמאות קוד פשוטות - והכל, בעברית. 

זהו.
תהנו.