שאלה:
יש לי מערך של קבציםומערך של אובייקטים המכיל ערך ישן וערך חדש.
אני צריכה להחליף בכל הקבצים את הערך הישן לערך החדש.
אני עשיתי את זה כך / אשמח לשמוע ביקורת לכאן ולכאן או אפשרות לשיפור הביצועים.
מה דעתכם? האם יש דרך לשפר?
ובצד יש את המחלקה:
תשובה:
יש מקום לשיפור ונעבור על זה ביחד דרך כללי Refactoring.
קודם נגדיר מילונית (או אבטיחית) מה זה החיה המוזרה הזאת Refactoring.
Refactoring זה שינוי הקוד הקיים, כך שהוא מבצע בדיוק מה שהוא ביצע קודם, רק כתוב בצורה שונה.
כלומר, הפונקציונליות לא תשתנה, אבל הקוד עצמו ישתנה.
מה אנחנו מנסים להשיג ב-Refactoring? קריאות קוד כדי שמתכנתים יוכלו להבין את הקוד, משהו שנקרא חלוקת אחריות נכונה שגם מסתבר שזאת חיה פרוותית שעוזרת במערכות, ובכלל ננסה להביא את המערכת למצב שבה היא ברת תחזוקה.
בואו נתחיל.
כלל ראשון שנראה היום ב-Refactoring - תמיד תתנו שם משמעותי לפונקציות שלכם.
Main זה לא שם של פונקציה. DoIt זה לא שם של פונקציה. Start זה לא שם של פונקציה.
שם של פונקציה הוא הדבר הכי תיאורי שבעולם. למשל, הקוד שמולנו כנראה מאוד עובר על רשימת קבצים ומשנה להם את התוכן.
נעביר את התוכן של הפונקציה Main לפונקציה שבאמת תתאר את מה שאנחנו מנסים להשיג - החלפת תוכן.
נסמן את כל הקוד בתוך Main, נלחץ כפתור ימני, נכנס ל-Refactor ונבחר Extract Method.
ואמרנו, אנחנו רוצים שהשם של המתודה (גם באנגלית: Method) יתאר את הקונספט מאחורי מה שהיא מבצעת. ולכן שם הפונקציה החדש יהיה:
ועכשיו הקוד שלנו נראה ככה:
מה הרווחנו מזה? תוכניתן שעכשיו רוצה להבין מה קטע הקוד המפחיד הזה שראינו בשאלה, לפחות יש לו כיוון כלשהו על מה הקוד הזה באמת מבצע.
כלל שני שנראה היום ב-Refactoring - אל תכתבו הערות.
הערות בקוד זה אינהרנטית רע ומסמנות תחלואה עמוקה יותר בקוד.
אני אשאל אתכם את השאלה הקבועה:
מתי פעם אחרונה קראתם הערה שכתבתם? אולי פעם בחודש-חודשיים עשיתם טובה וקראתם הערה.
מתי פעם אחרונה שיניתם הערה שכתבתם כי היא התיישנה? אף פעם.
לא רק שהרבו המוחץ של תוכניתנים לא קוראים הערות, עוד לא נולד התוכניתן שישנה הערה שכבר כתובה.
אז בסופו של דבר נקבל הערה מיושנת, לא נכונה ומטעה.
במקום לכתוב בהערות מה שצריך להיות כתוב בקוד, נכתוב מה שהתכוונו בקוד עצמו.
מסתבר שיש מחלקה בשם managedFiles והמטרה שלה היא להחזיק טקסט שעומדים להחליף והטקסט שיחליף אותו.
למה פשוט לא לקרוא למחלקה ככה?
נילך לשם המחלקה שלנו, נמחק אותו ונכתוב מחדש שם שבאמת מתאר מה המחלקה שלנו מחזיקה.
איזה שם ארוך PairOfTextToReplaceWithTextToReplaceIt.
עכשיו שיבוא תוכניתן וינסה להבין מה המחלקה הזו עושה ומה התפקיד שלה - הוא גם יבין.
שמתם לב שהופיע לנו קו אדום קטן מתחת לשם המחלקה, כאשר נעמוד עליו עם העכבר נקבל אפשרות ש-Visual Studio 2005 ישנה לנו את שם המחלקה בכל מקום שמשתמשים בו.
ובמקום הקוד הזה:
נקבל אוטומטית את הקוד הבא:
נתנו שם משמעותי למחלקה שאנחנו עובדים איתה.
עכשיו ניתן שם משמעותי למשתנה שאנחנו עובדים איתו. מה זה אוסף בשם mf? אני לא מבין מה האוסף הזה מכיל. השם לא מספיק תיאורי.
אז בואו נחשוב ביחד, זה אוסף של טקסטים להחלפה והטקסטים שמחליפים אותם...נקרא למשתנה TextsToReplaceWithTextToReplaceThem.
נמחק את השם mf ונכתוב במקומו TextsToReplaceWithTextToReplaceThem.
וכמובן ששוב קיבלנו קו אדום מתחת לשם החדש שכמובן לא שונה ברחבי האפליקציה.
נשנה את השם באופן גורף באמצעות כלי ה-Refactoring של VS2005 והקוד הזה:
יהפוך לקוד הבא אחריו:
אפשר גם כבר למחוק את ההערה הזו למעלה שם היות והיא כבר כתובה בקוד שלנו. והקוד שלנו נראה עכשיו ככה:
נטפל גם בהערה הנוספת "רשימת הקבצים החדשה". שלמעשה זהו אוסף (גם באנגלית: Collection) של שמות קבצים שבהם נעבור ונחליף טקסטים. אז נקרא לזה בדיוק ככה.
הופך ל:
נפנה את תשומת ליבנו לקטע הקוד הבא:
אמרנו כבר, יש כאן הערה, ולכן צריך להוציא את הקוד הזה למתודה נפרדת שהשם שלה יהיה הפונקציונליות שלה.
נסמן את הקוד, נלחץ Ctrl + R, Ctrl + M (קיצור מקלדת ל-Refactor --> Extract Method של העכבר) ונוציא את הקוד הזה למתודה נפרדת.
מה הקוד הזה עושה? לפי מה שנראה הוא עובר על רשימת קבצים, ומחליף בה רשימת טקסטים בטקסטים אחרים וכותב מחדש ל-HD.
ועכשיו נקרא את באנגלית ביחד:
Replace Text In Fiels( Texts To Replace with Text To Replace Them, URLs For Fiels To Replace Their Content
למישהו יש ספק קל ביותר מה המתודה הזו עושה או מה היא היא מקבלת כפרמטרים?
ככה נראה הקוד שלנו כרגע:
עכשיו נתאמץ ממש חזק להבין מה הולך פה - יש לולאה חיצונית שעוברת על כל הקבצים, ולולאה פנימית שעוברת על כל הביטויים להחלפה ומחליפה את התוכן שלה.
כבר אמרנו - אנחנו לא אוהבים הערות. נכניס כל לולאה למתודה נפרדת. הקוד הבא:
הופך ל:
ואז ל:
למעשה כתבנו כאן תיעוד - אמרנו שהלולאה הראשונה היא IterateOverAllFilesToReplaceTheirContent והלולאה השנייה היא ReplaceAllTextsInCurrentFile. הבהרנו למען קריאות בעתיד, למה בכלל כתבנו את הלולאה הזאת.
כתבנו למה בכלל יש כאן את הקוד הזה. מה הוא אמור לעשות, למה מבצעים אותו וכך הלאה.
שימו לב אגב מה קיבלנו בפונקציה הראשונה:
קיבלנו כאן מתודה כללית, לשימוש מתי שנרצה, שמקבלת אוסף של כתובות קבצים, אוסף של טקסטים להחלפה ומבצעת החלפה.
התוכניתן שקורא למתודה הזו, בכלל לא יודע מה היא עושה, לא יודע כלום על לולאות, לא יודע כלום מה הולך בפנים - רק יודע מה הפונקציה עושה עבורו.
נעבור לשורת קוד הזו:
לא כותבים פונטית אף-פעם.
בשפה שבה אתה כותב - זאת השפה שבאוצר מילים שלה אתה תשתמש.
אפשרות אחת (וזו שנדבוק בה כתכנתים) היא להשתמש רק באנגלית ולכן נשנה את שם המשתנה והפרמטרים ל-CurrentContentOfFile.
אפשרות נוספת, שלא נעבוד איתה היא לכתוב את שם הפרמטר בעברית.
זה חוקי לחלוטין מבחינת הקומפיילר והקוד הזה יעבור קומפילציה.
אישית אני לא מעודד כתיבה בעברית ממספר סיבות.
הראשונה, עברית לא נתמכת טוב ב-Visual Studio או בשום Add-in. אורן עיני כתב על זה פוסט נהדר שמראה את המכשולים הטכניים לכתוב בעברית.
השנייה, זה הצורך הקבוע לעבור כל הזמן מעברית לאנגלית.
השלישית, עברית היא שפה נטולת Capital Letters וצריך להסתמך על קו תחתון _ בשבילה.
נחזור לשורה הזו
עדיף לאתחל פרמטר string עם String.Empty ולא עם גרשיים כפולות "".
נחזור למתודה שאנחנו כרגע עובדים עליה:
מה אנחנו כבר יודעים שלא תקין כאן מבחינת קריאות?
- השם פרמטר manf לא באמת אומר כלום על מה הפרמטר עושה ומה המטרה שלו. נשנה אותו ל-currentPairToBeUsedForReplacing.
- כל הערה כזו מסמנת לנו מקום שצריך להוציא למתודה נפרדת.
תזכרו שכל מה שאנחנו כתבנו מכל הקוד הזה - זה רק שמות המתודות במהלך Refactoring. כל תהליך שינוי הקוד בוצע אוטומטית עבורנו.
שימו לב למתודה הראשית:
הקוד הזה כל-כך ברור שהאלגוריתם מאחורי הקוד כתוב ישר מולנו.
עבור כל זוג להחלפה:
1. תשיג תוכן הקובץ
2. תשנה את תוכן הקובץ
3. תמחק את הקובץ הישן
4. תכתוב את הקובץ עם התוכן החדש
זה בדיוק מה שכתוב בקוד שלנו! שום הערות, שום תיעוד מסורבל, הכל מובע וכתוב בקוד שלנו.
מה עוד יש לנו עכשיו? האחריות על למשל "מחיקת קובץ" או למשל "יצירת קובץ" כבר לא שייכת לאיזה אלגוריתם ענק ומסורבל שלא ברור מה הוא עושה, אלא נמצאת במקום נקודתי שאם בעתיד נרצה לשנות נוכל לשנות באופן נקודתי. וגם אם לא נרצה לדעת מה הולך בתהליך הכתיבה לקובת - לא נצטרך, כי הוא מוסתר בתוך מתודה.
ראינו כמה קונספטים מעניינים:
1. אחריות של מתודות - כל מתודה מבצעת דבר אחד ודבר אחד בלבד
2. הסתרה של אופן ביצוע - המתודה "הראשית" (ביחס לתהליך שלנו) לא חושפת מה בדיוק היא מבצעת.
נמשיך עם השורה הבאה:
נתנו למתודה ReplaceAllTextsInCurrentFile את האחריות לגלות את הנתיב המלא של הקובץ.
והרי אמרנו - אנחנו אוהבים שכל מתודה עושה דבר אחד ודבר אחד בלבד. אז נעביר את גילוי הנתיב המלא למתודה נפרדת.
עכשיו נראה מתודה את המתודה הבאה:
יש לנו בעיה עם הקוד הזה. בואו נראה את הקונסטרקטור (גם באנגלית: Contructor) של StreamWriter.
כרגע נגדיר שתמיד שנרצה לפתוח StreamWriter באפליקציה שלנו ולא רק במתודה הזו - אנחנו נרצה כברירת מחדל שהוא יהיה Append=true עם קידוד UTF8. שזה מצב מאוד הגיוני לאפליקציה שתומכת בעברית. במקום לאתחל ב-40 מקומות באפליקציה StreamWriter עם הפרמטרים האלו נרצה לרכז איפשהו אותם.
נרצה להגיד שכברירת מחדל באפליקציה שלנו עובדים עם append=true ו-encoding של UTF8.
נרצה גם שהמקום הזה שאומר את זה, יחזיר לנו מופע מאותחל של StreamWriter.
מסתבר שיש משהו שנקרא Factory Design Pattern שהמטרה שלו היא לענות בדיוק על מקרים כאלו.
ניצור מחלקה סטטית בצד שתהיה StreamWriterFactory ותחזיר לנו מופע חדש של StreamWriter.
המפעל שלנו מייצר מופעים של StreamWriter, ושנרצה בכל מקום מהאפליקציה לאתחל StreamWriter נפנה למפעל ונבקש StreamWriter חדש.
נביט על מתודה נוספת
במתודה הזו, אנחנו משתמשים ב-PairOfTextToReplaceWithTextToReplaceIt כדי להחליט איזה טקסט מוחלף באיזה טקסט.
לי אישית, לא ברור ש-oldName זה הטקסט שמוחלף ו-newName זה הטקסט שמחליף אותו. נלך למחלקה שמכילה אותם ונביט עליה
נוכל באמצעות VS2005 לשנות את שמות המאפיינים (גם באנגלית: Properties) ו-VS ידאג עבורנו לשנות את שמות ה-Propert בכל מקום אחר.
מחוץ למחלקה יש למאפיינים את השמות החדשים.
למרות זאת, בתוך המחלקה עדיין נישאר עם אותם שמות משתנים פנימיים וייתכן שנרצה לשנות אותם.
VS2005 לא תומך בזה, בשביל זה יש מוצרים צד שלישי כמו Resharper, כמו CodeSmart, כמו CodeItOnce ורבים נוספים.
נדגים שימוש ב-Resharper.
נשנה גם את NewName ל-TextToBeReplacedWith.
וכמובן שבאמצעות VS2005 או באמצעות מוצרי צד-שלישי הקוד מחוץ למחלקה יראה אותו דבר:
ניקח עוד צעד אחד אחרון במתודה הזו ונשנה את rg לשם משמעותי כלשהו.
עכשיו בואו נראה דוגמה למה כל ה-Refactoring הזה טוב...
יש לנו בעיה עם האלגוריתם שלנו. כל פעם שאנחנו מחליפים טקסט בטקסט, אנחנו קוראים כל פעם מחדש את הקובץ וכותבים אותו ל-HD.
זה במקום לקרוא פעם אחת את הקובץ, לבצע את כל ההחלפות בזכרון ולשמור להארד-דיסק בסוף.
אפשר גם לראות את זה בקוד שלנו:
אפשר לראות בעיינים וממש לקרוא שכל פעם שאנחנו מבצעים החלפה אנחנו קוראים את הקובץ מחדש, מוחקים את הישן וכותבים אחד חדש.
נשנה את הקוד ככה שהוא יקרא פעם אחת בלבד ויכתב פעם אחת בלבד. נעביר את המתודות הרלוונטיות מהמתודה השנייה למתודה הראשונה.
באמצעות Refactoring גם ראינו את הבעיה בצורה הרבה יותר ברורה (כי בפעם הראשונה נחשף לענינו האלגוריתם שמימשנו) וגם היה לנו הרבה יותר קל לבצע שינויים.
נעשה Refactoring עכשיו לכל התהליך שאנחנו כיננו "החלפה של טקסטים בתוך קבצים" לתוך מחלקה נפרדת לחלוטין. ככה נראה כרגע הקוד שלנו:
אפשר להגדיר לכל מתודה כאן תפקיד - ReplaceFilesContent הוא זה שמבין איזה קבצים נחליף להם את התוכן ומה יהיו ההחלפות ו-ReplaceTextInFiels הוא התהליך שבפועל מבצע החלפות.
אבל שתי המתודות האלו יושבות באותה מחלקה. מבחינת אחריות ומבחינת שימוש חוזר בקוד נוציא החוצה את ReplaceTextInFiels שהוא התהליך עם כל המתודות הקשורות למחלקה נפרדת.
נרצה להעביר את ReplaceTextInFiels למחלקה הזו. אבל אם נעביר רק אותה ויש עדיין מתודות פנימיות שהיא משתמשת מהמחלקה הקודמת, היא לא תתקמפל. נצטרך להעביר אותה וכל המתודות הקשורות.
VS2005 לא תומך בזה, ולכן אם נאלץ לעבוד איתו נצטרך להעביר ידנית מתודה-מתודה.
כלים צד-שלישי שציינתי קודם כן תומכים בתהליך. למשל באמצעות Resharper.
שואלים אותנו לאיפה אנחנו רוצים להעביר את המתודה (לאיזו מחלקה) ואיזה מתודות נרצה להעביר.
בכחול מסומן לנו המתודות שהמתודות שסימנו תלויות בהן וגם צריכות לעבור.
ובסופו של דבר הקוד שלנו יראה ככה:
public class RefactoringExample
{
public static void Main()
{
ReplaceFilesContent();
}
private static void ReplaceFilesContent()
{
List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem = new List<PairOfTextToReplaceWithTextToReplaceIt>();
TextsToReplaceWithTextToReplaceThem.Add(new PairOfTextToReplaceWithTextToReplaceIt("replace me", "with me"));
List<string> URLsForFilesToReplaceTheirContent = new List<string>();
URLsForFilesToReplaceTheirContent.Add("myFile1.txt");
URLsForFilesToReplaceTheirContent.Add("myFile2.txt");
URLsForFilesToReplaceTheirContent.Add("myFile3.txt");
ContentReplacer.ReplaceTextInFiels(TextsToReplaceWithTextToReplaceThem, URLsForFilesToReplaceTheirContent);
}
}
public class PairOfTextToReplaceWithTextToReplaceIt
{
public PairOfTextToReplaceWithTextToReplaceIt(string textToBeReplacedWith, string textToReplace)
{
this.textToBeReplacedWith = textToBeReplacedWith;
this.textToReplace = textToReplace;
}
private string textToBeReplacedWith;
private string textToReplace;
public string TextToBeReplacedWith
{
get
{
return textToBeReplacedWith;
}
}
public string TextToReplace
{
get
{
return textToReplace;
}
}
}
public class SteamWriterFactory
{
public static StreamWriter CreateStreamWriter(string path)
{
return new StreamWriter(path, true, System.Text.Encoding.UTF8);
}
}
public static class ContentReplacer
{
public static void ReplaceTextInFiels(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)
{
IterateOverAllFilesToReplaceTheirContent(TextsToReplaceWithTextToReplaceThem, URLsForFilesToReplaceTheirContent);
}
public static void IterateOverAllFilesToReplaceTheirContent(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)
{
foreach (string strFile in URLsForFilesToReplaceTheirContent)
{
string path = GetPathForFile(strFile);
string CurrentContentOfFile = GetContentFromFile(path);
CurrentContentOfFile = ReplaceAllTextsInCurrentFile(TextsToReplaceWithTextToReplaceThem, strFile, CurrentContentOfFile);
DeleteOldFile(path);
WriteNewFile(CurrentContentOfFile, path);
}
}
public static string ReplaceAllTextsInCurrentFile(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, string strFile, string CurrentContentOfFile)
{
foreach (PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing in TextsToReplaceWithTextToReplaceThem)
{
CurrentContentOfFile = ReplaceFileContentsForCurrentTextToBeReplaced(CurrentContentOfFile, currentPairToBeUsedForReplacing);
}
return CurrentContentOfFile;
}
public static string GetPathForFile(string strFile)
{
return HttpContext.Current.Server.MapPath("../../files/") + strFile;
}
public static void WriteNewFile(string CurrentContentOfFile, string path)
{
using (StreamWriter sw = SteamWriterFactory.CreateStreamWriter(path))
{
sw.Write(CurrentContentOfFile);
}
}
public static void DeleteOldFile(string path)
{
if (File.Exists(path))
{
File.Delete(path);
}
}
public static string ReplaceFileContentsForCurrentTextToBeReplaced(string CurrentContentOfFile, PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing)
{
Regex regexUsedToReplaceText = new Regex(currentPairToBeUsedForReplacing.TextToReplace);
CurrentContentOfFile = regexUsedToReplaceText.Replace(CurrentContentOfFile, currentPairToBeUsedForReplacing.TextToBeReplacedWith);
return CurrentContentOfFile;
}
public static string GetContentFromFile(string path)
{
string CurrentContentOfFile;
using (StreamReader sr = new StreamReader(path))
{
CurrentContentOfFile = sr.ReadToEnd();
}
return CurrentContentOfFile;
}
}
הקוד שקיבלנו הוא:
1. הרבה יותר קריא
2. הרבה יותר ברור מה המטרה של הקוד כ-כלל
3. הרבה יותר ברור מה המטרה של כל שורת קוד פונקציונלית שכתבנו
4. יותר קל לשנות אותו ולעשות שינויים של האלגוריתם וגם באופן הביצוע הפנימי שלו
את הקוד הזה יכול לקרוא תוכניתן שלא מבין כלום במה עושה המערכת. זה קוד בר-תחזוקה.
נעבור בקצרה על מה ראינו:
1. נתינת שמות משמעותיים למחלקות שמכילות מידע. איך לשנות את השם שלהן באמצעות VS2005.
2. נתינת שמות משמעותיים ל-Properties של מחלקות. איך לשנות את השם של Propert באמצעות VS2005, ואיך מוצרים צד-שלישי מאפשרים שינוי מקיף של כל המחלקה ולא רק שם ה-Property.
3. ברוב המוחץ של המקרים נעדיף להימנע מלכתוב הערות ונעדיף קוד שמתעד את עצמו.
4. נתינת שמות משמעותיים למשתנים פנימיים.
5. חלוקת אחריות בין מחלקות ומתודות שונות.
6. ההטבות ש-Refoactoring מביא לנו מבחינת קריאות ויכולת תחזוקת ושינוי הקוד.
7. פתרון בעיות נפוצות באמצעות Design Patterns.
8. אין שכפול קוד ברחבי המערכת.
לקוד הזה יש עוד כברת דרך לעבור. גם מבחינת Refactoring וגם היות שהוא עדיין לא Testabily. עדיין אי-אפשר לכתוב קוד שבודק את הקוד הזה באמצעות Unit Testing היות והוא ניגש ישירות ל-HD מה שהופך אותו ל-Intergration Testing. אז נדרש שם עוד שינוי ארכיטקטורי בשביל זה. אבל טסטביליות זה נושא נפרד מ-Refactoring וזה ליום אחר.
קישור: http://www.tapuz.co.il/tapuzforum/main/Viewmsg.asp?forum=831&msgid=100306278