GoF: Decorator Pattern

ความตั้งใจ

Decorator Pattern คือรูปแบบที่ช่วยให้เราสามารถเพิ่มเติม state และ พฤติกรรมใหม่ เข้าไปใน object แบบ dynamic ได้ นั่นคือการที่เราสามารถเพิ่ม state และ พฤติกรรมใหม่ เช่นนี้เข้าไปได้ เราจึงไม่จำเป็นต้องกลับไปแก้ไข code method หรือ state ของ object เดิมเลย

ประยุกต์ใช้

เมื่อผมต้องการเพิ่ม logic บางอย่างเช่นบันทึก log หรือต้องการ monitor บาง method เข้าไปในขั้นตอน ก่อนดำเนินการ(preprocessing) หรือ หลังดำเนินการ(postprocessing) ตามปกติ สมมติตอนนี้ผมมี service interace เพื่อการคำนวณบวกเลขสองตัวง่ายๆเป็น method ปกติ หรือกล่าวว่าเป็น logic ของ domain หลักก็ได้ ดังนี้

public interface ICalculateService
{
int Add(int a, int b);
}

ผมสร้าง service CalculateService เพื่อ implement interface ข้างต้นได้เป็น

using System.Threading;

public class CalculateService : ICalculateService
{
public int Add(int a, int b)
{
Thread.Sleep(new TimeSpan(0, 0, 1));
int result = a + b;
Console.WriteLine(“{0}: Add({1}, {2}) = {3}.”, DateTime.Now, a, b, result);
return result;
}
}

จะสังเกตุเห็นว่า ผมได้ simulate ด้วยโดยการ sleep หรือทำ delay ไว้ 1 วินาทีใน method Add เพื่อให้ดูเหมือนว่าใช้เวลาทำงานบ้าง และให้ print ออกมาเพื่อแสดงถึงการเรียก method Add นี้แล้ว

มาถึงที่ด้าน client ของผมก็จะเขียน code ออกมาลักษณะนี้เวลาเรียกใช้งาน CalculateService

static void Main(string[] args)
{
ICalculateService calculateService = new CalculateService();
calculateService.Add(1, 10);
}

หลังจากนั้นผมก็ต้องการที่จะ เก็บ log ก่อน และหลังเรียก method Add ในทุกๆครั้งที่มีการเรียก method Add ผมก็เลยต้องกลับไปแก้ไข code จะได้เป็นแบบนี้

public class CalculateService : ICalculateService
{
public int Add(int a, int b)
{
Preprocessing();
Thread.Sleep(new TimeSpan(0, 0, 1));
int result = a + b;
Console.WriteLine(“{0}: Add({1}, {2}) = {3}.”, DateTime.Now, a, b, result);
Postprocessing();
return result;
}
}

คุณจะเห็นว่าผมจะต้องกลับไปแก้ไข code method Add ซึ่งยังไม่ใช่ solution ที่ดีที่สุด เนื่องจากผมต้องทำการ compile code ใหม่ทุกครั้งที่มีการแก้ไข method ที่เพิ่มเข้าไปนี้ ร่วมกับ code ของ method Add ด้วยซึ่ง method ใหม่ที่เพิ่มเข้าไป และ method Add มันไม่มีผลกระทบ หรือเกี่ยวข้องกันได้เลย(logic ลักษณะนี้จะเรียกว่า cross-cutting concern)และหากว่าผู้ใช้ หรือ client คนอื่นที่ไม่ต้องการ cross-cutting concern แบบที่ผมทำไว้ก่อน นั่นหมายความผมจะต้องกลับไปแก้ไข method Add ของผมเพื่อเตรียมสนับสนุน cross-cutting concern ของทุกๆ client ซึ่งนั่นเป็นงานที่โครตจะน่าเบื่อ ใช้แรงงาน และเสี่ยงต่อการเกิดปัญหาขึ้นง่ายๆ(error-prone)อย่างแน่นอน

