۱۳۸۸/۰۵/۲۸

آشنايي با الگوي MVP


پروژه‌هاي زيادي را مي‌توان يافت كه اگر سورس كدهاي آن‌ها را بررسي كنيم، يك اسپاگتي كد تمام عيار را در آن‌ها مي‌توان مشاهده نمود. منطق برنامه، قسمت دسترسي به داده‌ها، كار با رابط كاربر، غيره و غيره همگي درون كدهاي يك يا چند فرم خلاصه شده‌اند و آنچنان به هم گره خورده‌اند كه هر گونه تغيير يا اعمال درخواست‌هاي جديد كاربران، سبب از كار افتادن قسمت ديگري از برنامه مي‌شود.
همچنين از كدهاي حاصل در يك پروژه، در پروژه‌‌هاي ديگر نيز نمي‌توان استفاده كرد (به دليل همين در هم تنيده بودن قسمت‌هاي مختلف). حداقل نتيجه يك پروژه براي برنامه نويس، بايد يك يا چند كلاس باشد كه بتوان از آن به عنوان ابزار تسريع انجام پروژه‌هاي ديگر استفاده كرد. اما در يك اسپاگتي كد، بايد مدتي طولاني را صرف كرد تا بتوان يك متد را از لابلاي قسمت‌هاي مرتبط و گره خورده با رابط كاربر استخراج و در پروژه‌اي ديگر استفاده نمود. براي نمونه آيا مي‌توان اين كدها را از يك برنامه ويندوزي استخراج كرد و آن‌ها را در يك برنامه تحت وب استفاده نمود؟

يكي از الگوهايي كه شيوه‌ي صحيح اين جدا سازي را ترويج مي‌كند، الگوي MVP يا Model-View-Presenter مي‌باشد. خلاصه‌ي اين الگو به صورت زير است:


Model :
من مي‌دانم كه چگونه اشياء برنامه را جهت حصول منطقي خاص، پردازش كنم.
من نمي‌دانم كه چگونه بايد اطلاعاتي را به شكلي بصري به كاربر ارائه داد يا چگونه بايد به رخ‌دادها يا اعمال صادر شده از طرف كاربر پاسخ داد.

View :
من مي‌دانم كه چگونه بايد اطلاعاتي را به كاربر به شكلي بصري ارائه داد.
من مي‌دانم كه چگونه بايد اعمالي مانند data binding و امثال آن را انجام داد.
من نمي‌دانم كه چگونه بايد منطق پردازشي موارد ذكر شده را فراهم آورم.

Presenter :
من مي‌دانم كه چگونه بايد درخواست‌هاي رسيده كاربر به View را دريافت كرده و آن‌ها را به Model‌ انتقال دهم.
من مي‌دانم كه چگونه بايد اطلاعات را به Model ارسال كرده و سپس نتيجه‌ي پردازش آن‌ها را جهت نمايش در اختيار View قرار دهم.
من نمي‌دانم كه چگونه بايد اطلاعاتي را ترسيم كرد (مشكل View است نه من) و نمي‌دانم كه چگونه بايد پردازشي را بر روي اطلاعات انجام دهم. (مشكل Model است و اصلا ربطي به اينجانب ندارد!)


يك مثال ساده از پياده سازي اين روش
برنامه‌اي وبي را بنويسيد كه پس از دريافت شعاع يك دايره از كاربر، مساحت ‌آن‌را محاسبه كرده و نمايش دهد.
يك تكست باكس در صفحه قرار خواهيم داد (txtRadius) و يك دكمه جهت دريافت درخواست كاربر براي نمايش نتيجه حاصل در يك برچسب به نام lblResult

الف) پياده سازي به روش متداول (اسپاگتي كد)

protected void btnGetData_Click(object sender, EventArgs e)
{
lblResult.Text = (Math.PI * double.Parse(txtRadius.Text) * double.Parse(txtRadius.Text)).ToString();
}
بله! كار مي‌كنه!
اما اين مشكلات را هم دارد:
- منطق برنامه (روش محاسبه مساحت دايره) با رابط كاربر گره خورده.
- كدهاي برنامه در پروژه‌ي ديگري قابل استفاده نيست. (شما متد يا كلاسي را اين‌جا با قابليت استفاده مجدد مي‌توانيد پيدا مي‌كنيد؟ آيا يكي از اهداف برنامه نويسي شيءگرا توليد كدهايي با قابليت استفاده مجدد نبود؟)
- چگونه بايد براي آن آزمون واحد نوشت؟

