۱۳۹۰/۰۳/۰۴

آشنايي با Fluent interfaces


تعريف مقدماتي fluent interface در ويكي پديا به شرح زير است: (+)

In software engineering, a fluent interface (as first coined by Eric Evans and Martin Fowler) is a way of implementing an object oriented API in a way that aims to provide for more readable code.

به صورت خلاصه هدف آن فراهم آوردن روشي است كه بتوان متدها را زنجير وار فراخواني كرد و به اين ترتيب خوانايي كد نوشته شده را بالا برد. پياده سازي آن هم شامل دو نكته است:
الف) نوع متد تعريف شده بايد مساوي با نام كلاس جاري باشد.
ب) در اين حالت خروجي متد‌هاي ما كلمه كليدي this خواهند بود.

براي مثال:
using System;

namespace FluentInt
{
public class FluentApiTest
{
private int _val;

public FluentApiTest Number(int val)
{
_val = val;
return this;
}

public FluentApiTest Abs()
{
_val = Math.Abs(_val);
return this;
}

public bool IsEqualTo(int val)
{
return val == _val;
}
}
}
مثالي هم از استفاده‌ي آن به صورت زير مي‌تواند باشد:
if (new FluentApiTest().Number(-10).Abs().IsEqualTo(10))
{
Console.WriteLine("Abs(-10)==10");
}
كه در آن توانستيم تمام متدها را زنجير وار و با خوانايي خوبي شبيه به نوشتن جملات انگليسي در كنار هم قرار دهيم.
خوب! اين مطلبي است كه همه جا پيدا مي‌كنيد و مطلب جديدي هم نيست. اما موردي را كه سخت مي‌شود يافت اين است كه طراحي كلاس فوق ايراد دارد. براي مثال شما مي‌توانيد تركيب‌هاي زير را هم تشكيل دهيد و كار مي‌كند؛ يا به عبارتي برنامه كامپايل مي‌شود و اين خوب نيست:
if(new FluentApiTest().Abs().Number(-10).IsEqualTo(10)) ...
if (new FluentApiTest().Abs().IsEqualTo(10)) ...
مي‌شود در كدهاي برنامه يك سري throw new exception را هم قرار داد كه ... هي! اول بايد اون رو فراخواني كني بعد اين رو!
ولي ... اين روش هم صحيح نيست. از ابتداي كار نبايد بتوان متد بي‌ربطي را در طول اين زنجيره مشاهده كرد. اگر قرار نيست استفاده گردد، نبايد هم در intellisense ظاهر شود و پس از آن هم نبايد قابل كامپايل باشد.

بنابراين صورت مساله به اين ترتيب اصلاح مي‌شود:
مي‌خواهيم پس از نوشتن FluentApiTest و قرار دادن يك نقطه، در intellisense فقط Number ظاهر شود و نه هيچ متد ديگري. پس از ذكر متد Number فقط متد Abs يا مواردي شبيه به آن مانند Sqrt ظاهر شوند. پس از انتخاب مثلا Abs آنگاه متد IsEqualTo توسط Intellisense قابل دسترسي باشد. در روش اول فوق، به صورت دوستانه همه چيز در دسترس است و هر تركيب قابل كامپايلي را مي‌شود با متدها ساخت كه اين مورد نظر ما نيست.
اينبار پياده سازي اوليه به شرح زير تغيير خواهد كرد:
using System;

namespace FluentInt
{
public class FluentApiTest
{
public MathMethods<FluentApiTest> Number(int val)
{
return new MathMethods<FluentApiTest>(this, val);
}
}

public class MathMethods<TParent>
{
private int _val;
private readonly TParent _parent;

public MathMethods(TParent parent, int val)
{
_val = val;
_parent = parent;
}

public Restrictions<MathMethods<TParent>> Abs()
{
_val = Math.Abs(_val);
return new Restrictions<MathMethods<TParent>>(this, _val);
}
}

public class Restrictions<TParent>
{
private readonly int _val;
private readonly TParent _parent;

public Restrictions(TParent parent, int val)
{
_val = val;
_parent = parent;
}

public bool IsEqualTo(int val)
{
return _val == val;
}
}
}
در اينجا هم به همان كاربرد اوليه مي‌رسيم:
if (new FluentApiTest().Number(-10).Abs().IsEqualTo(10))
{
Console.WriteLine("Abs(-10)==10");
}
با اين تفاوت كه intellisense هربار فقط يك متد مرتبط در طول زنجيره را نمايش مي‌دهد و تمام متدها در همان ابتداي كار قابل انتخاب نيستند.
در پياده سازي كلاس MathMethods از Generics استفاده شده به اين جهت كه بتوان نوع متد Number را بر همين اساس تعيين كرد تا متدهاي كلاس MathMethods در Intellisense (يا به قولي در طول زنجيره مورد نظر) ظاهر شوند. كلاس Restrictions نيز به همين ترتيب معرفي شده است و از آن جهت تعريف نوع متد Abs استفاده كرديم. هر كلاس جديد در طول زنجيره، توسط سازنده خود به وهله‌اي از كلاس قبلي به همراه مقادير پاس شده دسترسي خواهد داشت. به اين ترتيب زنجيره‌اي را تشكيل داده‌ايم كه سازمان يافته است و نمي‌توان در آن متدي را بي‌جهت پيش يا پس از ديگري صدا زد و همچنين ديگر نيازي به بررسي نحوه‌ي فراخواني‌هاي يك مصرف كننده نيز نخواهد بود زيرا برنامه كامپايل نمي‌شود.