และนี่คือ Decorator Pattern ที่จักเข้ามาช่วยเราแก้ปัญหาข้างต้น ผมจึง implement Decorator Pattern ได้ดังนี้

เริ่มต้นผมประกาศ decoration class ในแบบกลางๆเตรียมไว้ โดยรับ object ที่เป็น cross-cutting concern เป้าหมายที่ต้องการ และสามารถรับ parameters อื่นๆเข้ามาได้ด้วย ได้เป็น

public class Decoration
{
public Decoration(Action<object, object[]> action, object[] p)
{
_action = action;
paras = p;
}

private Action<object, object[]> _action;
private object[] paras;

public Action<object, object[]> Action
{
get { return _action; }
}

public object[] Parameters
{
get { return paras; }
}
}

ต่อมาผมประกาศ object proxy class เพื่อทำหน้าที่เป็น decorator งานของมันคือต่อขยายเพิ่มเติม cross-cutting concern ทีต้องการเข้าไปใน method หรือ state จาก object ใดๆในแบบ dynamic ได้โดยไม่กระทบ code method หรือ state ของ object นี้เลย

โดย object proxy class  ของผมนี้จะพัฒนาขยายต่อจาก System.Runtime.Remoting.Proxies.RealProxy และ implement interface System.Runtime.Remoting.IRemotingTypeInfo ซึ่งเป็นมารตฐานการสร้าง Dynamic Decorator ในแบบ .NET Framwork ดังนี้

using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Proxies;
using System.Runtime.Remoting.Messaging;

public class ObjectProxy : RealProxy, IRemotingTypeInfo
{
private object target;
private Decoration preAspect;
private Decoration postAspect;
private String[] arrMethods;

protected internal ObjectProxy(object target, String[] arrMethods,
Decoration preAspect, Decoration postAspect)
: base(typeof(MarshalByRefObject))
{
this.target = target;
this.preAspect = preAspect;
this.postAspect = postAspect;
this.arrMethods = arrMethods;
}

public override ObjRef CreateObjRef(System.Type type)
{
throw new NotSupportedException(“ObjRef for DynamicProxy isn’t supported”);
}

public bool CanCastTo(System.Type toType, object obj)
{
return true;
}

public string TypeName
{
get { throw new System.NotSupportedException(“TypeName for DynamicProxy isn’t supported”); }
set { throw new System.NotSupportedException(“TypeName for DynamicProxy isn’t supported”); }
}

public override IMessage Invoke(IMessage message)
{
object returnValue = null;
ReturnMessage returnMessage;

IMethodCallMessage methodMessage = (IMethodCallMessage)message;
MethodBase method = methodMessage.MethodBase;

// Perform the preprocessing
if (HasMethod(method.Name) && preAspect != null)
{
try
{
preAspect.Action(target, preAspect.Parameters);
}
catch (Exception e)
{
returnMessage = new ReturnMessage(e, methodMessage);
return returnMessage;
}
}

// Perform the call
try
{
returnValue = method.Invoke(target, methodMessage.Args);
}
catch (Exception ex)
{
if (ex.InnerException != null)
throw ex.InnerException;
else
throw ex;
}

// Perform the postprocessing
if (HasMethod(method.Name) && postAspect != null)
{
try
{
postAspect.Action(target, postAspect.Parameters);
}
catch (Exception e)
{
returnMessage = new ReturnMessage(e, methodMessage);
return returnMessage;
}
}

// Create the return message (ReturnMessage)
returnMessage = new ReturnMessage(returnValue, methodMessage.Args,
methodMessage.ArgCount, methodMessage.LogicalCallContext, methodMessage);
return returnMessage;
}

private bool HasMethod(String mtd)
{
foreach (string s in arrMethods)
{
if (s.Equals(mtd))
return true;
}

return false;
}
}

