360. Implementing an interception framework - Runtime subclassing

בהמשך לפעם הקודמת, נראה עכשיו שיטה נוספת למימוש Interception.

ראינו בעבר איזשהי שיטה באמצעות Decorator (ראו גם טיפ מספר 353).

באופן דומה לטיפ מספר 349, די מבאס (וקשה!) לכתוב את המימוש של הממשק כך שיתמוך בInterception.

מה שאפשר לעשות זה את הדבר הבא: באופן דומה לטיפ מספר 349, ניצור טיפוס בזמן ריצה שיורש מהטיפוס שלנו (או לחלופין מממש את מהממשק שלנו) ומבצע את הInterception שלנו.

כמו בטיפ מספר 349, ניתן לעשות דבר כזה ע”י שימוש בReflection.Emit.

כמו בטיפ ההוא, אני לא אראה כאן איך מממשים דבר כזה בReflection.Emit, ואראה במקום זאת שימוש בFramework קיים שנקרא DynamicProxy של Castle שיודע לבצע את זה.

נניח שאנחנו מעוניינים בLoggingInterception: בDynamicProxy יש ממשק בשם IInterceptor שנוכל לממש:

1
2
3
4
public interface IInterceptor
{
void Intercept(IInvocation invocation);
}

כאשר IInvocation הוא הממשק של Castle שמייצג קריאה לפונקציה.

זהו הממשק של Castle לInterception (ראו גם טיפ מספר 357)

כעת אנחנו יכולים לממש אותו כאוות נפשנו:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class LoggingInterceptor : IInterceptor
{
private ILog mLog = LogManager.GetLogger("MyLogger");
public void Intercept(IInvocation invocation)
{
mLog.DebugFormat("Entering {0}, with parameters {1}",
invocation.Method,
GetParameters(invocation));
try
{
invocation.Proceed();
mLog.DebugFormat("Exiting {0} with return value: {1}",
invocation.Method,
invocation.ReturnValue);
}
catch (Exception ex)
{
mLog.Error("An error occured on " + invocation.Method, ex);
throw;
}
}
private static string GetParameters(IInvocation call)
{
return string.Join
(",",
call.Method.GetParameters().Select((x, i) =>
new {x.Name, Index = i}).
Select(x => x.Name + ":" + call.Arguments[x.Index]));
}
}

כעת נצטרך ליצור טיפוס ולהתקין עליו את הInterceptor הזה. זה נעשה באופן דומה למה שראינו בטיפ מספר 357:

1
2
3
4
5
6
Bank myBank = new Bank();
ProxyGenerator generator = new ProxyGenerator();
IBank intercepted = generator.CreateInterfaceProxyWithTargetInterface<IBank>(myBank, new LoggingInterceptor());
intercepted.GetAvailableBudget("My Identity"); // Logs!

הערות:

  • הביצועים של שימוש בטכניקה כזאת היא יותר מהירה בעשרות מונים משימוש בContextBoundObject שכבר ראינו אותו
  • אין חובה לעשות Interception לInterface, ניתן גם לטיפוס עצמו: כל עוד הוא לא Sealed, נוכל לעשות Intercept לכל המתודות הוירטואליות שלו.

סופ"ש עם ירושה בזמן ריצה טוב.

שתף

359. Implementing an interception framework – ContextBoundObject

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

הדרך הראשונה היא באמצעות משהו שנקרא ContextBoundObject. זה מאוד מזכיר את RealProxy (טיפ מספר 348).

שוב, תזכורת:

אנחנו מעוניינים באיזשהו ממשק של הFramework שאם נממש אותו נקבל יכולת Intercept לאובייקטים שלנו.

ContextBoundObject הוא אובייקט שיורש מMarshalByRefObject – נאמר שתי מילים על MarshalByRefObject:

