למה אני פחות אוהב פונקציות Initialize ומה אפשר לעשות בנידון

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

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

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

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

אולי לא הדוגמה הכי טובה והכי מדויקת, אבל דמיינו מחלקה שתפקידה לספק מספרים אקראיים:

var primeGenerator = new RandomGenerator();             
var num = primeGenerator.GetNext();

לעומת:

var primeGenerator = new RandomGenerator();      
primeGenerator.Initialize(seed:DateTime.Now); 
var num = primeGenerator.GetNext();


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

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

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

דמיינו שאתם מנסים למצוא את הסיבה לבאג ואתם מגלים שזה בגלל קוד מסובך ולוגי שמתחבא ב-Constructor, לא תתעצבנו?

אספקט נוסף הוא קריאה לפונקציות וירטואליות, שיכול להיות הרסני מאד ב-Constructor - דוגמה:

public class Base
{
        public Base()
        {
            Foo();
        }
  
        protected virtual void Foo() {}
    }

    public class Derived : Base
    {
        private object _object;
  
        public Derived()
        {
            _object = new object();
        }
              
        protected override void Foo()
        {
            _object.ToString();    //Null Reference!!!!
        }
    }

בגלל שסדר הקריאה ל- Constructors ביצירה של Derived הוא קודם כל ל- Constructor של Base, תהיה קריאה לפונקציה הוירטואלית שתגיע ישר למימוש שנמצא בתוך Derived ומשם נקבל Exception כיוון שעדיין לא בוצע אתחול ל-Object.

עוד חסרון בכתיבת קוד מורכב ב-Constructor - אולי לא שמתם לב או חשבתם על זה אף פעם, אבל אין ל-Constructor ערך מוחזר.
במידה והאתחול יכשל, אי אפשר יהיה להחזיר Null או false. המקסימום שנוכל לעשות הוא לזרוק Exception ולהשאיר את האובייקט שבקושי נולד במצב לא יציב.
חוץ מזה, מי רוצה לזרוק Exception ב-Constructor?
לצפות דבר שכזה, משמעו לעטוף קוד נוסף בבלוקים של Try וזה לא דבר יפה, צפוי או נכון במיוחד לעשות.

פתרונות -

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

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

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

כעת תהליך הבניה יהיה צפוי יותר, מורכב יותר ויוכל להחזיר שגיאות.


     public sealed class SQLConnector : IDBConnector
    {
        internal SQLConnector(string connectionString)
        {
            Connect(connectionString);
        }
   
        private void Connect(string connectionString)
        {
            // Initialize Logic...
        }
   
        public void Query(string query)
        {
            //Query Logic, Must be connected
        }           
    }

    public class PersistencyFactory()
    {
        public IDBConnector CreateDBConnector(string connectionString)
        {
            //here we can return something, handle excpetions, insert more creation logic and return a valid object            
            return new SQLConnector(connectionString);
        }
    }

פתרון נוסף - אתחול סמוי רק כאשר צריך, מה שקרוי Lazy Loading.

    public class SQLConnector : IDBConnector
    {
        private string _connectionString;
        private bool _isConnected = false;
   
        public SQLConnector(string connectionString)
        {   
           _connectionString = connectionString;
        }
   
        private void Connect(string connectionString)
        {
            if(_isConnected) return;
       
            // Initialize Logic...
       
            _isConnected = true;
        }
   
        public void Query(string query)
        {
            Connect(_connectionString); //Lazy...
       
            //Query Logic, Must be connected or will get excpetion
        }           
    }

תהנו,
עידן



אין תגובות:

הוסף רשומת תגובה