שלום לכולם,
באחד מהגיחות שלי לפורום של “מתכנתים-עם-יותר-מדי-זמן-פנוי” (או בשמו האמיתי: הנדסת תוכנה בתפוז) יצא לי להיתקל בשאלה הבאה:
איך הייתם ניגשים לממש שאלה כזו מבחינת האפיון וגם תכנותית (.NET)?
1. שתי ישויות - CONTACT , CONTACT-GROUP (לטובת EMAIL)2. CONTACT-GROUP יכולה להכיל גם CONTACT וגם CONTACT-GROUP3. CONTACT יכול להיות במספר CONTACT-GROUPS או באף אחת4. יש לאפין ולכתוב תכנה המאפשרת א. יצירת CONTACT ו CONTACT-GROUP ב. הוספת CONTACT ל CONTACT-GROUP ג. מחיקת CONTACT מ CONTACT-GROUP ד. הדפסת כל ה CONTACTS בתצורה עצית ה. מיון CONTACTS עפ"י שם
1. שתי ישויות - CONTACT , CONTACT-GROUP (לטובת EMAIL)
2. CONTACT-GROUP יכולה להכיל גם CONTACT וגם CONTACT-GROUP
3. CONTACT יכול להיות במספר CONTACT-GROUPS או באף אחת
4. יש לאפין ולכתוב תכנה המאפשרת א. יצירת CONTACT ו CONTACT-GROUP ב. הוספת CONTACT ל CONTACT-GROUP ג. מחיקת CONTACT מ CONTACT-GROUP ד. הדפסת כל ה CONTACTS בתצורה עצית ה. מיון CONTACTS עפ"י שם
חלק א': תיאוריה, OODA
ברגע שקראתי את השאלה הזאת עלתה במוחי התשובה הרגילה והסטנדרטית: “אההה... OODA עם שלושה שכבות? נבנה את הכל מובנה יפה והוא ירוץ סבבי-בבי”. אז הנה איך אני הייתי ממש את השאלה הזאת לפי OODA.
1. פקד בסיס אבסטרקטי - המינימום שמשותף ל-Contact ו-Contact Group (להלן: CG) הוא שלשניהם יש שם תצוגה כלשהו. ולכן, ניצור פקד אבסטרקטי כלשהו שממנו שניהם יירשו. שימו לב, הכוח האמיתי של יצירת מחלקות בסיס אבסטרקטיות יתברר בהמשך התשובה. כרגע, נציין שאנחנו עובדים לפי Abstract Factory Design Pattern או בשמה המיקרוסופטי המחודש Provider.
שימו לב שלא כתבתי Constructor וכן איתחלתי את המשתנה הפנימי של ה-DisplayName property לערך מחרוזת ריק. אין טעם לכתוב Constructor מצב הזה שבו ברור לנו שלא ניתן לאתחל את ContactBase.
2. ניישם את Contact באמצעות ירושה מ-ContactBase. נוסיף לו איזה תכונה לשם הדגמה בכדי להבדיל אותו מ-ContactBase. בדרישות הפרוייקט מצויין שצריך לשמור בו אי-מייל, אז באמת נוסיף לו Email property. בנוסף לפי דרישת פרוייקט (4) סעיף א', נבנה Constructor ל-Contact שיקלוט DisplayName ו-Email.
3. ניישם את CG (דהלן: Contact-Group). לפי דרישת פרוייקט (4) סעיף א', יהיה לו Contructor.
מבחינת היישום של סעיף (1) של דרישות הפרוייקט שלנו - סיימנו. להזכירכם את סעיף 1: “שתי ישויות - CONTACT , CONTACT-GROUP (לטובת EMAIL)“. כעת נביט על דרישות פרוייקוט (2), (3) וסעיפים ב', ג' ו-ה' בדרישה (4). אנו יודעים כי: CG צריכה צריכה אפשרות להוסיף ולזרוק: CGים ו-Contactים. בנוסף, צריך שיהיה אפשר למיין את מבנה הנתונים CG לפי מפתח שמי כלשהו.
כעת נדבר נזכיר שSortedList היא בדיוק כמו ArrayList מהבחינה שנוכל להכניס לתוכה כל Object אחרי שנעשה לו Boxing, והערך המוסף שלה (ובמיוחד לתרגיל זה) הוא שניתן לסדר את ה-ArrayList לפי מפתח שנקבע.
נסכם, עלינו להוסיף מתודות שיאפשרו (ביחס ל-CG): להוסיף Contact, להוריד Contact, להוסיף CG ולהוריד CG. היות ו-Contact ו-Contact-Group שניהם יורשים מ-BaseContact זהו מצב אופטימלי ליצור Strongly typed Collection או במקרה שלנו BaseContact type SortedList. כלומר, דבר ראשון ניצור טיפוס נתונים פנימי שלנו שכל מטרתו בחיים היא לשמור SortedList היכולה לקבל רק ContactBase ויורשיו. מדובר על תחום השווה למאמר בפני עצמו, אבל נסתפק ביישום הזה.
בנינו את ה-Strony typed collection עם כל ה-Members שהיינו צריכים לתרגיל זה: אפשר להוסיף אך ורק ContactBase או יורשיו, ניתן להוריד אך ורק ContactBase או יורשיו, ישמנו מחדש את Count וכמו כן בנינו Indexer כדי שנוכל לגשת למערך הפנימי. באמת כל מה שעשינו זה Encupsulation ל-SortedList כך שיוכל לקבל אך ורק אובייקטים מהסוג שנקבע לו ויחשוף בדיוק מספיק פונקציונליות כדי שנוכל לעבוד איתו.
ניישם ונשכתב את ContactGroup כך שתכיל מערך פנימי של ContactItems כ-Property של ContactGroup:
נבדוק שכיסינו את הדרישות הבאות: דרישות פרוייקט (2) ו-(3), וסעיפים ב' ו-ג' בדרישת פרוייקט (4).
דרישת פרוייקט 2 מבקשת מאתנו ש-CG תוכל להכיל CG ו-Contact. היות ושני המחלקות הללו יורשות מ-ContactBase ולכל CG יש ContactItems המכיל SortedList של ContactBase אז מילאנו דרישה זו.
דרישת פרוייקט 3 מדברת על כך ש-CG ו-Contact יכולים או יכולים שלא להיות חלק מ-CG. היות וניתן ליישם את שניהם ללא צורך בלממש אותם בתוך CG גם דרישה זו מולאה.
דרישת פרוייקט 4 סעיפים ב' ו-ג' מדברים על כך שניתן יהיה להוסיף או להסיר CG או Contact מרשימה CG קיימת. היות ושני מחלקות אלו יורשות מ-ContactBase ולכל CG יש ContactItems המכיל SortedList של ContactBase ואחת מהאפשרויות היא להסיר ולהוסיף ContactBase ב-SortedList מילאנו דרישות אלו.
דרישת פרוייקט 4 סעיף ה' מדברת על מיון של CG לפי שמות התצוגה של ה-ContactItems שלה. היות ו-SortedList, שהוא טיפוס המידע הפנימי של ContactItems, מתמיין אוטומטית לפי מפתח - מילאנו דרישה זו כמו כן.
4. כל מה שנשאר הוא לממש את סעיף ד' של דרישת פרוייקט (4). והוא: תצוגה במבט עץ. ניצור Presentation Layer ובתוכה כל המימוש לתצוגת נתונים אלו. המימוש הפשוט ביותר לדרישה זו הוא דרך פקד תצוגה TreeView.
נדבר מעט על המבנה של שכבת התצוגה. כל אלמנט בשכבת התצוגה צריך אפשרות להפוך אותו ל-TreeNode (ענף בתצוגת העץ שלנו). ולכן, עלינו ליצור מחלקה שאחראית לתצוגה ל-Contact ול-CG. היות ויש לה תכונה משותפת אלינו לדאוג שהן ירשו מאותה מחלקה\ממשק. בדוגמה זו ישמתי ממשק שנורש ע“י abstract class וזה בתורו נורש ע“י פקדי התצוגה שלנו. צריך להזכיר שלא צריך לממש גם ממשק וגם abstract class, אלא אחד מהם.
ניישם את המחלקה הזו על ContactPresentation שתהיה אחראית על לממש את התצוגה של Contact:
שימו לב, כל מה שעשינו זה לבנות קונסטרקטור שמקבל לתוכו Contact ומימשנו אפשרות להחזיר ענף בעץ אשר כתוב עליו את Contact.DisplayName. נעשה דבר דומה עם CG הרי כל CG הופכת בהכרח לענף בעץ לפי ContactGroup.DisplayName. אך, בנוסף לכך - נעבור על כל הפקדים ב-ContactItems ובהתאם לאיזה סוג האובייקט נשלח אותם לפקד המתאים שיחזיר לנו עבורם ענף בעץ.
נציג דוגמה למימוש כל הקוד באפליקציה חלונאית (אשר קיים בה treeview בתצוגת עיצוב):
דבר ראשון - אני רוצה תגובות על איך ה-OOP שלי, תרגישו חופשיים להשתמש במילים כמו “אלוהי“, “חביב“ או “צריך לקחת את המקלדת שלך ולשלול לך את התעודות הכשרה של מיקרוסופט“. ביקורת בונה מאוד תועיל.
חלק ב': המציאות שלי
דבר שני - רק אם היו מאיימים לשבור לי עצם חיונית מאוד (או ערימה גבוהה של כסף) הייתי כותב ככה.
ועכשיו ברצינות - לא הייתי כותב ככה למרות כל היתרונות של הקוד (מודלריות, קלות תחזוקה אלעק וכיו”ב). עכשיו אכתוב איך באמת הייתי ניגש לפרוייקט כזה מבחינת אפיון-עיצוב ותכנות.
אני לא מאמין באפיון לפי נוהל מפת”ח. אני חושב שזה בזבזני ומיותר לכתוב את הפרוייקט שלך יראה בלי לדעת מה סביבת העבודה שלך. לעומת זאת אני מאמין גדול בשני חלקים של עיצוב מערכת: הראשון הוא אפיון-עיצוב עם הלקוח, והשני עיצוב לתכנות.
השלב הראשון של אפיון-עיצוב עם הלקוח הוא אחד מהעקרונות של Agile Software developement, לעבוד עם הלקוח ולתת לו הרגשה שהוא מבין באמת כל מה שאנו הולכים לעשות. כל תוצר שהלקוח רואה הוא צריך להיות יכול להבין בצורה מלאה בלי להשתמש במונחים שזרים לו (כגון: ”שחקנים”, “שאילתות”, “פקדים” וכיו”ב).
לצורך הזה אני כותב מסמך מאוד מפורט שמכיל את כל מה שהיה מכיל מסמך אפיון וכל מה שהיה מכיל מסמך עיצוב. אבל אני כותב את זה בצורה שלא משנה מי יקרא אותו הוא יבין אותו. בין אם מחר אני זה שקורא אותו, או התוכניתן שירש את המערכת, או המנהל שאני צריך לעבוד איתו בארגון, או המזכירה באחד המשרדים - השפה צריכה להיות אחידה, ברורה ואינטואטיבית מאוד. לעומת זאת, היא צריכה לשרת ב-100% את המטרה של מי שקורא את המסמך הזה. המנהל צריך להשיג מהמסמך הזה את כל מה שקשור אליו, התוכניתן שבונה את המערכת צריך להוציא ממנה עיצוב בקלות, התוכניתן שמתחזק את המערכת צריך להבין בדיוק מה האינטרקציה בין הגורמים השונים במערכת וכיצד זה בא לכדי ביטוי בעיצוב ובקוד, והמזכירה צריכה להבין בקלות מה התפקיד שלה בכל זה.
נשמע בלתי אפשרי? כנראה, אבל עם כמה שנות ניסיון בזה אני משתפר מפרוייקט לפרוייקט.
הפורמט הנוכחי של המסמך הוא:
1. כל מה שהיה מכיל אפיון והמסמכים הקודמים והצמודים לו (דרישות הלקוח לפי Features, טכנולוגיות בשימוש, אנשי קשר, חלוקת אחריות, לוחות זמנים וכיו“ב).
מבחינת דרישות הלקוח התצוגה היא פר Feature (עוד על Features מתישהו במאמר על Feature-driven development). למשל: הוספת פרטי ספר, עריכת פרטי ספר, חיפוש ספר, תצוגת פרטי ספר, הוספת הוצאת ספרים, עריכת הוצאת ספרים, חיפוש הוצאת ספרים, תצוגת פרטי הוצאה וכך הלאה...
הפירוט פר Feature מציג תרשים UMLי אינטואטיבי מאוד שהוא סה”כ תמונה יפה. רואים בו את האנשים וכיצד הם מתקשרים ואיזה מקורות מידע וממשוק למערכת חיצוניות\פנימיות הם חלק מאותו תרשים. העובדה שהתרשים הזה הוא “תמונה יפה“ משחרר המון-המון-המון עבודה שבכל העולם היום משקיעים ב-UML. הבעיה הרצינית עם UML נובעת מכך שהוא כלי ולא סטנדרט. בשום מקום לא כתוב לאיזה רמת פירוט צריך להגיע בתרשימי UML. ברגע שמגיעים לסטנדרט ארגוני בנושא נשקיע כל-כך הרבה זמן לעמוד בו שההשקעה הזאת פשוט לא תשתלם. התרשים ה-UMLי במסמך הזה מיועד לכולם: המנהל שמבין כיצד אנו נבנה את התוכנה שלו, המזכירה שרואה את מקומה בארגון ומציעה שינויים, התוכניתן שצריך לבנות ובשלוש שניות מבט הבין מה התהליך שהוא בונה בקוד, והתוכניתן שצריך לתחזק את המערכת ומבין בתוך אותם שלוש שניות מה הולך ומי-נגד-מי.
בנוסף הפירוט פר Feature מכיל רשימה טקסטואלית לחלוטין של Sub-Features של ה-Features הגדולים יותר. כאן זה המקום שנרשום את ה-Busniss Logic שלנו.
2. החלק השני במסמך הוא החלק של העיצוב: חילקתי את ה-Features למסכים ובתוכם יש פירוט לרמת: תצוגת נתונים, קישור בין מסכים, טפסים, עיבוד טפסחם וכיו“ב. שכותבים שהשם של הספר יהיה הכותרת של המסך זה “תצוגת נתונים“. שרוצים לציין איזה שדות ומה התנאי וולידציה עליהם נכתוב את זה “טופס“. שרוצים להגיד שנקלטים נתונים הטופס ומוסיפים אותם לטבלת ספרים זה “עיבוד טופס“. שרוצים לקשר בין מסך של ספר למסך של המחבר שלו זה “קישור בין מסכים“.
3. ERD של המסד נתונים. בנקודה שאתה כבר יודע איזה שדות יש לך במסך ואיזה כותרות ואיזה הכל - אין שום בעיה לכתוב ERD. עכשיו, בניגוד לכל שאר המסמך הזה שמלבד שהוא “מפת-דרכים“ לפרוייקט שלנו ונועד בעיקר ככלי אינטואטיבי להבנת האפליקציה - הERD אצלי הוא כלי עבודה חשוב ומהותי. כרגע אני כותב את ה-ERD בתוך תוכנת עיצוב מסדי נתונים בשם Erwin והוא מופיע במסמך עיצוב כקובץ בתוך מהמסמך. הסיבה היא שבחרתי Data-Driven development כצורת המחשבה שתקשר בין ה-Features האינטואטיביים לקוד של הפרוייקט.
עכשיו, אחרי שדיברתי המון על מה אני מאמין ומה אני חושב שצריך, תכלס' - איך הייתי ניגש לבעיה שכתובה למעלה. אני יוצא מתוך נקודת הנחה שיש לי בסיס ריאלי להעמיד אליו אפליקציה והיא לא מתרחש או ב-Active directory או ב-Outlook או במסד נתונים, אלא באחד מאלו. לשם הדגמה נבחר במסד נתונים.
א) Features במערכת:
1. יצירת איש-קשר חדש בטבלת אנשי-קשר.
2. יצירת קבוצת-קשר חדשה בטבלת קבוצות-קשר.
3. עריכת פרטי איש-קשר קיים בטבלת אנשי-קשר.
4. עריכת פרטי קבוצת-קשר קיימת בטבלת קבוצות-קשר.
5. בחירת אנשי-קשר לקבוצת-קשר קיימת בטבלה שמקשרת בין אנשי-קשר לקבוצות-קשר.
6. עריכת אנשי-קשר בקבוצת-קשר קיימת בטבלה שמקשרת בין אנשי-קשר לקבוצות-קשר.
6. תצוגת אנשי-קשר בתצוגת-עץ.
ב) ERD של המערכת - כתיבת Entity-Reletionship dIagram לשלושת הטבלאות (אנשי-קשר, קבוצות-קשר, וטבלה המקשרת בין אנשי-קשר וקבוצות-קשר). אחר כך הייתי מוסיף את הפונקציות שנובעות מה-Features. למשל אם עובדים עם Stored Procedures הייתי מוסיף לטבלת אנשי-קשר את contacts.AddNew ו-contact.EditExisting, לטבלת קבוצות-קשר הייתי מוסיף את contact_groups.AddNew ו-contact_groups.EditExisting, לטבלה שמקשרת בין שתי הטבלאות הללו הייתי יוצר את relation.AddRelation ואת relation.RemoveRelation. כמו כן הייתי מוסיף SP שמטרתה להחזיר טבלה שתתורגם בקלות ל-TreeView.
ג) כתיבת המסכים ב-Dot net forms ומקשר ביניהם ל-Stored Procedures. על חלק זה אפשר להרחיב ואני מבטיח לעשות זאת.
יש עוד המון שהיה אפשר להרחיב כאן על הארכיקטורה שאימצתי אני מאמין שהרוב ברור.
רק בקשה אחת קטנה, תחשבו על זה. זה כל מה שאני מבקש, לפני שמסכימים, לפני שלא מסכימים, לפני ששוללים, לפני שכותבים תגובה, לחשוב על זה.
ברכות,
ג'סטין-יוסף אנג'ל
היסטורית שינויים:
24.7.2005 - בחלק א': יישום Composite Design Pattern, תודה לייוניי.
25.7.2005 - בחלק א': כתיבת Presentation layer. הוספת Strongly-typed collection.
Remember Me