เพื่อความง่ายผมจะใช้ Factory Pattern สร้าง instance ObjectProxy Decorator ให้ดังนี้

public class ObjectProxyFactory
{
public static object CreateProxy(object target, String[] arrMethods,
Decoration preAspect, Decoration postAspect)
{
ObjectProxy dp = new ObjectProxy(target, arrMethods, preAspect, postAspect);
object o = dp.GetTransparentProxy();
return o;
}
}

เสร้จแล้วครับคราวนี้ผมก็จะกลับไปแก้ไข client เพื่อเรียกใช้ method Add โดยเพิ่ม cross-cutting logic เข้าไปด้วย โดยมันจะแค่แสดงข้อความให้เห็นการทำงานเฉยๆ ก่อน และ หลังจาก ดำเนินการปกติครับ ตามนี้

static void Main(string[] args)
{
ICalculateService calculateService = (ICalculateService)ObjectProxyFactory.CreateProxy(
new CalculateService(),
new String[] { “Add” },
new Decoration(Preprocessing, null),
new Decoration(Postprocessing, null));

calculateService.Add(1, 2);
}

public static void Preprocessing(object target, object[] parameters)
{
Console.WriteLine(“{0}: PreAction.”, DateTime.Now);
Thread.Sleep(new TimeSpan(0, 0, 1));
}

public static void Postprocessing(object target, object[] parameters)
{
Console.WriteLine(“{0}: PostAction.”, DateTime.Now);
}

ลอง run ครับมันจะแสดงข้อความแบบนี้

07/06/2011 14:45:43: PreAction.
07/06/2011 14:45:45: Add(1, 10) = 11.
07/06/2011 14:45:45: PostAction.

คราวนี้ผมเพิ่ม cross-cuting logic เข้าไปอีกคือแค่ console log แสดงข้อความออกมาเฉยๆ ปลับปลุง code ที่ client อีกเล็กน้อยได้ดังนี้

static void Main(string[] args)
{

ICalculateService calculateService = (ICalculateService)ObjectProxyFactory.CreateProxy(
new CalculateService(),
new String[] { “Add” },
new Decoration(Preprocessing, null),
new Decoration(Postprocessing, null));

var result = calculateService.Add(1, 10);

            ICalculateService calculateService2 = (ICalculateService)ObjectProxyFactory.CreateProxy(
                calculateService,
                new String[] { “Add” },
                null,
                new Decoration(PostLog, null));

            result = calculateService2.Add(1, 10);
}

public void PostLog(object target, object[] parameters)
{
Console.WriteLine(“{0}: PostLog.”, DateTime.Now);
}

คุณจะสังเกตุเห็นว่าผมสร้าง decorator calculateService2 โดยสร้างจาก calculateService เดิม แล้วทำการเพิ่มเติม method PostLog เข้าไปอีกเท่านั้นเอง ผลจากการ run code เพิ่มเติมใหม่คือ

07/06/2011 14:54:59: PreAction.
07/06/2011 14:55:01: Add(1, 10) = 11.
07/06/2011 14:55:01: PostAction.
07/06/2011 14:55:01: PreAction.
07/06/2011 14:55:03: Add(1, 10) = 11.
07/06/2011 14:55:03: PostAction.
07/06/2011 14:55:03: PostLog.

เสร็จแล้วครับกับ Decorator Pattern  เป็น 1 ใน 23 pattern ของ GoF มันจะอยู่ในหมวดของ Structural Patterns ตัวอย่างของ Decorator Pattern เช่น Aspect-Oriented Programming(AOP) นั่นเอง

คำเตือน: หากต้องการทำ AOP ไม่แนะนำให้เขียนเอง หรือนำ code จากบทความนี้ไปใช้ เพราะมันไม่ได้ผ่านการทดสอบอีกหลายกรณี ขอแนะนำให้ใช้ Spring.NET Framework

ขอบคุณครับ

#:P

Advertisements

#design-patterns, #gof