שאלה ב OOP

שאלה ב OOP

מתלבט במשהו, ויכול להיות שיש איזה pattern או פתרון יותר אלגנטי. אני מפתח בסביבת דוט נט (התחלתי ב 1.1 ועברתי ל 2), בעיקר ב asp.net. יש את הקובץ web.config שניתן להגדיר אותו כך שיחזיק הגדרות לאפליקציה כולה, תחת המפתח appSettings. העניין הוא שלפעמים איזה משתמש שיש לו גישה לקובץ משנה / עלול לשנות את ההגדרה כך שהיא "בלתי אפשרית", ואני רוצה להגן על האפליקציה בפני מצב שכזה, ולהתעלם להערך השגוי ופשוט לשים ערך אחר, שמוגדר בקוד עצמו, כלומר hard coded. למשל, הגדרה שאמורה להיות מספר שלם חיובי - מרגע שאינה כזו אני קובע לה את הערך 123 (למשל). מכיוון שרוב ההגדרות שאני רוצה להגן עליהן בצורה הזו הן מספרים מסוג int, יצרתי מחלקה קטנה. ב CTOR שלה אני מכניס את ה string שמגיע מההגדרה, ערך ברירת מחדל (123 בדוגמה הקודמת), וכן גבולות לבדיקה (אופציונלי). להלן המחלקה:
internal class IntSafeValue { private int _value; public int Value { get { return _value; } } public IntSafeValue(string configName, int defaultValue, int? minValue, int? maxValue) { string temp; int k; // try to get the config value temp = System.Configuration.ConfigurationManager.AppSettings[configName]; if (string.IsNullOrEmpty(temp)) { // no value, return default value _value = defaultValue; return; } // there was something in the config file, // now check if it's an int if (!int.TryParse(temp, out k)) { // not an int _value = defaultValue; return; } // do some more logic checks... if ((null != minValue) && (minValue.HasValue) && (k < minValue.Value)) { _value = defaultValue; return; } if ((null != maxValue) && (maxValue.HasValue) && (k > maxValue.Value)) { _value = defaultValue; return; } // all ok, return the value from config
_value = k; } }​
בסוף התהליך אני מקבל ב property שנקרא Value ערך שהגיע תקין מההגדרות או שעבר תיקון (לא מפריע לי). דוגמה לשימוש:
IntSafeValue temp = new IntSafeValue("activationHours", 5, 1, 1000); if (myValue <= temp.Value) { // do something... }​
הסבר: קח מה web.config את הערך שיש תחת המפתח "activationHours", ודא שהוא int, וכן שהוא בין 1 ל 1000. אם הוא לא עומד במי מהתנאים שהוגדרו - השתמש בערך 5. ואחרי כל ההקדמה הזאת, מגיעה השאלה שלי. טכנית, ניתן גם לכתוב מתודה (סטאטית) שמבצעת את כל הבדיקות הנ"ל ומחזירה int. האם יש איזשהו יתרון בכתיבה של הסיפור הזה כמחלקה לעומת מתודה סטאטית של איזה Util? איך אתם הייתם כותבים את זה (ולמה?). תודה רבה מראש ...
 

ייוניי

New member
פונקציה סטאטית כמובן

מאחר והפעולה שאתה מבצע בעזרת הקוד שכתבת היא סטאטית - כלומר היא לא תלויה במצב (state) של האובייקט - היא צריכה להיות פונקציה סטאטית וגם יהיה לך הרבה יותר נוח לעבוד איתה ככה. אתה גם תחסוך לא מעט שורות קוד במחלקה IntSafeValue וגם תקבל בסופו של דבר בקוד שמשתמש בפונקציה טיפוס int שזה מה שרצית לקבל מלכתחילה... העקרון שעל פיו אני בודק אם אני זקוק למחלקה "אמיתית" או לא הוא האם המחלקה הזו מיישמת ממשק מסויים באופן מיוחד ושונה ממחלקות אחרות. לדוגמא, אם היית רוצה בעתיד להשתמש ב IntSafeValue עבור שליפת ערכים מ AppSettings וגם עבור שליפת ערכים מ DBMS כלשהו הייתי ממליץ לך לכתוב שתי מחלקות שונות שממשות ממשק בסגנון:
public interface SettingsStorageLocation { string getValue(string key); }​
ואז לפונקציה שלך הייתי מעביר אובייקט מהמחלקה המתאימה כל פעם (וכמובן שבתור ברירת מחדל הייתי ממשיך לעבוד עם AppSettings לטובת תמיכה לאחור). ההבדל הוא שכאן יש משמעות למצב של האובייקט שמועבר לפונקציה (האם הוא אובייקט שקורא מ AppSettings או מ DBMS) בניגוד למצב סטאטי (כמו המצב הנוכחי) שבו לא משנה איך תפעיל את IntSaveValue אתה תקבל את אותה התנהגות.
 
תודה, ממשיך באותו עניין

תודה על התשובה. אני ממשיך באותו עניין ומסבך קצת. נניח שאני גם רוצה לקבל ערך שהוא תקין בכל מקרה, וגם לדעת אם מה שהתקבל היה תקין מלכתחילה, או שעבר תיקון. אז אני יכול ליצור enum ובו מספר ערכים:
Valid DoesNotExist NotAnInt OutOfBounds​
מצד אחד, אפשר ליצור עוד property לקריאה בלבד שיספק את המידע הזה. מצד שני אפשר להוסיף פרמטר ref/out לפונקציה הסטאטית. מצד שלישי, אפשר ליצור מבנה נתונים נוסף (שמכיל int ואת ה enum הנ"ל) ולהחזיר אותו מהפונקציה הסטאטית. זה יכול להיות ValueType וזה יכול להיות class. מה דעתכם? ושוב תודה מראש לעונים ולמשתתפים...
 

ייוניי

New member
פתרון OO

בעקרון כל האפשרויות שציינת יכולות להתאים וקשה לי להצביע על מי מהם הוא טוב יותר. אם אני מסתכל על זה באופן OO הייתי מציע פתרון אחר, מאחר ואני מניח שאם מעניין אותך מה קרה בבדיקת הערך אז אתה גם תרצה לבצע פעולות שונות בהתאם למה שקרה. אני מציע מצב שבו הפונקציה שמחזירה את ה int התקין תקבל ממשק כזה:
public interface ValidityCheckResult { void valid(); void notFoundInConfigFile(); void wasFoundButWasntAnInt(); void wasFoundButOutOfBounds(); }​
לטובת הנוחות הייתי גם שם מתחת לממשק הזה מחלקה אבסרקטית כזו:
abstract class ValidityCheckResultBase : ValidityCheckResult { public virtual void valid(){} public virtual void notFoundInConfigFile(){} public virtual void wasFoundButWasntAnInt(){} public virtual void wasFoundButOutOfBounds(){} }​
עכשיו בכל פעם שתשתמש בפונקציה תוכל להחליט להתמודד עם אחת או יותר מהאפשרויות באופן הבא:
void method() { int result = IntSafeValue.getValue("name", 1, 0, 50, new ValidityCheckResultImpl()); } class ValidityCheckResultImpl : ValidityCheckResultBase { public override void wasFoundButOutOfBounds() { // TODO: Send an email to the administrator suggesting a change in // bounds of this int } }​
זה אולי נראה קצת מסורבל למי שלא רגיל ל OOP אבל בסופו של דבר זה גם פותר את הבעיה הנקודתית וגם מעודד שימוש חוזר בקוד. השיטה הזו מקלה מאוד על התהליך המתבקש שבו הטיפול בבעיות בהמרת ערכים יעבור למספר מחלקות קטן שיעשה בהן שימוש חוזר בכל האפליקציה ולא ישאר מפוזר בקוד ספציפי במקומות רבים. אם יהיו לך מקומות רבים בקוד שמבצעים:
if (IntSafeValue.WasOutOfBounds) ...​
אז יהיה לך הרבה קוד לשנות אם תחליט לשנות את ההתנהגות הכללית של האפליקציה.
 

עידו פ

New member
אפשר לחלק את זה לשתיים

1. מחלקה שמטפלת בטיפוס נתונים ספציפי (int, string, enum) ויודעת להחזיר ערכי ברירת מחדל לפי הגדרות של בדיקות תקינות (ערך חוקי, טווח ערכים, ערך מקבוצת ערכים וכו') עפ"י ערך מקור מחרוזתי. 2. מחלקה שמטפלת באחזור ערכים מקובץ קונפיגורציה - במקרה שלך כבר יש לך לגבי הראשון, אתה צריך לחשוב על התיחום של נושא זה - אם מדובר על תמיכה בטיפוסים שונים (int, float, enum...) אפשר לתכנן משהו בסגנון: 1. interface בסיסי ג'נרי (generic) שמגדיר: א. מתודה שמקבלת מחרוזת ומחזירה טיפוס T ב. מתודה שמקבלת אוסף אובייקטי "בדיקות תקינות" לגבי ערך ב"מ שיוחזר - אני משאיר לך לחשוב אם הערך הוא כללי (ולכן ניתן להגדרה במתודה הראשונה) או בהתאם לבדיקות התקינות (ואז יוגדר כחלק מאובייקט בדיקת התקינות) 2. interface בסיסי (ג'נרי) שמגדיר מתודת validate שמקבלת טיפוס T ומחזירה ערך בוליאני (או ערך ב"מ כפי שרשמתי מקודם) ואז לדוגמה: 1. אתה בונה מחלקה שמממשת הרצת בדיקות על טיפוס מספרי - מקבלת מחרוזת, בודקת אם המחרוזת היא מספר (בדיקה ראשונית) ואח"כ מריצה את כל אובייקטי בדיקות התקינות שהועברו לה ומחזירה את התוצאה המספרית הסופית 2. אתה בונה מחלקה שמממשת בדיקת תקינות של טווח ערכים עבור int, ובנוסף מחלקה שמממשת בדיקת תקינות של נניח ערך זוגי/אי-זוגי של ערכים. ואז כל מה שנותר הוא ליצור מופעים של המחלקה הראשונה, לספק להם מופעים של ולידטורים מהמחלקה השנייה, ואז להריץ את מתודת הבדיקה על המספרים שברצונך לבדוק. שיטה זו נוחה יותר אם ברצונך להרחיב בעתיד את יכולות הולידציה ולתמוך בטיפוסי משתנים נוספים באמצעות אותו מנגנון. אני משער שיש לזה אישהו שם של pattern ואני בטוח שייוניי ימצא לנו אותו.
 
תודה

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

עידו פ

New member
עדיף סטאטי

כי כפי שציינו - אין state. איך אפשר לממש מתודות סטטיות שמוגדרות ב-Interface, את השאלות האלה אני משאיר לך לשאול בפורום דוט נט
 

ייוניי

New member
חידוד קטן לגבי עניין ה state

כתבתי מקודם שליצור מופע של אובייקט על מנת לקרוא למתודה בעלת אופי סטאטי זה חסר טעם משום שאין לך בעצם state ולקיום המופע אין משמעות. הדבר אינו נכון כאשר אנחנו מדברים על קריאה וירטואלית! (כלומר על מימוש של interface) כמו שהסברתי קיומו של state הוא לא הטיעון היחיד (או אפילו העיקרי) שמצדיק את קיומה של מחלקה. מחלקה היא מוצדקת כאשר היא מיישמת ממשק קיים בדרך שונה מהמחלקות הקיימות שמיישמות אותו. ובמקרה הספציפי שאנחנו מדברים עליו הסיבה להגדרה של interface מלכתחילה היא לא סתם כדי ליצור לנו סדר או כדי ליצור סטנדרט שנוכל ליישם אותו באותו אופן במחלקות שונות. הסיבה היא שכך נוכל לעשות שימוש חוזר בקוד שקורא למתודות על ה interface על ידי כך שנחליף לו כל הזמן את אובייקט שהוא עובד עליו מאובייקט של מחלקה אחת לאובייקט של אחרת. (כמו למשל אותו קוד יקרא למתודת ה validate אבל הוא כל פעם יעבוד מול אובייקט של מחלקה אחרת שמיישמת את ממשק ה validator). אז ברור שבמצב כזה אנחנו זקוקים למופעים של אובייקטים ולמחלקות שאינן סטאטיות (מה גם שאני לא חושב שאפשר ליישם ממשק במחלקה סטאטית ובצדק).
 
תגובה

למיטב הבנתי interface לא יכול לכלול מתודות שהן static. (לפחות לא בדוט נט) מכיוון שכך, וכדי לא להגיע לאי-ודאות, לא ניתן לממש Interface ע"י מתודות סטאטיות (ניסיתי - לא עובר קומפילציה). מחלקה יכולה לממש interface רק בצורה "רגילה" ולא סטאטית. אז ממה שאני מבין עד עכשיו, אם רוצים לעבוד עם interface כפי שהוצע, אין ברירה אלא לכתוב מחלקות רגילות, גם אם אין להן state. עניין נוסף: ידוע לי שניתן לכתוב קוד שמגיע לפונקציונליות דומה לזו שהצעת בפתרון שלך עם ה interface שכולל מתודות void במקום בדיקת enum. ניתן לבצע את זה, למשל, ע"י פונקציות סטאטיות ו delegate במקום interface. מה דעתך/דעתכם לגבי טכניקה שכזו. תודה, על התגובות עד עכשיו, ואני מקווה שאני לא נשמע מציק, אני פשוט רוצה להבין טוב יותר את הגישות השונות ב OOP ולקבל רעיונות חדשים.
 

ייוניי

New member
->

קודם כל הבנת נכון. כדי לעבוד עם interface צריך לכתוב מחלקות רגילות וגם אם אין להן state זה בסדר גמור ואני כבר אומר לך שבמקרים רבים אתה תרצה להוסיף להן state בהמשך הדרך ותוכל לעשות זאת בקלות. לגבי פונקציות סטאטיות ו delegate אני לא בטוח שאני מבין את האופן שבו אתה חושב להשתמש בהם - אשמח אם תרחיב. אני רק אומר בכלליות ש delegate הוא למעשה interface של מתודה אחת במהותו. ההבדל בינו לבין interface של מתודה אחת הוא אופן היישום - מתודה פרטית במקרה של delegate לעומת מחלקה במקרה של interface. אני חייב להודות שאישית אני לא מצליח להתחבר ל delegate אולי בגלל שבגרסאות הקודמות של #C הביצועים שלו היו גרועים ואולי בגלל שבאמת חשובה לי האפשרות להוסיף מתודות נוספות בכל עת (שיש לי בממשקים). הסיבה שאני שוקל להתחיל להשתמש ב delegate היא בעיקר ה Anonymous Delegates שאני רואה הרבה צורך בשימוש בהם. אבל שוב אני כנראה אעטוף אותם עם מחלקה שחושפת ממשק
. חבל שדווקא בקטע הזה אנשי #C החליטו להיות מקוריים ולא הוסיפו את ה Anonymous Inner-Classes כמו שיש ב Java... טוב, סטיתי מהנושא, סליחה
 
דוגמה עם delegate

להלן דוגמה די דומה למה שהצעת (קצת קיצרתי את כל האפשרויות וכו'). יש כאן Console Application, וצריך להוסיף רפרנס ל System.Configuration
using System; using System.Collections.Generic; using System.Text; namespace ConsoleApplication1 { public class C1 { public static void DoesNotExistInConfig() { Console.WriteLine("hey! the value doesn't exist in the config resource!"); } } public class Settings { public static int GetResultsPerPage(DoesNotExistInConfigDelegate doesNotExistInConfigDelegate) { const int DEFAULT_VALUE = 1234; string temp; temp = System.Configuration.ConfigurationManager.AppSettings["abc"]; if (string.IsNullOrEmpty(temp)) { doesNotExistInConfigDelegate.Invoke(); return DEFAULT_VALUE; } // etc. return 678; } } public delegate void DoesNotExistInConfigDelegate(); class Program { static void Main(string[] args) { DoesNotExistInConfigDelegate myDelegate = new DoesNotExistInConfigDelegate(C1.DoesNotExistInConfig); int resultsPerPage = Settings.GetResultsPerPage(myDelegate); Console.WriteLine(string.Format("resultsPerPage = {0}", resultsPerPage.ToString())); Console.ReadLine(); } } }​
 

עידו פ

New member
לגבי הסטטי

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

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