ب) بهبود كد و جدا سازي لايه‌ها از يكديگر

در روش MVP متداول است كه به ازاي هر يك از اجزاء ابتدا يك interface نوشته شود و سپس اين اينترفيس‌ها پياده سازي گردد.

پياده سازي منطق برنامه:

1- ايجاد Model :
يك فايل جديد را به نام CModel.cs به پروژه اضافه كرده و كد زير را به آن خواهيم افزود:

using System;

namespace MVPTest
{
public interface ICircleModel
{
double GetArea(double radius);
}

public class CModel : ICircleModel
{
public double GetArea(double radius)
{
return Math.PI * radius * radius;
}
}
}
همانطور كه ملاحظه مي‌كنيد اكنون منطق برنامه از موارد زير اطلاعي ندارد:
- خبري از textbox و برچسب و غيره نيست. اصلا نمي‌داند كه رابط كاربري وجود دارد يا نه.
- خبري از رخ‌دادهاي برنامه و پاسخ دادن به آن‌ها نيست.
- از اين كد مي‌توان مستقيما و بدون هيچ تغييري در برنامه‌هاي ديگر هم استفاده كرد.
- اگر باگي در اين قسمت وجود دارد، تنها اين كلاس است كه بايد تغيير كند و بلافاصله كل برنامه از اين بهبود حاصل شده مي‌تواند بدون هيچگونه تغييري و يا به هم ريختگي استفاده كند.
- نوشتن آزمون واحد براي اين كلاس كه هيچگونه وابستگي به UI ندارد ساده است.


2- ايجاد View :
فايل ديگري را به نام CView.cs را به همراه اينترفيس زير به پروژه اضافه مي‌كنيم:

namespace MVPTest
{
public interface IView
{
string RadiusText { get; set; }
string ResultText { get; set; }
}
}

كار View دريافت ابتدايي مقادير از كاربر توسط RadiusText و نمايش نهايي نتيجه توسط ResultText است البته با يك اما.
View نمي‌داند كه چگونه بايد اين پردازش صورت گيرد. حتي نمي‌داند كه چگونه بايد اين مقادير را به Model جهت پردازش برساند يا چگونه آن‌ها را دريافت كند (به همين جهت از اينترفيس براي تعريف آن استفاده شده).

3- ايجاد Presenter :
در ادامه فايل جديدي را به نام CPresenter.cs‌ با محتويات زير به پروژه خواهيم افزود:

namespace MVPTest
{
public class CPresenter
{
IView _view;

public CPresenter(IView view)
{
_view = view;
}

public void CalculateCircleArea()
{
CModel model = new CModel();
_view.ResultText = model.GetArea(double.Parse(_view.RadiusText)).ToString();
}
}
}

كار اين كلاس برقراري ارتباط با Model است.
مي‌داند كه چگونه اطلاعات را به Model ارسال كند (از طريق _view.RadiusText) و مي‌داند كه چگونه نتيجه‌ي پردازش را در اختيار View قرار دهد. (با انتساب آن به _view.ResultText)
نمي‌داند كه چگونه بايد اين پردازش صورت گيرد (كار مدل است نه او). نمي‌داند كه نتيجه‌ي نهايي را چگونه نمايش دهد (كار View است نه او).
روش معرفي View به اين كلاس به constructor dependency injection معروف است.

اكنون كد وب فرم ما كه در قسمت (الف) معرفي شده به صورت زير تغيير مي‌كند:

using System;

namespace MVPTest
{
public partial class _Default : System.Web.UI.Page, IView
{
protected void Page_Load(object sender, EventArgs e)
{
}

public string RadiusText
{
get { return txtRadius.Text; }
set { txtRadius.Text = value; }
}
public string ResultText
{
get { return lblResult.Text; }
set { lblResult.Text = value; }
}

protected void btnGetData_Click(object sender, EventArgs e)
{
CPresenter presenter = new CPresenter(this);
presenter.CalculateCircleArea();
}
}
}

در اين‌جا يك وهله از Presenter براي برقراري ارتباط با Model ايجاد مي‌شود. همچنين كلاس وب فرم ما اينترفيس View را نيز پياده سازي خواهد كرد.