TDD – תכנות מונחה מבחנים (Part Three)

26 באוגוסט 2007

2 תגובות


חופש גדול הגיע


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


מאירועי הפרקים הקודמים, והכנה להמשך.


בפרק הראשון הצגנו את הרעיון מאחורי TDD – כתיבת המבחנים לפני כתיבת הקוד עצמו, הרצה של כל המבחנים, והמנטרה האינסופית "Red-Green-Refactor". בפרק השני, הורדנו והתקנו את NUnit, והתחלנו לממש את ה- Rules Of Play של משחק הקוביות Craps, לפי Wikipedia. כתבנו את המבחנים CanCreateCrapsTable ו- CanAddPlayer. לא נשאר לנו הרבה – רק לכתוב את כל השאר….


המשפט השני ב-Wiki לגביי המשחק הוא, ששחקנים מחליפים תורות, ומה שלשחקן יש לעשות, פחות או יותר, זה לזרוק שתי קוביות. לטובת כך, אני מכין Class בשם CrapsPlayerTests, ומכין אותה לשימוש ע"י NUnit בצורה הבאה:



  1. (אם עדיין אין) אני מוסיף Reference לפרוייקט ל- NUnit.Framework. (במקרה שלנו, כבר קיים כזה, מפרק 2)
  2. אני מוסיף לקובץ שלי הוראת Using עבור ה- Namespace שהוספנו לפרוייקט.
  3. אני 'מקשט' את המחלקה ב- Attribute של NUnit בשם TestFixture, והופך את המחלקה ל- Public.
  4. כל מתודה שנרצה ש- NUnit תריץ צריכה לקבל את ה- Attribute המתאים, Test, את המבחנה המתאים (להיות מסוג Void ולא לקבל פרמטרים) ולהכיל הוראת Assert אחת לפחות.

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


מבחן ראשון


כמו תמיד, אני אוהב להתחיל את הבוקר עם CanCreatePlayer. ה- Class של CrapsPlayer כבר כתוב (בפרוייקט Craps), ואני לא אהסס להשתמש בו.



        [Test]


        public void CanCreateCrapsPlayer()


        {


            CrapsPlayer cp = new CrapsPlayer("Joe");


            Assert.IsNotNull(cp);


        }


עכשיו, שוב 'דילגנו' על שלב ה- Red, וכמו שאמרנו בעבר, זה צפוי במקרה של המבחן הזה – זה לא באמת המבחן החשוב שאנחנו רוצים לכתוב. אחד הקוראים ביקש שאני אסביר למה זה צפוי שנדלג על ה- Red, ואם אנחנו מדלגים על ה- Red, למה שלא נעשה את זה תמיד. הסיבה לכך שאנחנו כותבים את המבחן הזה אינה ה- Red שלו, אלא שיש לנו תעוד בנוגע לאיך יוצרים Craps Player חדש. הסיבה לכך שאנחנו לא נכשלים בו, היא שהוראת new של השפה לא מאפשרת להחזיר Null – במילים אחרות, אין איך להיכשל בזה. בדרך כלל, לא נרצה לדלג על הכישלון של המבחן – הכישלון מראה לנו מה יש לנו לעשות, והמטרה היא רק לגרום למבחן לעבור.


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



        [Test]


        public void CanThrowTwoDice()


        {


            CrapsPlayer cp = new CrapsPlayer("Joe");


            int firstDie;


            int secondDie;


 


            cp.Roll();


            firstDie = cp.FirstDie;


            secondDie = cp.SecondDie;


 


            Assert.GreaterOrEqual((firstDie + secondDie),2,


                "Sum of dice too low.");


            Assert.LessOrEqual((firstDie + secondDie), 12,


                "Sum of dice too high.");


        }


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



        [Test]


        public void CanThrowTwoDice()


        {


            CrapsPlayer cp = new CrapsPlayer("Joe");


            int firstDie;


            int secondDie;


 


            cp.Roll(out firstDie, out secondDie);


 


            Assert.GreaterOrEqual((firstDie + secondDie),2,


                "Sum of dice too low.");


            Assert.LessOrEqual((firstDie + secondDie), 12,


                "Sum of dice too high.");


        }


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



        [Test]


        public void CanThrowTwoDice()


        {


            CrapsPlayer cp = new CrapsPlayer("Joe");


            RollOfTwoDice roll = cp.Roll();


 


            Assert.GreaterOrEqual(roll.Sum, 2,


                "Sum of dice too low.");


            Assert.LessOrEqual(roll.Sum, 12,


                "Sum of dice too high.");


        }