ב.net קיימת אופציה לסרלז אובייקט: להפוך אותו לייצוג בינארי. כך אפשר להעביר אובייקטים בין השאר בין כמה AppDomainים. MarshalByRefObject הוא אובייקט שהCLR יודע לסרלז אותו באופן מיוחד: הוא מסרלז אותו בצורה כך שכאשר הוא עובר לAppDomain אחר, הוא מועבר לפי הReference שלו.

ContextBoudObject מאפשר יותר שליטה על האובייקט בCLR ובעצם מאפשר להתערב בקריאות של הפונקציות של האובייקט שלנו.

אז איך זה עובד?

זה קצת מסובך להשתמש בזה Out of the box, אבל ניתן לעשות זאת עם קצת (או קצת יותר) זיעה. להלן מימוש של LogInterceptor באמצעות ContextBoundObject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
[AttributeUsage(AttributeTargets.Class)]
public class LogAttribute : ContextAttribute
{
#region Constructor
public LogAttribute()
: base("Log")
{
}
#endregion
#region ContextAttribute Overrides
public override void Freeze(Context newContext)
{
}
public override void GetPropertiesForNewContext(System.Runtime.Remoting.Activation.IConstructionCallMessage ctorMsg)
{
ctorMsg.ContextProperties.Add(new LogProperty());
}
public override bool IsContextOK(Context ctx, System.Runtime.Remoting.Activation.IConstructionCallMessage ctorMsg)
{
LogProperty property =
ctx.GetProperty("Log") as LogProperty;
if (property == null)
{
return false;
}
return true;
}
public override bool IsNewContextOK(Context newCtx)
{
LogProperty property =
newCtx.GetProperty("Log") as LogProperty;
if (property == null)
{
return false;
}
return true;
}
#endregion
}
public class LogProperty : IContextProperty, IContributeObjectSink
{
#region IContextProperty Members
public string Name
{
get
{
return "Log";
}
}
public bool IsNewContextOK(Context newCtx)
{
LogProperty property =
newCtx.GetProperty("Log") as LogProperty;
if (property == null)
{
return false;
}
return true;
}
public void Freeze(Context newContext)
{
}
#endregion
#region IContributeObjectSink Members
public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink nextSink)
{
return new LogSink(nextSink);
}
#endregion
}
public class LogSink : IMessageSink
{
#region Data Members
private IMessageSink mNextSink;
private ILog mLog = LogManager.GetLogger("MyLogger");
#endregion
#region Constructor
public LogSink(IMessageSink nextSink)
{
this.mNextSink = nextSink;
}
#endregion
#region IMessageSink Members
public IMessage SyncProcessMessage(IMessage msg)
{
IMethodCallMessage call = msg as IMethodCallMessage;
mLog.DebugFormat("Entering {0}, with parameters {1}",
call.MethodBase.Name,
GetParameters(call));
IMethodReturnMessage result =
mNextSink.SyncProcessMessage(msg) as IMethodReturnMessage;
if (result.Exception != null)
{
mLog.Error("An error occured on " + call.MethodBase.Name,
result.Exception);
}
else
{
mLog.DebugFormat("Exiting {0} with return value: {1}",
result.MethodBase,
result.ReturnValue);
}
return result;
}
public IMessageSink NextSink
{
get
{
return this.mNextSink;
}
}
public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
{
IMessageCtrl rtnMsgCtrl = mNextSink.AsyncProcessMessage(msg, replySink);
return rtnMsgCtrl;
}
private string GetParameters(IMethodCallMessage call)
{
return string.Join
(",",
call.MethodBase.GetParameters().Select((x, i) =>
new { x.Name, Index = i }).
Select(x => x.Name + ":" + call.Args[x.Index]));
}
#endregion
}

תכלס זה קצת קשה להבין מה קורה כאן:

נתחיל בסוף: מה שבאמת חשוב כאן זאת הפונקציה SyncProcessMessage שמשנה את ההתנהגות לקריאה לפונקציה ומבצעת במקומה התנהגות משלנו.

(מזכירה את Route שתמיד ראינו כשדיברנו על Interception)