כעת, כדי לגרום לזה להתקמפל, יש באמת ליצור Class בשם RollOfTwoDice, עם מאפיין (Property) בשם Sum. אחרי שקיבלנו את כל המבחנים שנכשלו, נממש את ה- Class, ועכשיו הוא נראה בערך ככה :



    public class RollOfTwoDice


    {


        private int m_firstDie;


        private int m_secondDie;


 


        public int Sum


        {


            get { return m_firstDie + m_secondDie; }


        }


 


        public RollOfTwoDice()


        {


            Random r = new Random();


            m_firstDie = r.Next(1, 6);


            m_secondDie = r.Next(1, 6);


        }


אנחנו מקבלים Green Bar, ואנחנו מאושרים. אפשר לעבור לשלב הבא, Refactoring, אבל לגביי אי-אלו Refactorings של ארכיטקטורה שצריך לעשות, בינתיים, אם צריך לעשות, אני מוותר. אפשר לטעון שצריך להוסיף כאן מפעל לזריקות (Rolls-Factory), נניח, נקרא לו APairOfDice, ואז, נוכל גם לבנות Dice Factory, אבל אני לא רואה שום צורך ממשי בכך כרגע. אם בהמשך אני אצטרך לבנות מפעל, אני אבנה אותו אז – אבל לא עכשיו. עכשיו אני רוצה להתקדם.


מה שכן, אני חייב לתהות בנוגע למבחן שכתבתי. בזכות שהחלטתי קודם כל על איך אני רוצה לקבל את ה- Roll, יצרתי מחלקה בשם RollOfTwoDice. זה חמוד, אבל זה גורם למבחן שלי להיות 'תלוש' – הרי אני מעוניין לבחון את היכולות של Player, לא של ה- Roll, ובכ"ז אני בוחן את התוצאה של ה- Roll. אני חושב שאני אצור TestClass חדש בשם CrapsRollTests, ושם אבחן את התוצאה:



    [TestFixture]


    public class CrapsRollTests


    {


        [Test]


        public void CanCreateRoll()


        {


            RollOfTwoDice roll = new RollOfTwoDice();


            Assert.IsNotNull(roll);


            Assert.GreaterOrEqual(roll.Sum, 2,


                "Sum of dice too low.");


            Assert.LessOrEqual(roll.Sum, 12,


                "Sum of dice too high.");


        }


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


מבחן שני להיום


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



        [Test]


        public void CanRememberItsOwnName()


        {


            CrapsPlayer cp = new CrapsPlayer("Joe");


            Assert.AreEqual("Joe", cp.Name,


                "Player can't remember its name.");


        }


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


מבחן אחרון להיום – ה- Shooter (ללא השלכותיו)


כדי לסגור את הפרק של היום, אני חושב שנסיים לממש את הפסקה הראשונה, שם מסופר כי השחקן שזורק את הקוביות נקרא Shooter. מצד אחד, זו דרישה דיי סתומה. מצד שני, זה מזכיר לנו שבמשחק יש (בד"כ) יותר משחקן אחד. את המבחנים הבאים אני מוסיף לקובץ CrapsTableTests – ואני בוחר להתחיל דווקא בבחינת העובדה שיתכנו שני שחקנים במשחק.



        [Test]


        public void CanAddTwoPlayers()


        {


            CrapsTable ct = CreateCrapsTable();


            CrapsPlayer playerOne = new CrapsPlayer("Joe");


            CrapsPlayer playerTwo = new CrapsPlayer("Bob");


            Assert.AreEqual(0, ct.Players.Count);


 


            ct.AddPlayer(playerOne);


            ct.AddPlayer(playerTwo);


 


            Assert.AreEqual(2, ct.Players.Count);


        }


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


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



        [Test]


        public void FirstPlayerIsIndeedTheShooter()


        {


            CrapsTable ct = CreateCrapsTable();


            CrapsPlayer playerOne = new CrapsPlayer("Joe");


            CrapsPlayer playerTwo = new CrapsPlayer("Bob");


 


            ct.AddPlayer(playerOne);


            ct.AddPlayer(playerTwo);


 


            Assert.AreSame(playerOne, ct.Shooter);


        }


 כאשר שווה לשים לב לשני דברים:



  1. (מעניין, אבל לא מאוד חשוב לדעת ש)הבדיקה AreSame לא רק בודקת שוויון – אלא היא בודקת ששני האובייקטים מפנים לאותו האובייקט (או בעברית, ששני המשתנים מצביעים לאותו Instance של אובייקט).
  2. (הרבה יותר חשוב לשים לב ש)הקוד לא מתקמפל. עדיין.

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



public CrapsPlayer Shooter


{


    get { throw new NotImplementedException(); }


}


 המימוש הוא אמנם טריוויאלי, אבל אנחנו כן מקבלים Red בחלון ה- NUnit:


image


וזה כישלון יפה – רואים בדיוק מה לא בסדר בתוכנה שלנו. את המימוש המלא אני משאיר לקובץ המצורף, אבל אני מוסיף משתנה פרטי לשולחן מסוג CrapsPlayer שאם הוא null אז AddPlayer תקבע לו ערך. זה בדיוק מינימום הקוד שצריך לכתוב כדי שהמבחן יעבור, וזה לא שורת קוד אחת יותר ממה שאני רוצה לכתוב.


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


חטאים קטנים של קוד


שמתם לב שכמעט לא השתמשנו בממשקי תכנה עדיין (בעברית, Interfaceים) ? אני חושב שיש המון טעם בשימוש בהם, בעיקר, כי הם גורמים לירידה משמעותית ב- Cost Of Change של תוכנה. הגדרה מלאה ל- CoC לא מצאתי, אבל הרעיון הוא פשוט: בזכות Interfaceים יותר קל להחליף חלקים בתוכנה, לפעמים הרים של קוד, ע"י החלפה של מימוש של אותו Intercface. בנוסף, שימוש ב- Interfaceים יאפשר לנו להשתמש ב- Mock Objects (ליתר דיוק, ב- NMock, שהוא מימוש נהדר של Mock Objects).


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


לאן מכאן ?


התכנון לפרק הבא אם כן, הוא כך :



  1. לעשות Refactoring למבחן האחרון. נראה לי הגיוני להוציא את המתודה הזו של SetShooter מהלוגיקה ב- AddPlayer, ואולי אפילו להפוך אותה ל- virtual. יש בזה היגיון, אבל – לפרק הבא.
  2. לעשות כמה שיותר Extract Interface – לייצר Interfaceים מהקוד הקיים.
  3. להציג את NMock, ולהשתעשע איתה מעט.

לפרקים שאחרי הפרק הבא אני משאיר חירויות ארטיסטיות, אבל אין לי ספק שאני אצטרך להתעסק עם ה- State Machine של המשחק – ומשום מה אני מפנטז על Windows Workflow Foundation – אבל כאשר המציאות טופחת על פניי, אני נוטה כרגע לממש את ה- State Machine באמצעות State Pattern, שהוא Design Pattern שנועד לכך, רק בגלל שאני לא מצליח להסתדר עם ה- WF. כנראה שקודם אני צריך לקנות איזה ספר…


קוד, רבותיי, קוד


הקוד ניתן להורדה מכאן.


בקשתי האחרונה אליכם היא, אם נהנתם, ספרו לנו, וגם אם לא. תגובות יתקבלו בברכה.

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *

2 תגובות

  1. Justin-Josef Angel26 באוגוסט 2007 ב 14:54

    הצורת פיתוח הזו היא לא TDD.
    זה Unit testing, אבל לא TDD.

    TDD אומר "לכתוב מבחן מינימלי ביותר שנכשל ולכתוב את המינימום קוד שנדרש כדי לגרום לו לעבור", הישום שאתה מדגים הוא "לכתוב מבחן שנכשל ולכתוב קוד כדי לגרום לו לעבור".

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

    במבחן הראשון:
    Assert.IsNotNull אחרי יצירה עם new נראה מיותר משהו וסטייל סיפיפי מדי. מספיק לשים Assert.IsTrue(true) אחרי היצירה רק כדי להיות סגורים שלא נזרקת חריגה מקונסטרקטור ריק.

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

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

    במבחן השלישי:
    כנ"ל שום דבר לא עוצר אותי בטסטים שלך מלהכניס מספר סטטי קבוע של 2 ב-sum.
    כנ"ל, שום דבר לא עוצר אותי מלהחזיר סכום בטווח 2-12 אבל עם מספרים לא רלוונטיים פר-קוביה (שליליים ואף אלפים).

    כל המבחנים שלך:
    לא שמים שני Assertים במבחן ב-TDD.
    (למרות שכאן מדובר במבחני Range, אז אני עוד סלחן)
    שני Assertים או יותר במבחן אומר שאתה בודק 2 או יותר התנהגויות למבחן, במקום לבחון התנהגות אחת.
    אם אתה בודק שתי התנהגויות במקביל ב-TDD, סימן שאחת מהן אתה פשוט מניח שתעבוד ביחד עם השנייה ולכן לא כתבת את הקוד המינימלי הנדרש והמבחן המינימלי הנדרש.

    בגדול, טסט צריך להכיל בדיוק שלוש שורות: (שורות, לא חלקים)
    1. הכנת הפרמטרים הרלוונטיים כפי שמוסכם שעובדים במבחנים אחרים.
    2. ביצוע שינוי לבדיקת התנהגות אחת קטנה ויחידנית.
    3. Assert אחד ויחיד.

    זה Unit Test של TDD.

    ככה הייתי כותב את האפליקציה הזו ב-TDD:
    1. מבחן של אתחול CrapsPlayer עם קונסטרקטור ריק.
    מקפמפל, שגיאת קומפליציה, יוצר מחלקה, מריץ טסטים, הכל עובר.
    2. מבחן של פרמטר לקונסטרקטור עם Property של Name.
    מקפמל, שגיאת קומפליציה, מוסיף פרמטר לקונסטרקטור, מריץ את המבחן שבודק שהשם שמתקבל בקונסטרקטור מוחזר ב-Name Property, המבחן עובר, נסענו.
    3. עוד מבחן על שם וקונסטרקטור, אבל עם שם שונה, כדי לבדוק שהטקס שה-Name מחזיר באמת תואם לקונסטרקטור ולא Hard coded.
    4. מבחן על GetRollOneDice שבאמת מחזיר מספר.
    מקפמל, שגיאת קומפליציה, מוסיף מאפיין עם חריגת NotImplemented, בודק שמדובר במספר, המבחן נכשל, מחזיר Hard coded -1000, המבחן עובר ונסענו.
    5. מבחן שבודק רנדומליות.
    מייצר Dependency Injection של RandomNumberGenerator עם מתודה אחת ReturnGenerator שמקבל גבול עליון ותחתון ומחזיר מספר.
    כותב מבחן שעושה Injection ל-Mock RandomNumberGenerator ובודק שהקריאה הפנימית באמת מקבלת גבול תחתון של 1 וגבול עליון של 6.
    6. בודק את ה-RandomNumberGenerator האמיתי שיחזיר שישה מספרים יחודיים וברגע שיש לך שש מספרים יחודיים לבדוק שהם ששת המספרים בין 1 ל-6 (כאן אפשר ועדיף לפצל למבחן ראשי אחד עם שש תת-מבחנים שקוראים לו).

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

    הגב
  2. Felix27 באוגוסט 2007 ב 12:34

    Justin –

    1. אתה מלך. תודה על תגובה נפלאה.

    2. לא חשבתי על הפיתרון של Assert.isTrue (true), והוא פיתרון אלגנטי – אבל משהו מפריע לי בו (וזו דעתי, ודעתי בלבד): בד"כ, אחרי ש- TestFixture מחזיק 4-5 טסטים, אני משנה אותו כך שהתנאים יונחו במתודה אחת, שהמחבנים יקראו לה. אז (ואפשר לטעון שרק אז, אבל עם זה בדיוק אני לא מסכים), השימוש ב- Assert.IsNotNull הוא יותר הגיוני. בנוסף, כתבת 'לא קיימת חריגה מקונסטרקטור ריק'. מי אמר שיש קונסטרקטור ריק ? זה לא מתחייב.. ובגלל שברוב המקומות, השמות של המשתנים הם עם 2 אותיות, ההפרש בין השורה Assert.IsTrue לבין השורה Assert.IsNotNull הוא תו אחד – זה סיזיפי באותה המידה לדעתי.

    3. לא חסר לי תרחיש של גלגול קובייה אחת, אלא חסר לי (המון) מבחנים על קובייה בודדת. אני מסכים עם זה ב-100% – אבל זה Yet to be written. מעבר להזרקה של Random Number Generator, שגם אותו צריך לבחון, לפי החוקים של Craps שמופיעים בויקיפדיה, כאשר מתחלף שחקן אז אחד מעובדי הקזינו מציע לו 5 קוביות שמתוכן הוא בוחר 2 – כך שגם זה חסר לי.  בכוונה נמנעתי מזה עדיין, בגלל שהפוסט הבא בסדרה מתוכנן להציג את כל הנושא של Injection. גם ממש מפריע לי שחסר לי קלאס בשם Die (קובייה) שגם הוא צריך לעבור בחינות ולהיכתב.

    4. בספר שעליו המלצתי בחלק הראשון, של Robert C. Martin, בפרק שדן ב- Testing, בקוד השני הוא מבצע שני Assertים (עמ' 35). בנוסף, לכל אורך הספר Applying Domain Driven Design של Jimmy Nilsson, יש מבחנים עם יותר מ- Aseert יחיד. מדפדוף קל, מצאתי מבחן עם 6 Assertים (עמ' 91).

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

    המטרה שלי בשלב הזה היא לעצב את התוכנה, ולהשתדל להימנע (במידת האפשר) מפרטים טכניים. אתה צודק, אתה יכול להכניס -1000 בקובייה אחת, ו- 1002 בקובייה שנייה, וזה יצעק Snake Eyes, כי התוצאה היא 2.

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

    6. בתגובה שלך, אמרת בדיקה של קובייה אחת, ועל זה כבר הסכמנו, אבל השתמשת במתודה של GetRollOneDice – בסתירה לחוקי המשחק. לשחקן אין אפשרות לזרוק קובייה אחת, זה פשוט לא החוקים. לפי החוקים היבשים של המשחק, לפני כל הלוגיקה של על מה ניתן להמר, משתמשים בשתי קוביות כדי ליצור מספר שלם אקראי בין 2 ל- 12 (Inclusive). לו היתה קיימת קוביית ק11 (מ- D&D, קובייה בעלת 11 פאות), והיה ניתן לגלגל (ק11 + 1), זה היה שקול מבחינת החוקים הבסיסיים ממש. זה תופס על שלושת המבחנים האחרונים שכתבת.

    7. כדי להימנע מטעויות מקצועיות, אני כותב את זה ב- Baby Steps. קראתי את הפוסט שלך, How to become a great .Net developer, וזה מה שאני עושה.

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

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

    אז תודה.

    פליקס.

    הגב