כל שאר הסיפור שיש כאן זה מה שצריך לעשות בFramework של ContextBoudObject כדי שתקרא הפונקציה SyncProcessMessage. (למען האמת, אני לא מכיר את זה לעומק, ויכול שיש פה טיפה יתירות)

זה רק הרקע שצריך לעשות כדי לכתוב את הInterceptor. כדי להשתמש בו, זה יותר קל:

נירש מContextBoundObject ונשים את הAttribute שלנו:

1
2
3
4
5
[Log]
public class Bank : ContextBoundObject, IBank
{
// ...
}

עכשיו כשנקרא לפונקציות של האובייקט שלנו, הן תעבורנה דרך הInterceptor 😀:

1
2
Bank myBank = new Bank();
myBank.GetAvailableBudget("My Identity"); // Logs!

כמה הערות:

מן הסתם אפשר לכתוב Framework שיקטין משמעותית את כמות הקוד שיש פה. בצוות שלי עשינו משהו, ואז כתיבת Interceptorים פשוט נראה כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// <summary>
/// Logs exceptions thrown out of the method call
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class LogExceptionsAttribute : AspectAttribute
{
private ILogger mLog;
private LogLevel mLevel = LogLevel.Error;
#region AspectAttribute Members
public override void PostProcess(ProcessingContext context)
{
if (context.ReturnMessage.Exception != null)
{
// Initialize the logger first time the method is called.
if (mLog == null)
{
mLog = Infrastructure.GetLayer<ILoggingLayer>().
GetLogger(context.ReturnMessage.MethodBase.DeclaringType.ToString());
}
mLog.Log(mLevel, context.ReturnMessage.Exception);
}
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the log level of the exceptions thrown out of the method
/// </summary>
public LogLevel Level
{
get
{
return mLevel;
}
set
{
mLevel = value;
}
}
#endregion
}

אותו שמים פשוט מעל מתודה שאנחנו רוצים שתכתוב לLog את הExceptionים.


הערה נוספת:

זאת דרך אפשרית לממש Interception. היתרון המובהק שלה שהיא יחסית Built in בFramework.

יש לה די הרבה חסרונות:

  • ContextBoundObject לא נחמד בדיבוג – נראה בWatch ובשאר חלונות הדיבוג הערה ש"מדובר בProxy ולכן אי אפשר לראות אותו בWatch".
  • ContextBoundObject כמו RealProxy וחבריו איטי יותר. אני לא אערוך פה השוואה, אבל אני מניח שמדובר באותם סדרי גודל, אם לא איטי יותר.

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

המשך יום עם Context חסום לטובה.

שתף

358. Implementing an interception framework - Aspect oriented programming

בהמשך לפעם הקודמת,

ראינו מה היינו מצפים בערך מFramework של Interception. כעת נדבר על Framework מעט יותר ספציפי, מה שנקרא גם הרבה פעמים “Aspect Oriented Programming” – מעתה ועד אילך (עד סוף הטיפ) יקרא AOP (עם כי לפעמים מתייחסים בAOP לרעיון הכללי של Interception ולאו דווקא למה שאציג כעת)

תזכורת:

יש לנו פעולות שאנחנו מעוניינים לעשות בפונקציות מסוימות שלנו, ביניהן:

  • כתיבה ללוג
  • Profiling
  • וואלידאציה
  • נראה בהמשך עוד

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

הפתרון: הקונספט של Interception המאפשר לנו להזריק התנהגויות אלה לפונקציות שלנו באיזושהי צורה, מבלי לפגוע בפונקציות עצמן.

אז מה זה AOP בכלל?

AOP היא פרדיגמת תכנות (כמו OOP, תכנות פרוצדורלי, תכנות פונקציונאלי, SOA וכו’) שמטרתה למנוע להגדיל את המודולריות של הקוד על ידי הפרדת ההתנהגות של הפונקציה מ”ההתעסקות במסביב”.

תכלס, מה שבד”כ זה אומר זה שאת “ההתעסקות במסביב” נוציא למחלקות נפרדות, שנקראות גם Aspectים או Interceptorים ואותן נרכיב על הפונקציות שלנו.

איך נרכיב אותן על הפונקציות שלנו? למשל בתור Attributeים!

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

1
2
3
4
5
6
[Log]
[Validate]
public void Deposit([NotNull]string id, [IsPositive]int amount)
{
// ...
}

עצם העובדה שקישטנו את הפונקציה שלנו עם הAspectים (הAttributeים) המתאימים גורמת לכך שהפונקציה שלנו משנה את התנהגותה ותבצע את הפעולות שאנחנו רוצים.

שימו לב שזה מתקשר איכשהו לטיפ הקודם: אם בטיפ הקודם הצגנו את הרעיון המרכזי של Interception Framework, שאנחנו רוצים שיהיה איזשהו ממשק שממנו ירשו Interceptorים ויוכלו להתלבש על פונקציות, כאן הAttributeים האלה יממשו את הממשק הזה.

בהמשך נדבר על הדרכים שאפשר לממש Interception framework, ונתייחס לAOP גם.

אחרי זה נחזור לראות עוד כמה דוגמאות על Interception.

ואחרי זה אין לי מושג.

המשך יום עם אספקטים מעולים.

שתף

357. Implementing an interception framework – Introduction

הכרנו בעבר את הרעיון של Interception, וראינו מספר דוגמאות שאפשר לממש באמצעות רעיון זה.

ישנן עוד מספר דוגמאות יפות שנראה בהמשך שניתן להשתמש בInterception כדי לממש אותן, אותן נראה בהמשך.

כעת נתחיל נדבר על בניית Framework לInterception.


נתחיל לדבר על מה אנחנו רוצים לעשות. אנחנו בעצם רוצים לכתוב Framework כך שיהיה לנו נוח לעשות Interception משלנו.

מה שאנחנו בעצם מצפים מFramework כזה זה שיהיה איזשהו ממשק שאנחנו נוכל לממש, וברגע שנממש אותו, נוכל “להתקין” אותו על איזשהו Container/טיפוס כדי שתקרא הפונקציה של הממשק במקום.

נקרא לצורך העניין לממשק שאנחנו מעוניינים לממש Interceptor והוא באופן לא מפתיע יראה כך:

1
2
3
4
public interface IInterceptor
{
void Intercept(MethodCallInfo call);
}

כעת כל הInterceptים שראינו עד כה יהיו Interceptorים אותם נתקין איכשהו:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class LoggingInterceptor : IInterceptor
{
public void Intercept(MethodCallInfo call)
{
Console.WriteLine("Entering {0}, with parameters {1}",
call.Method,
GetParameters(call));
try
{
call.Proceed();
Console.WriteLine("Exiting {0} with return value: {1}",
call.Method,
call.ReturnValue);
}
catch (Exception ex)
{
Console.WriteLine("An error occured on " + call.Method, ex);
throw;
}
}
private static string GetParameters(MethodCallInfo call)
{
return string.Join
(",",
call.Method.GetParameters().Select(x => x.Name + ":" + call.Arguments[x.Position]));
}
}

קיימים מספרים Frameworkים נחמדים המאפשרים Interception.

בהמשך נסקור Frameworkים אלה ונדבר על איך הם עובדים.

אחר כך נחזור לעוד דוגמאות לדברים שאפשר לממש עם Interception.

המשך יום מיורט לטובה!

שתף

356. Validation interception

נראה הפעם עוד שימוש לInterception.

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

למשל:

1
2
3
4
public int MyMethod(int id, string name)
{
// MyMethod Content
}

אופציה אחת היא להוסיף את הוואלידיציה למתודה עצמה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int MyMethod(int id, string name)
{
if (id < 0)
{
throw new ArgumentException("Argument must be positive", "id");
}
if (name == null)
{
throw new ArgumentNullException("name");
}
// MyMethod Content
}

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

כך שאם יש לנו 3 פרמטרים שאנחנו צריכים לבדוק בפונקציה, נצטרך להוסיף לה 9 שורות לפחות.

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

אז איך נפתור את זה בInterception?

פה אנחנו נכניס אלמנט שלא השתמשנו בו עד כה והוא הוספת Attributeים מעל פרמטרים של הפונקציה (ראו גם טיפ מספר 245)

יהיה לנו Attribute שיציין שאנחנו מעוניינים בוואלידציה ומהסוג שלה, ונשים אותו מעל פרמטרים.

להלן דוגמה לAttribute כזה:

1
2
3
4
5
[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
public abstract class ValidationAttribute : Attribute
{
public abstract bool IsValid(object obj);
}

והנה שני מימושים לדוגמה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class NotNullAttribute : ValidationAttribute
{
public override bool IsValid(object obj)
{
return (obj != null);
}
}
public class IsPositiveAttribute : ValidationAttribute
{
public override bool IsValid(object obj)
{
int? value = obj as int?;
return (value >= 0);
}
}

כעת הInterception שלנו יעשו משהו פשוט 😃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void Route(MethodCallInfo call)
{
var parameters =
call.Method.GetParameters().
Select((x, i) => new {Parameter = x, Index = i})
.Where(x => x.Parameter.GetCustomAttributes(true)
.OfType<ValidationAttribute>().Any());
foreach (var parameter in parameters)
{
ValidationAttribute attribute =
parameter.Parameter.GetCustomAttributes(true)
.OfType<ValidationAttribute>().First();
if (!attribute.IsValid(call.Arguments[parameter.Index]))
{
throw new ArgumentException("Argument wasn't valid",
parameter.Parameter.Name);
}
}
call.Proceed();
}

שימו לב שאנחנו תמיד שמים אותה הודעה בException, אבל אפשר לעשות שזה גם יגיע מהAttribute.

אפשר גם שהAttribute יקבל את השם של הפרמטר והוא יזרוק את הException.

כעת בהנחה ויש לנו Interception כזה, פשוט נשים Attributeים מתאימים:

1
2
3
4
public int MyMethod([IsPositive]int id, [NotNull]string name)
{
// MyMethod Content
}

מגניב ביותר!

בהמשך כשנראה דרכים יותר יפות לממש Interception, יהיה גם כיף להשתמש בדברים כאלה.

סופ"ש ואלידי טוב.

שתף

355. Profiling interception

בהמשך לפעמים הקודמות,

נראה כעת עוד שימוש לInterception.

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

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

אופציה שנייה היא לשנות את הקוד של הפונקציות ולמדוד את משך הקריאה (ראו גם טיפ מספר 298):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int MyMethod(int id, string name)
{
Stopwatch sw = new Stopwatch();
sw.Start();
// My Method content
sw.Stop();
mLog.DebugFormat("My method took {0} milli-seconds to run",
sw.ElapsedMilliseconds);
return result;
}

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

מה שנוכל לעשות במקום זה להשתמש בInterception כדי לעשות את זה בצורה הבאה:

נפנה את כל המתודות לRoute ונמדוד כמה זמן לקח להן לבצע את הקריאה מקורית.

אחר כך נכתוב זאת ללוג.

1
2
3
4
5
6
7
8
9
10
11
12
13
private void Route(MethodCallInfo call)
{
Stopwatch sw = new Stopwatch();
sw.Start();
call.Proceed();
sw.Stop();
mLog.DebugFormat("My method took {0} milli-seconds to run",
sw.ElapsedMilliseconds);
}

מגניב 😃

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

בהמשך נראה עוד שימושים יפים לInterception.

המשך שנמדד לטובה.

שתף

354. Logging interception

נתחיל כעת לראות שימושים נחמדים בInterception.

השימושים שנראה הם בעיקר על מנת לנקות את הקוד שלנו מההתעסקות ב”מסביב” ולהשאיר בו את הלוגיקה שלו. (נדבר על הקונספט הזה עוד בהמשך, מה שנקרא גם Aspect Oriented Programming)

השימוש הראשון שנראה ואולי המתבקש ביותר הוא שימוש בInterception בשביל Logging:

כמו כל מערכת טובה, אנחנו מעוניינים לכתוב לLog את הפעולות הבאות:

  1. כאשר מתבצעת כניסה לפונקציה
  2. כאשר מתבצעת יציאה מפונקציה עם ערך ההחזר
  3. כאשר עף Exception

אם היינו רוצים לעשות את זה על כל פונקציה, כל פונקציה שלנו הייתה נראית כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public int MyMethod(int id, string name)
{
mLog.DebugFormat("Entering MyMethod, with parameters id:{0}, name:{1}",
id, name);
try
{
int result;
// My Method content
mLog.DebugFormat("Exiting MyMethod with return value: {0}", result);
return result;
}
catch (Exception ex)
{
mLog.Error("An error occured on MyMethod", ex);
throw;
}
}

שימו לב שהמתודה שלנו התנפחה ב7 שורות בערך מבלי שבכלל כתבנו את הלוגיקה שלה.

Interception נותן לנו פתרון יותר אלגנטי:

ניצור מתודה שאליה יכנסו כל הקריאות שלנו:

1
2
3
4
public void Route(MethodCallInfo call)
{
call.Proceed();
}

ואז נשנה אותה כך שתבצע את הפעולות שדיברנו עליהן:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void Route(MethodCallInfo call)
{
mLog.DebugFormat("Entering {0}, with parameters {1}",
call.Method,
GetParameters(call));
try
{
call.Proceed();
mLog.DebugFormat("Exiting {0} with return value: {1}",
call.Method,
call.ReturnValue);
}
catch (Exception ex)
{
mLog.Error("An error occured on " + call.Method, ex);
throw;
}
}

מטורף! כעת רק צריך איכשהו "להתקין" את הInterception הזה על המחלקות שלנו וקיבלנו הרבה לוגים בחינם!

ראינו בעבר מספר דרכים לביצוע Intercept: שינוי קוד ועטיפת קוד בDesign Pattern של Decorator.

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

המשך יום עם לוגים טוב!

הערה: פונקציית עזר ששומשה בטיפ:

1
2
3
4
5
6
private static string GetParameters(MethodCallInfo call)
{
return string.Join
(",",
call.Method.GetParameters().Select(x => x.Name + ":" + call.Arguments[x.Position]));
}
שתף

353. Implementing interception using decoration

בפעמים הקודמות דיברנו קצת על הקונספט של Interception.

לא כל כך דיברנו על איך אפשר לממש דבר כזה – הראיתי שאפשר לממש Interception ע”י עריכת הקוד עצמו, אבל זה כמובן לא קביל מכמה סיבות:

  1. זה דורש לשנות קוד כדי להוסיף התנהגות
  2. צריך לשנות בכל המתודות ולא רק במתודה אחת

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

השיטה מבוססת על הDesign Pattern ששמו Decorator – (ראו גם טיפ מספר 85).

היא מניחה שאנחנו רוצים לעשות Intercept לממשק / מתודה וירטואלית.

בנוסף, היא מניחה שאנחנו יוצרים את הInstance דרך איזשהו Container/Factory.

נניח שאנחנו מעוניינים לעשות את הInterception שעשינו בפעמים הקודמות שכותב לConsole לפני ואחרי כניסה לפונקציה.

נוכל לעשות זאת בצורה הבאה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class BankInterception : IBank
{
private IBank mSource;
public BankInterception(IBank source)
{
mSource = source;
}
public void Deposit(string id, int amount)
{
WriteMethodCall(new MethodCallInfo("Deposit")
{
{"id", id},
{"amount", amount}
});
mSource.Deposit(id, amount);
WriteMethodReturn(new MethodCallInfo("Deposit"));
}
public void Withdraw(string id, int amount)
{
WriteMethodCall(new MethodCallInfo("Withdraw")
{
{"id", id},
{"amount", amount}
});
mSource.Withdraw(id, amount);
WriteMethodReturn(new MethodCallInfo("Withdraw"));
}
public int GetAvailableBudget(string id)
{
WriteMethodCall(new MethodCallInfo("GetAvailableBudget")
{
{"id", id},
});
int result = mSource.GetAvailableBudget(id);
WriteMethodReturn(new MethodCallInfo("GetAvailableBudget")
{
ReturnValue = result
});
return result;
}
private static void WriteMethodCall(MethodCallInfo methodCallInfo)
{
Console.WriteLine("Called {0} with arguments:");
foreach (KeyValuePair<string, object> argument in methodCallInfo)
{
Console.WriteLine("\t{0} : {1}",
argument.Key,
argument.Value);
}
}
private static void WriteMethodReturn(MethodCallInfo methodCallInfo)
{
Console.WriteLine("Returned from {0} with value {1}.",
methodCallInfo.Name,
methodCallInfo.ReturnValue);
}
}

כעת אם מישהו מעוניין לקבל את ההתנהגות הזאת, הוא יעשה זאת כך:

1
2
3
4
5
6
Bank myBank = new Bank();
IBank intercepted = new BankInterception(myBank);
intercepted.Deposit("6a91e9f9813a479db7c4bf549bb91c4d", 300);
// Prints to console the parameters

שימו לב שכפי שהערתי בפעם הקודמת, דרך יותר נכונה לעשות את זה היא משהו בסגנון הזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int GetAvailableBudget(string id)
{
MethodCallInfo methodCallInfo =
new MethodCallInfo
("GetAvailableBudget",
mSource,
message => mSource.GetAvailableBudget((string) message.Arguments[0]))
{
{"id", id},
};
Route(methodCallInfo);
return (int)methodCallInfo.ReturnValue;
}
private void Route(MethodCallInfo methodCallInfo)
{
WriteMethodCall(methodCallInfo);
methodCallInfo.Proceed();
WriteMethodReturn(methodCallInfo);
}

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

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

בהמשך נראה פתרון יותר גנרי (כמו שראינו במקרה של Proxyים)

סופ"ש מיורט לטובה!

שתף

352. Continuing with interception

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

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

בנוסף, לפונקציה האחרת יש אופציה לבצע את הקריאה המקורית שהייתה לפונקציה.

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

הפונקציה תקבל איזשהו אובייקט עם המידע הבא:

כל המידע שקיבלנו בProxy (מועתק מטיפ מספר 345):

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

בנוסף למידע הזה, נרצה לקבל את האובייקט שהופעלה עליו הפונקציה. (המתודה אליה ננותב, לאו דווקא תהיה שייכת לאובייקט, לכן לא נוכל להשתמש בthis)

בנוסף, היינו מצפים ליכולות הבאות שהיו לנו בProxy (מועתק מטיפ מספר 345):

  • לקבוע את ערך ההחזר של המתודה שהופעלה
  • לשנות את הפרמטרים שהם ref או out
  • לזרוק Exception ביציאה מהפונקציה

בנוסף ליכולות אלה, נרצה אופציה להפעיל את הקריאה של הפונקציה המקורית, עם אופציה לשנות את הפרמטרים.

הדבר מאוד דומה לProxy, רק שיש לנו אופציה להמשיך את הקריאה המקורית.


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

במקום זאת, היה אולי יותר טוב לכתוב כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public int GetAvailableBudget(string id)
{
MethodCallInfo callInfo =
new MethodCallInfo("GetAvailableBudget",
message => InnerGetAvailableBudget
((string) message.Arguments[0]))
{
{"id", id},
};
Route(callInfo);
return (int)callInfo.ReturnValue;
}
private int InnerGetAvailableBudget(string id)
{
// Implementation
}
private void Route(MethodCallInfo methodCallInfo)
{
Console.WriteLine("Called {0} with arguments:");
foreach (KeyValuePair<string, object> argument in methodCallInfo)
{
Console.WriteLine("\t{0} : {1}",
argument.Key,
argument.Value);
}
methodCallInfo.Proceed();
Console.WriteLine("Returned from {0} with value {1}.",
methodCallInfo.Name,
methodCallInfo.ReturnValue);
}

שימו לב:

הפונקציה קוראת לפונקציה Route עם הפרמטרים שקיבלה ומחזירה משם את ערך ההחזר.

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

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

שימו לב שזה מאוד דומה לProxy, רק שיש לנו אופציה להמשיך את הקריאה המקורית.

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

המשך יום מיורט לטובה.

שתף

351. Introducing Interception

המשך לסדרה על Proxies, נתחיל היום סדרה שמדברת על המושג של Interception.

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

כך אנחנו יכולים להנות מהיכולת לבצע דברים בצורה אוטומטית ע”י הפניה למתודה אחת שמבצעת עבורנו את העבודה.

המושג של Interception הוא מושג שמאוד קשור למושג של Proxy.

בגדול, Interception מאפשר לנו להוסיף או לשנות ההתנהגות של פונקציות שלנו.

למה הכוונה?

נניח, למשל, שכתבנו איזושהי מחלקה, למשל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Bank : IBank
{
public void Deposit(string id, int amount)
{
// Implementation of Deposit
}
public void Withdraw(string id, int amount)
{
// Implementation of Withdraw
}
public int GetAvailableBudget(string id)
{
// Implementation of GetAvailableBudget
}
}

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

למשל, כל פעם שאנחנו נכנסים או יוצאים מפונקציה כלשהי, אנחנו מעוניינים לכתוב לConsole.

אז נוכל לעשות זאת כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Bank : IBank
{
public void Deposit(string id, int amount)
{
WriteCall(new MethodCallInfo("Deposit")
{
{"id", id},
{"amount", amount}
});
// Implementation of Deposit
WriteCallReturn(new MethodReturnInfo("Deposit"));
}
public void Withdraw(string id, int amount)
{
WriteCall(new MethodCallInfo("Withdraw")
{
{"id", id},
{"amount", amount}
});
// Implementation of Withdraw
WriteCallReturn(new MethodReturnInfo("Withdraw"));
}
public int GetAvailableBudget(string id)
{
WriteCall(new MethodCallInfo("GetAvailableBudget")
{
{"id", id},
});
// Implementation of GetAvailableBudget
int result;
// End implementation of GetAvailableBudget
WriteCallReturn(new MethodReturnInfo("GetAvailableBudget") { ReturnValue = result });
}
private void WriteCall(MethodCallInfo methodCallInfo)
{
Console.WriteLine("Called {0} with arguments:");
foreach (KeyValuePair<string, object> argument in methodCallInfo)
{
Console.WriteLine("\t{0} : {1}",
argument.Key,
argument.Value);
}
}
private void WriteCallReturn(MethodReturnInfo methodReturnInfo)
{
Console.WriteLine("Returned from {0} with value {1}.",
methodReturnInfo.Name,
methodReturnInfo.ReturnValue);
}
}

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

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

שימו לב לדמיון בדוגמה שמופיעה כאן לProxy: בשתיהן אנחנו קוראים לפונקציה כלשהי קבועה בעת הקריאה לפונקציה.

אבל יש גם הבדל: בProxy היינו קוראים רק לפונקציה הקבועה. בInterception אנחנו יכולים גם לקרוא לפונקציה לא קבועה (מה שמופיע בהערה כImplementation).

בפעם הבאה ננסה לתאר טיפה יותר מה אנחנו רוצים להשיג בInterception, ובהמשך נראה שימושים (מדהימים!) של Interception ומימושים אפשריים בFramework.

המשך יום מיורט לטובה.

שתף