‏نمایش پست‌ها با برچسب Refactoring. نمایش همه پست‌ها
‏نمایش پست‌ها با برچسب Refactoring. نمایش همه پست‌ها

۱۳۹۰/۱۰/۲۱

مروري بر كدهاي كلاس SqlHelper


قسمتي از يك پروژه به همراه كلاس SqlHelper آن در كامنت‌هاي مطلب «اهميت Code review» توسط يكي از خوانندگان بلاگ جهت Code review مطرح شده كه بهتر است در يك مطلب جديد و مجزا به آن پرداخته شود. قسمت مهم آن كلاس SqlHelper است و مابقي در اينجا نديد گرفته مي‌شوند:

//It's only for code review purpose!  
using System.Data;
using System.Data.SqlClient;
using System.Web.Configuration;


public sealed class SqlHelper
{
    private SqlHelper() { }

    
    //  Send Connection String
    //---------------------------------------------------------------------------------------
    public static string GetCntString()
    {
        return WebConfigurationManager.ConnectionStrings["db_ConnectionString"].ConnectionString;
    }

   
    //  Connect to Data Base SqlServer
    //---------------------------------------------------------------------------------------
    public static SqlConnection Connect2Db(ref SqlConnection sqlCnt, string cntString)
    {
        try
        {
            if (sqlCnt == null) sqlCnt = new SqlConnection();
            sqlCnt.ConnectionString = cntString;
            if (sqlCnt.State != ConnectionState.Open) sqlCnt.Open();
            return sqlCnt;
        }
        catch (SqlException)
        {
            return null;
        }
    }

  
    //  Run ExecuteScalar Command
    //---------------------------------------------------------------------------------------
    public static string RunExecuteScalarCmd(ref SqlConnection sqlCnt, string strCmd, bool blnClose)
    {
        Connect2Db(ref sqlCnt, GetCntString());
        using (sqlCnt)
        {
            using(SqlCommand sqlCmd = sqlCnt.CreateCommand())
            {
                sqlCmd.CommandText = strCmd;
                object objResult = sqlCmd.ExecuteScalar();
                if (blnClose) CloseCnt(ref sqlCnt, true);
                return (objResult == null) ? string.Empty : objResult.ToString();
            }
        }
    }

    //   Close SqlServer Connection
    //---------------------------------------------------------------------------------------
    public static bool CloseCnt(ref SqlConnection sqlCnt, bool nullSqlCnt)
    {
        try
        {
            if (sqlCnt == null) return true;
            if (sqlCnt.State == ConnectionState.Open)
            {
                sqlCnt.Close();
                sqlCnt.Dispose();
            }
            if (nullSqlCnt) sqlCnt = null;
            return true;
        }
        catch (SqlException)
        {
            return false;
        }
    }   
}


مثالي از نحوه استفاده ارائه شده:

protected void BtnTest_Click(object sender, EventArgs e)
        {
            SqlConnection sqlCnt = new SqlConnection();
            string strQuery = "SELECT COUNT(UnitPrice) AS PriceCount FROM [Order Details]";


            // در این مرحله پارامتر سوم یعنی کانکشن باز نگه داشته شود
            string strResult = SqlHelper.RunExecuteScalarCmd(ref sqlCnt, strQuery, false);



            strQuery = "SELECT LastName + N'-' + FirstName AS FullName FROM Employees WHERE (EmployeeID = 9)";
            // در این مرحله پارامتر سوم یعنی کانکشن بسته شود
            strResult = SqlHelper.RunExecuteScalarCmd(ref sqlCnt, strQuery, true);
        }


مروري بر اين كد:

1) نحوه كامنت نوشتن
بين سي شارپ و زبان سي++ تفاوت وجود دارد. اين نحوه كامنت نويسي بيشتر در سي++ متداول است. اگر از ويژوال استوديو استفاده مي‌كنيد، مكان نما را به سطر قبل از يك متد منتقل كرده و سه بار پشت سر هم forward slash را تايپ كنيد. به صورت خودكار ساختار خالي زير تشكيل خواهد شد:
/// <summary>
    /// 
    /// </summary>
    /// <param name="sqlCnt"></param>
    /// <param name="cntString"></param>
    /// <returns></returns>
    public static SqlConnection Connect2Db(ref SqlConnection sqlCnt, string cntString)

اين روش مرسوم كامنت نويسي كدهاي سي شارپ است. خصوصا اينكه ابزارهايي وجود دارند كه به صورت خودكار از اين نوع كامنت‌ها، فايل CHM‌ درست مي‌كنند.

2) وجود سازنده private
احتمالا هدف اين بوده كه نه شخصي و نه حتي كامپايلر، وهله‌اي از اين كلاس را ايجاد نكند. بنابراين بهتر است كلاسي را كه تمام متدهاي آن static است (كه به اين هم خواهيم رسيد!) ، راسا static معرفي كنيد. به اين ترتيب نيازي به سازنده private نخواهد بود.

3) وجود try/catch
يك اصل كلي وجود دارد: اگر در حال طراحي يك كتابخانه پايه‌اي هستيد، try/catch را در هيچ متدي از آن لحاظ نكنيد. بله؛ درست خونديد! لطفا try/catch ننويسيد! كرش كردن برنامه خوب است! لا‌يه‌هاي بالاتر برنامه كه در حال استفاده از كدهاي شما هستند متوجه خواهند شد كه مشكلي رخ داده و اين مشكل توسط كتابخانه مورد استفاده «خفه» نشده. براي مثال اگر هم اكنون SQL Server در دسترس نيست، لايه‌هاي بالاتر برنامه بايد اين مشكل را متوجه شوند. Exception اصلا چيز بدي نيست! كرش برنامه اصلا بد نيست!
فرض كنيد كه دچار بيماري شده‌ايد. اگر مثلا تبي رخ ندهد، از كجا بايد متوجه شد كه نياز به مراقبت پزشكي وجود دارد؟ اگر هيچ علامتي بروز داده نشود كه تا الان نسل بشر منقرض شده بود!

4) وجود ref و out
دوستان گرامي! اين ref و out فقط جهت سازگاري با زبان C در سي شارپ وجود دارد. لطفا تا حد ممكن از آن استفاده نكنيد! مثلا استفاده از توابع API‌ ويندوز كه با C نوشته شده‌اند.
يكي از مهم‌ترين كاربردهاي pointers در زبان سي، دريافت بيش از يك خروجي از يك تابع است. براي مثال يك متد API ويندوز را فراخواني مي‌كنيد؛ خروجي آن يك ساختار است كه به كمك pointers به عنوان يكي از پارامترهاي همان متد معرفي شده. اين روش به وفور در طراحي ويندوز بكار رفته. ولي خوب در سي شارپ كه از اين نوع مشكلات وجود ندارد. يك كلاس ساده را طراحي كنيد كه چندين خاصيت دارد. هر كدام از اين خاصيت‌ها مي‌توانند نمايانگر يك خروجي باشند. خروجي متد را از نوع اين كلاس تعريف كنيد. يا براي مثال در دات نت 4، امكان ديگري به نام Tuples معرفي شده براي كساني كه سريع مي‌خواهند چند خروجي از يك تابع دريافت كنند و نمي‌خواهند براي اينكار يك كلاس بنويسند.
ضمن اينكه براي مثال در متد Connect2Db، هم كانكشن يكبار به صورت ref معرفي شده و يكبار به صورت خروجي متد. اصلا نيازي به استفاده از ref در اينجا نبوده. حتي نيازي به خروجي كانكشن هم در اين متد وجود نداشته. كليه تغييرات شما در شيء كانكشني كه به عنوان پارامتر ارسال شده، در خارج از آن متد هم منعكس مي‌شود (شبيه به همان بحث pointers در زبان سي). بنابراين وجود ref غيرضروري است؛ وجود خروجي متد هم به همين صورت.

5) استفاده از using در متد RunExecuteScalarCmd
استفاده از using خيلي خوب است؛ هميشه اينكار را انجام دهيد!
اما اگر اينكار را انجام داديد، بدانيد كه شيء sqlCnt در پايان بدنه using ، توسط GC نابوده شده است. بنابراين اينجا bool blnClose ديگر چه كاربردي دارد؟! تصميم شما ديگر اهميتي نخواهد داشت؛ چون كار تخريبي پيشتر انجام شده.

6) متد CloseCnt
اين متد زايد است؛ به دليلي كه در قسمت (5) عنوان شد. using هاي استفاده شده، كار را تمام كرده‌اند. بنابراين بستن اشياء dispose شده معنا نخواهد داشت.

7) در مورد نحوه استفاده
اگر SqlHelper را در اينجا مثلا يك DAL ساده فرض كنيم (data access layer)، جاي قسمت BLL (business logic layer) در اينجا خالي است. عموما هم چون توضيحات اين موارد را خيلي بد ارائه داده‌اند، افراد از شنيدن اسم آن‌ها هم وحشت مي‌كنند. BLL يعني كمي دست به Refactoring بزنيد و اين پياده سازي منطق تجاري ارائه شده در متد BtnTest_Click را به يك كلاس مجزا خارج از code behind پروژه منتقل كنيد. Code behind فقط محل استفاده نهايي از آن باشد. همين! فعلا با همين مختصر شروع كنيد.
مورد ديگري كه در اينجا باز هم مشهود است، عدم استفاده از پارامتر در كوئري‌ها است. چون از پارامتر استفاده نكرده‌ايد، SQL Server مجبور است براي حالت EmployeeID = 9 يكبار execution plan را محاسبه كند، براي كوئري بعدي مثلا EmployeeID = 19، اينكار را تكرار كند و الي آخر. اين يعني مصرف حافظه بالا و همچنين سرعت پايين انجام كوئري‌ها. بنابراين اينقدر در قيد و بند باز نگه داشتن يك كانكشن نباشيد؛ مشكل اصلي جاي ديگري است!

8) برنامه وب و اطلاعات استاتيك!
اين پروژه، يك پروژه ASP.NET است. ديدن تعاريف استاتيك در اين نوع پروژه‌ها يك علامت خطر است! در اين مورد قبلا مطلب نوشتم:
متغيرهاي استاتيك و برنامه‌هاي ASP.NET


يك درخواست عمومي!
لطف كنيد در پروژ‌هاي «جديد» خودتون اين نوع كلاس‌هاي SqlHelper رو «دور بريزيد». ياد گرفتن كار با يك ORM جديد اصلا سخت نيست. مثلا طراحي Entity framework مايكروسافت به حدي ساده است كه هر شخصي با داشتن بهره هوشي در حد يك عنكبوت آبي يا حتي جلبك دريايي هم مي‌تونه با اون كار كنه! فقط NHibernate هست كه كمي مرد افكن است و گرنه مابقي به عمد ساده طراحي شده‌اند.
مزاياي كار كردن با ORM ها اين است:
- كوئري‌هاي حاصل از آن‌ها «پارامتري» است؛ كه اين دو مزيت عمده را به همراه دارد:
امنيت: مقاومت در برابر SQL Injection
سرعت و همچنين مصرف حافظه كمتر: با كوئري‌هاي پارامتري در SQL Server همانند رويه‌هاي ذخيره شده رفتار مي‌شود.
- عدم نياز به نوشتن DAL شخصي پر از باگ. چون ORM يعني همان DAL كه توسط يك سري حرفه‌اي طراحي شده.
- يك دست شدن كدها در يك تيم. چون همه بر اساس يك اينترفيس مشخص كار خواهند كرد.
- امكان استفاده از امكانات جديد زبان‌هاي دات نتي مانند LINQ و نوشتن كوئري‌هاي strongly typed تحت كنترل كامپايلر.
- پايين آوردن هزينه‌هاي آموزشي افراد در يك تيم. مثلا EF را مي‌شود به عنوان يك پيشنياز در نظر گرفت؛ عمومي است و همه گير. كسي هم از شنيدن نام آن تعجب نخواهد كرد. كتاب(هاي) آموزشي هم در مورد آن زياد هست.
و ...


۱۳۹۰/۱۰/۱۶

اهميت code review


تا جايي كه دقت كردم (در بلاگ‌هايي كه منتشر مي‌شوند) در آنسوي آب‌ها، «code review» يك شغل محسوب مي‌شود. سازمان‌ها، شركت‌ها و امثال آن از مشاورين يا برنامه نويس‌هايي با مطالعه بيشتر دعوت مي‌كنند تا از كدهاي آن‌ها اشكال‌گيري كنند و بابت اينكار هم هزينه مي‌كنند.
اگر علاقمند باشيد قسمتي از يك پروژه سورس باز دريافت شده از همين دور و اطراف را با هم مرور كنيم:

//It's only for code review purpose!
protected void Button1_Click1(object sender, EventArgs e)
{        
        string  strcon;        
        string strUserURL;
        string strSQL;
        string strSQL1;
        strSQL = "SELECT UserLevel FROM listuser " + "WHERE Username='" + TextBox2.Text + "' " + "And Password='" + TextBox3.Text + "';";
        strSQL1 = "SELECT Pnumber FROM listuser " + "WHERE Username='" + TextBox2.Text + "' " + "And Password='" + TextBox3.Text + "';";
        strcon = @"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\bimaran.mdf;Integrated Security=True;User Instance=True";
        SqlConnection myConnection = new SqlConnection(strcon);

        SqlCommand myCommand = new SqlCommand(strSQL, myConnection);
        SqlCommand myCommand1 = new SqlCommand(strSQL1, myConnection);
        myConnection.Open();

        strUserURL = (string)myCommand.ExecuteScalar();
        send = (string)myCommand1.ExecuteScalar();
        myCommand.Dispose();
        myCommand1.Dispose();
        myConnection.Close();


        if (strUserURL != null)
        {                      
            Label1.Text = "";

            url = "?Pn=" + code(send);
            FormsAuthentication.SetAuthCookie(TextBox2.Text, true);
            Response.Redirect("Page/" + strUserURL + url);
       }
        else
            Label3.Text = "چنین کاربری با این مشخصات ثبت نشده است.";
}


مروري بر اين كد يا «مشكلات اين كد»:
- كانكشن استرينگ داخل كدها تعريف شده. يعني اگر نياز به تغييري در آن بود بايد كدهاي برنامه تغيير كنند. آن هم نه فقط در اين تابع بلكه در كل برنامه.
- از پارامتر استفاده نشده. كد 100 درصد به تزريق اس كيوال آسيب پذير است.
- نحوه‌ي dispose شيء كانكشن غلط است. هيچ ضمانتي وجود ندارد كه كدهاي فوق سطر به سطر اجرا شود و خيلي زيبا به سطر بستن كانكشن استرينگ برسد. فقط كافي است اين ميان يك استثنايي صادر شود و تمام. به عبارتي اين سايت فقط با كمتر از 30 كاربر همزمان از كار مي‌افته. بعد نيايد بگيد من يك سرور دارم با 16 گيگ رم ولي باز كم مياره! همش برنامه كند ميشه. همش سايت بالا نمياد!
- همين تعريف كردن متغيرها در ابتداي تابع يعني اين برنامه نويس هنوز حال و هواي ANSI C را دارد!
- مهم نيست لايه بندي كنيد. ولي يك لايه در اين نوع پروژه‌ها الزامي است و آن هم DAL نام دارد. DAL يعني كثافت كاري نكنيد. يعني داخل هر تابع كُپه كُپه بر نداريد open و close بذاريد. بريد يك تابع يك گوشه‌اي درست كنيد كه اين عمليات را محصور كند.
- همين وجود Button1 و Label1 يعني تو خود شرح مفصل بخوان از اين مجمل!

۱۳۹۰/۰۹/۰۵

آشنايي با Refactoring - قسمت 14


در بسياري از زبان‌هاي برنامه نويسي امكان null بودن Reference types وجود دارد. به همين جهت مرسوم است پيش از استفاده از آن‌ها، بررسي شود آيا شيء مورد استفاده نال است يا خير و سپس براي مثال متد يا خاصيت مرتبط با آن فراخواني گردد؛ در غير اينصورت برنامه با يك استثناء خاتمه خواهد يافت.
مشكلي هم كه با اين نوع بررسي‌ها وجود دارد اين است كه پس از مدتي كد موجود را تبديل به مخزني از انبوهي از if و else ها خواهند كرد كه هم درجه‌ي پيچيدگي متدها را افزايش مي‌دهند و هم نگهداري ‌آن‌ها را در طول زمان مشكل مي‌سازند. براي حل اين مساله، الگوي استانداردي وجود دارد به نام null object pattern؛ به اين معنا كه بجاي بازگشت دادن null و يا سبب بروز يك exception شدن، بهتر است واقعا مطابق شرايط آن متد يا خاصيت، «هيچ‌كاري» را انجام نداد. در ادامه، توضيحاتي در مورد نحوه‌ي پياده سازي اين «هيچ‌كاري» را انجام ندادن، ارائه خواهد شد.


الف) حين معرفي خاصيت‌ها از محافظ استفاده كنيد.

براي مثال اگر قرار است خاصيتي به نام Name را تعريف كنيد كه از نوع رشته‌ است، حالت امن آن رشته بجاي null بودن، «خالي» بودن است. به اين ترتيب مصرف كننده مدام نگران اين نخواهد بود كه آيا الان اين Name نال است يا خير. مدام نياز نخواهد داشت تا if و else بنويسد تا اين مساله را چك كند. نحوه پياده سازي آن هم ساده است و در ادامه بيان شده است:

private string name = string.Empty;
public string Name
{
    get { return this.name; }
    set 
    {
        if (value == null)
        {
            this.name = "";
            return;
        }
        this.name = value; 
    }
}

دقيقا در زمان انتساب مقداري به اين خاصيت، بررسي مي‌شود كه آيا مثلا null است يا خير. اگر بود، همينجا و نه در كل برنامه، مقدار آن «خالي» قرار داده مي‌شود.

ب) سعي كنيد در متدها تا حد امكان null بازگشت ندهيد.

براي نمونه اگر متدي قرار است ليستي را بازگشت دهد:

public IList<string> GetCultures()
{
    //...
}

و حين تهيه اين ليست، عضوي مطابق منطق پياده سازي آن يافت نشد، null را بازگشت ندهيد؛ يك new List خالي را بازگشت دهيد. به اين ترتيب مصرف كننده ديگري نيازي به بررسي نال بودن خروجي اين متد نخواهد داشت.


ج) از متدهاي الحاقي بجاي if و else استفاده كنيد.

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

public static class LanguageExtender
{
        public static string GetSafeStringValue(this XElement input)
        {
            return (input == null) ? string.Empty : input.Value;
        }

        public static DateTime GetSafeDateValue(this XElement input)
        {
            return (input == null) ? DateTime.MinValue : DateTime.Parse(input.Value);
        }
}

به اين ترتيب مي‌توان امكانات كلاس پايه‌‌اي را بدون نياز به دسترسي به كدهاي اصلي آن مطابق نياز‌هاي خود تغيير و توسعه داد.


۱۳۹۰/۰۸/۱۹

آشنايي با Refactoring - قسمت 13


يكي از مواردي كه حين كار كردن با iTextSharp واقعا اعصاب خردكن است، طراحي نامناسب ثوابت اين كتابخانه مي‌باشد. براي مثال:


public class PdfWriter
{
        /** A viewer preference */
        public const int PageLayoutSinglePage = 1;
        /** A viewer preference */
        public const int PageLayoutOneColumn = 2;
        /** A viewer preference */
        public const int PageLayoutTwoColumnLeft = 4;
        /** A viewer preference */
        public const int PageLayoutTwoColumnRight = 8;
        /** A viewer preference */
        public const int PageLayoutTwoPageLeft = 16;
        /** A viewer preference */
        public const int PageLayoutTwoPageRight = 32;

        // page mode (section 13.1.2 of "iText in Action")
        
        /** A viewer preference */
        public const int PageModeUseNone = 64;
        /** A viewer preference */
        public const int PageModeUseOutlines = 128;
        /** A viewer preference */
        public const int PageModeUseThumbs = 256;
        /** A viewer preference */
        public const int PageModeFullScreen = 512;
        /** A viewer preference */
        public const int PageModeUseOC = 1024;
        /** A viewer preference */
        public const int PageModeUseAttachments = 2048;

        //...
        //...
}

6 ثابت اول مربوط به گروه PageLayout هستند و 6 ثابت دوم به گروه PageMode ارتباط دارند و اين كلاس پر است از اين نوع ثوابت (اين كلاس نزديك به 3200 سطر است!). اين نوع طراحي نامناسب است. بجاي گروه بندي خواص يا ثوابت با يك پيشوند، مثلا PageLayout يا PageMode، اين‌ها را به كلاس‌ها يا در اينجا (حين كار با ثوابت عددي) به enum‌هاي متناظر خود منتقل و Refactor كنيد. مثلا:

public enum ViewerPageLayout
{
     SinglePage = 1,
     OneColumn = 2,
     TwoColumnLeft = 4,
     TwoColumnRight = 8,
     TwoPageLeft = 16,
     TwoPageRight = 32
}

مزيت‌ها:
- طبقه بندي منطقي ثوابت در يك enum و گروه بندي صحيح آن‌ها، بجاي گروه بندي توسط يك پيشوند
- استفاده بهينه از intellisense در visual studio
- منسوخ سازي استفاده از اعداد بجاي معرفي ثوابت خصوصا عددي (در اين كتابخانه شما مي‌توانيد بنويسيد PdfWriter.PageLayoutSinglePage و يا 1 و هر دو صحيح هستند؛ اين خوب نيست. ترويج استفاده از اصطلاحا magic numbers هم حين طراحي يك كتابخانه مذموم است.)
- كم شدن حجم كلاس اوليه (مثلا كلاس PdfWriter در اينجا) و در نتيجه نگهداري ساده‌تر آن در طول زمان

۱۳۹۰/۰۸/۰۵

آشنايي با Refactoring - قسمت 12



قبلا در مورد تبديل switch statement به الگوي استراتژي مطلبي را در اين سايت مطالعه كرده‌ايد (^) و بيشتر مربوط است به حالتي كه داخل هر يك از case هاي يك switch statement چندين و چند سطر كد و يا فراخواني يك تابع وجود دارد. حالت ساده‌تري هم براي refactoring يك عبارت switch وجود دارد و آن هم زماني است كه هر case، تنها از يك سطر تشكيل مي‌شود؛ مانند:

namespace Refactoring.Day12.RefactoringSwitchStatement.Before
{
    public class Translator
    {
        public string ToPersian(string englishWord)
        {
            switch (englishWord)
            {
                case "zero":
                    return "صفر";
                case "one":
                    return "يك";
                default:
                    return string.Empty;
            }
        }
    }
}

در اينجا مي‌توان از امكانات ساختار داده‌هاي توكار دات نت استفاده كرد و اين switch statement را به يك dictionary تبديل نمود:

using System.Collections.Generic;

namespace Refactoring.Day12.RefactoringSwitchStatement.After
{
    public class Translator
    {
        IDictionary<string, string> Words = new Dictionary<string, string>
        {
            { "zero", "صفر" },
            { "one", "يك" }
        };

        public string ToPersian(string englishWord)
        {
            string persianWord;
            if (Words.TryGetValue(englishWord, out persianWord))
            {
                return persianWord;
            }

            return string.Empty;
        }
    }
}

همانطور كه ملاحظه مي‌كنيد هر case به يك key و هر return به يك value در Dictionary تعريف شده، تبديل گشته‌اند. در اينجا هم بهتر است از متد TryGetValue جهت دريافت مقدار كليدها استفاده شود؛ زيرا در صورت فراخواني يك Dictionary با كليدي كه در آن موجود نباشد يك استثناء بروز خواهد كرد.
براي حذف اين متد TryGetValue، مي‌توان يك enum را بجاي كليدهاي تعريف شده، معرفي كرد. به صورت زير:

using System.Collections.Generic;

namespace Refactoring.Day12.RefactoringSwitchStatement.After
{
    public enum EnglishWord
    {
        Zero,
        One
    }

    public class Translator2
    {
        IDictionary<EnglishWord, string> Words = new Dictionary<EnglishWord, string>
        {
            { EnglishWord.Zero, "صفر" },
            { EnglishWord.One, "يك" }
        };

        public string ToPersian(EnglishWord englishWord)
        {
            return Words[englishWord];
        }
    }
}


به اين ترتيب از يك خروجي پر از if و else و switch به يك خروجي ساده و بدون وجود هيچ شرطي رسيده‌ايم.

۱۳۹۰/۰۸/۰۱

آشنايي با Refactoring - قسمت 11


قسمت يازدهم آشنايي با Refactoring به توصيه‌هايي جهت بالا بردن خوانايي تعاريف مرتبط با اعمال شرطي مي‌پردازد.

الف) شرط‌هاي تركيبي را كپسوله كنيد

عموما حين تعريف شرط‌هاي تركيبي، هدف اصلي از تعريف آن‌ها پشت انبوهي از && و || گم مي‌شود و براي بيان مقصود، نياز به نوشتن كامنت خواهند داشت. مانند:

using System;

namespace Refactoring.Day11.EncapsulateConditional.Before
{
    public class Element
    {
        private string[] Data { get; set; }
        private string Name { get; set; }
        private int CreatedYear { get; set; }

        public string FindElement()
        {
            if (Data.Length > 1 && Name == "E1" && CreatedYear > DateTime.Now.Year - 1)
                return "Element1";

            if (Data.Length > 2 && Name == "RCA" && CreatedYear > DateTime.Now.Year - 2)
                return "Element2";

            return string.Empty;
        }
    }
}

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

using System;

namespace Refactoring.Day11.EncapsulateConditional.After
{
    public class Element
    {
        private string[] Data { get; set; }
        private string Name { get; set; }
        private int CreatedYear { get; set; }

        public string FindElement()
        {
            if (hasOneYearOldElement)
                return "Element1";

            if (hasTwoYearsOldElement)
                return "Element2";

            return string.Empty;
        }

        private bool hasTwoYearsOldElement
        {
            get { return Data.Length > 2 && Name == "RCA" && CreatedYear > DateTime.Now.Year - 2; }
        }

        private bool hasOneYearOldElement
        {
            get { return Data.Length > 1 && Name == "E1" && CreatedYear > DateTime.Now.Year - 1; }
        }
    }
}


همانطور كه ملاحظه مي‌كنيد پس از اين جايگزيني، خوانايي متد FindElement بهبود يافته است و برنامه نويس اگر 6 ماه بعد به اين كدها مراجعه كند نخواهد گفت: «من اين كدها رو نوشتم؟!»؛ چه برسد به سايريني كه احتمالا قرار است با اين كدها كار كرده و يا آن‌ها را نگهداري كنند.


ب) از تعريف خواص Boolean با نام‌هاي منفي پرهيز كنيد

يكي از مواردي كه عموما علت اصلي بروز بسياري از خطاها در برنامه است، استفاده از نام‌هاي منفي جهت تعريف خواص است. براي مثال در كلاس مشتري زير ابتدا بايد فكر كنيم كه مشتري‌هاي علامتگذاري شده كدام‌ها هستند كه حالا علامتگذاري نشده‌ها به اين ترتيب تعريف شده‌اند.

namespace Refactoring.Day11.RemoveDoubleNegative.Before
{
    public class Customer
    {
        public decimal Balance { get; set; }

        public bool IsNotFlagged
        {
            get { return Balance > 30m; }
        }
    }
}

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

namespace Refactoring.Day11.RemoveDoubleNegative.After
{
    public class Customer
    {
        public decimal Balance { get; set; }

        public bool IsFlagged
        {
            get { return Balance <= 30m; }
        }
    }
}

۱۳۹۰/۰۷/۲۸

آشنايي با Refactoring - قسمت 10


يكي ديگر از روش‌هايي كه جهت بهبود كيفيت كدها مورد استفاده قرار مي‌گيرد، «طراحي با قراردادها» است؛ به اين معنا كه «بهتر است» متدهاي تعريف شده پيش از استفاده از آرگومان‌هاي خود، آن‌ها را دقيقا بررسي كنند و به اين نوع پيش شرط‌ها، قرارداد هم گفته مي‌شود.
نمونه‌اي از آن‌را در قسمت 9 مشاهده كرديد كه در آن اگر آرگومان‌هاي متد AddRole، خالي يا نال باشند، يك استثناء صادر مي‌شود. اين نوع پيغام‌هاي واضح و دقيق در مورد عدم اعتبار ورودي‌هاي دريافتي، بهتر است از پيغام‌هاي كلي و نامفهوم null reference exception كه بدون بررسي stack trace و ساير ملاحظات، علت بروز آن‌ها مشخص نمي‌شوند.
در دات نت 4، جهت سهولت اين نوع بررسي‌ها، مفهوم Code Contracts ارائه شده است. (اين نام هم از اين جهت بكارگرفته شده كه Design by Contract نام تجاري شركت ثبت شده‌اي در آمريكا است!)


يك مثال:
متد زير را در نظر بگيريد. اگر divisor مساوي صفر باشد، استثناي كلي DivideByZeroException صادر مي‌شود:

namespace Refactoring.Day10.DesignByContract.Before
{
    public class MathMehods
    {
        public double Divide(int dividend, int divisor)
        {
            return dividend / divisor;
        }
    }
}

روش متداول «طراحي با قراردادها» جهت بهبود كيفيت كد فوق پيش از دات نت 4 به صورت زير است:

using System;

namespace Refactoring.Day10.DesignByContract.After
{
    public class MathMehods
    {
        public double Divide(int dividend, int divisor)
        {
            if (divisor == 0)
                throw new ArgumentException("divisor cannot be zero", "divisor");

            return dividend / divisor;
        }
    }
}

در اينجا پس از بررسي آرگومان divisor، قرارداد خود را به آن اعمال خواهيم كرد. همچنين در استثناي تعريف شده، پيغام واضح‌تري به همراه نام آرگومان مورد نظر، ذكر شده است كه از هر لحاظ نسبت به استثناي استاندارد و كلي DivideByZeroException مفهوم‌تر است.

در دات نت 4 ، به كمك امكانات مهياي در فضاي نام System.Diagnostics.Contracts، اين نوع بررسي‌ها نام و امكانات درخور خود را يافته‌اند:

using System.Diagnostics.Contracts;

namespace Refactoring.Day10.DesignByContract.After
{
    public class MathMehods
    {
        public double Divide(int dividend, int divisor)
        {
            Contract.Requires(divisor != 0, "divisor cannot be zero");

            return dividend / divisor;
        }
    }
}

البته اگر قطعه كد فوق را به همراه divisor=0 اجرا كنيد، هيچ پيغام خاصي را مشاهده نخواهيد كرد؛ از اين لحاظ كه نياز است تا فايل‌هاي مرتبط با آن‌را از اين آدرس دريافت و نصب كنيد. اين كتابخانه با VS2008 و VS2010 سازگار است. پس از آن، برگه‌ي Code contracts به عنوان يكي از برگه‌هاي خواص پروژه در دسترس خواهد بود و به كمك آن مي‌توان مشخص كرد كه برنامه حين رسيدن به اين نوع بررسي‌ها چه عكس العملي را بايد بروز دهد.

براي مطالعه بيشتر:

۱۳۹۰/۰۷/۲۵

آشنايي با Refactoring - قسمت 9


اين قسمت از آشنايي با Refactoring به كاهش cyclomatic complexity اختصاص دارد و خلاصه آن اين است: «استفاده از if هاي تو در تو بيش از سه سطح، مذموم است» به اين علت كه پيچيدگي كدهاي نوشته شده را بالا برده و نگهداري آن‌ها را مشكل مي‌كند. براي مثال به شبه كد زير دقت كنيد:

if
   if
     if
       if
         do something
       endif
     endif
   endif
 endif


كه حاصل آن شبيه به نوك يك پيكان (Arrow head) شده است. يك مثال بهتر:


namespace Refactoring.Day9.RemoveArrowhead.Before
{
    public class Role
    {
        public string RoleName { set; get; }
        public string UserName { set; get; }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;

namespace Refactoring.Day9.RemoveArrowhead.Before
{
    public class RoleRepository
    {
        private IList<Role> _rolesList = new List<Role>();

        public IEnumerable<Role> Roles { get { return _rolesList; } }

        public void AddRole(string username, string roleName)
        {
            if (!string.IsNullOrWhiteSpace(roleName))
            {
                if (!string.IsNullOrWhiteSpace(username))
                {
                    if (!IsInRole(username, roleName))
                    {
                        _rolesList.Add(new Role 
                        {
                            UserName=username, 
                            RoleName=roleName
                        }); 
                    }
                    else
                    {
                        throw new InvalidOperationException("User is already in this role.");
                    }
                }
                else
                {
                    throw new ArgumentNullException("username");
                }
            }
            else
            {
                throw new ArgumentNullException("roleName");
            }
        }

        public bool IsInRole(string username, string roleName)
        {
            return _rolesList.Any(x => x.RoleName == roleName && x.UserName == username);
        }
    }
}

متد AddRole فوق، نمونه‌ي بارز پيچيدگي بيش از حد حاصل از اعمال if هاي تو در تو است و ... بسيار متداول. براي حذف اين نوك پيكان حاصل از if هاي تو در تو، از بالاترين سطح شروع كرده و شرط‌ها را برعكس مي‌كنيم؛ با اين هدف كه هر چه سريعتر متد را ترك كرده و خاتمه دهيم:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Refactoring.Day9.RemoveArrowhead.After
{
    public class RoleRepository
    {
        private IList<Role> _rolesList = new List<Role>();

        public IEnumerable<Role> Roles { get { return _rolesList; } }

        public void AddRole(string username, string roleName)
        {
            if (string.IsNullOrWhiteSpace(roleName))
                throw new ArgumentNullException("roleName");

            if (string.IsNullOrWhiteSpace(username))
                throw new ArgumentNullException("username");

            if (IsInRole(username, roleName))
                throw new InvalidOperationException("User is already in this role.");

            _rolesList.Add(new Role
            {
                UserName = username,
                RoleName = roleName
            });
        }

        public bool IsInRole(string username, string roleName)
        {
            return _rolesList.Any(x => x.RoleName == roleName && x.UserName == username);
        }
    }
}

اكنون پس از اعمال اين Refactoring ، متد AddRole بسيار خواناتر شده و هدف اصلي آن كه اضافه كردن يك شيء به ليست نقش‌ها است، واضح‌تر به نظر مي‌رسد. به علاوه اينبار قسمت‌هاي مختلف متد AddRole، فقط يك كار را انجام مي‌دهند و وابستگي‌هاي آن‌ها به يكديگر نيز كاهش يافته است.


۱۳۹۰/۰۷/۲۱

آشنايي با Refactoring - قسمت 8


يكي از اشتباهاتي كه همه‌ي ما كم و بيش به آن دچار هستيم ايجاد كلاس‌هايي هستند كه «زياد مي‌دانند». اصطلاحا به آن‌ها God Classes هم مي‌گويند و براي نمونه، پسوند يا پيشوند Util دارند. اين نوع كلاس‌ها اصل SRP را زير سؤال مي‌برند (Single responsibility principle). براي مثال يك فايل ايجاد مي‌شود و داخل آن از انواع و اقسام متدهاي «كمكي» كار با ديتابيس تا رسم نمودار تا تبديل تاريخ ميلادي به شمسي و ... در طي بيش از 10 هزار سطر قرار مي‌گيرند. يا براي مثال گروه بندي‌هاي خاصي را در اين يك فايل از طريق كامنت‌هاي نوشته شده براي قسمت‌هاي مختلف مي‌توان يافت. Refactoring مرتبط با اين نوع كلاس‌هايي كه «زياد مي‌دانند»، تجزيه آن‌ها به كلاس‌هاي كوچكتر، با تعداد وظيفه‌ي كمتر است.
به عنوان نمونه كلاس CustomerService زير، دو گروه كار متفاوت را انجام مي‌دهد. ثبت و بازيابي اطلاعات ثبت نام يك مشتري و همچنين محاسبات مرتبط با سفارشات مشتري‌ها:

using System;
using System.Collections.Generic;

namespace Refactoring.Day8.RemoveGodClasses.Before
{
    public class CustomerService
    {
        public decimal CalculateOrderDiscount(IEnumerable<string> products, string customer)
        {
            // do work
            throw new NotImplementedException();
        }

        public bool CustomerIsValid(string customer, int order)
        {
            // do work
            throw new NotImplementedException();
        }

        public IEnumerable<string> GatherOrderErrors(IEnumerable<string> products, string customer)
        {
            // do work
            throw new NotImplementedException();
        }

        public void Register(string customer)
        {
            // do work
        }

        public void ForgotPassword(string customer)
        {
            // do work
        }
    }
}

بهتر است اين دو گروه، به دو كلاس مجزا بر اساس وظايفي كه دارند، تجزيه شوند. به اين ترتيب نگهداري اين نوع كلاس‌هاي كوچكتر در طول زمان ساده‌تر خواهند شد:

using System;
using System.Collections.Generic;

namespace Refactoring.Day8.RemoveGodClasses.After
{
    public class CustomerOrderService
    {
        public decimal CalculateOrderDiscount(IEnumerable<string> products, string customer)
        {
            // do work
            throw new NotImplementedException();
        }

        public bool CustomerIsValid(string customer, int order)
        {
            // do work
            throw new NotImplementedException();
        }

        public IEnumerable<string> GatherOrderErrors(IEnumerable<string> products, string customer)
        {
            // do work
            throw new NotImplementedException();
        }
    }
}

namespace Refactoring.Day8.RemoveGodClasses.After
{
    public class CustomerRegistrationService
    {
        public void Register(string customer)
        {
            // do work
        }

        public void ForgotPassword(string customer)
        {
            // do work
        }
    }
}

۱۳۹۰/۰۷/۱۸

آشنايي با Refactoring - قسمت 7



يكي ديگر از روش‌هاي Refactoring ، معرفي كردن يك كلاس بجاي پارامترها است. عموما تعريف متدهايي با بيش از 5 پارامتر مذموم است:

using System;
using System.Collections.Generic;

namespace Refactoring.Day7.IntroduceParameterObject.Before
{
    public class Registration
    {
        public void Create(string name, DateTime date, DateTime validUntil,
                           IEnumerable<string> courses, decimal credits)
        {
            // do work
        }
    }
}

در اين حالت بجاي تعريف اين تعداد بالاي پارامترهاي مورد نياز، تمام آن‌ها را تبديل به يك كلاس كرده و استفاده مي‌كنند:

using System;
using System.Collections.Generic;

namespace Refactoring.Day7.IntroduceParameterObject.After
{
    public class RegistrationContext
    {
        public string Name {set;get;}
        public DateTime Date {set;get;}
        public DateTime ValidUntil {set;get;}
        public IEnumerable<string> Courses {set;get;}
        public decimal Credits { set; get; }
    }
}

namespace Refactoring.Day7.IntroduceParameterObject.After
{
    public class Registration
    {
        public void Create(RegistrationContext registrationContext)
        {
            // do work
        }
    }
}

يكي از مزاياي اين روش، منعطف شدن معرفي متدها است؛ به اين صورت كه اگر نياز به افزودن پارامتر ديگري باشد، تنها كافي است يك خاصيت جديد به كلاس RegistrationContext اضافه شود و امضاي متد Create،‌ ثابت باقي خواهد ماند.

روش ديگر تشخيص نياز به اين نوع Refactoring ، يافتن پارامترهايي هستند كه در يك گروه قرار مي‌گيرند. براي مثال:

public int GetIndex(int pageSize, int pageNumber, ...) { ...

همانطور كه ملاحظه مي‌كنيد تعدادي از پارامترها در اينجا با كلمه page شروع شده‌اند. بهتر است اين‌ پارامترهاي مرتبط را به يك كلاس مجزا به نام Page انتقال داد.


۱۳۹۰/۰۷/۱۷

آشنايي با Refactoring - قسمت 6


در ادامه بحث «حذف كدهاي تكراري»، روش Refactoring ديگري به نام "Extract Superclass" وجود دارد كه البته در بين برنامه نويس‌هاي دات نت به نام Base class بيشتر مشهور است تا Superclass. هدف آن هم انتقال كدهاي تكراري بين چند كلاس، به يك كلاس پايه و سپس ارث بري از آن مي‌باشد.

يك مثال:
در WPF و Silverlight جهت مطلع سازي رابط كاربري از تغييرات حاصل شده در مقادير داده‌ها، نياز است كلاس مورد نظر، اينترفيس INotifyPropertyChanged را پياده سازي كند:

using System.ComponentModel;

namespace Refactoring.Day6.ExtractSuperclass.Before
{
    public class User : INotifyPropertyChanged
    {
        string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                if (_name == value) return;
                _name = value;
                raisePropertyChanged("Name");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        void raisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler == null) return;
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}


و نكته‌ي مهم اين است كه اگر 100 كلاس هم داشته باشيد، بايد اين كدهاي تكراري اجباري مرتبط با raisePropertyChanged را در آن‌ها قرار دهيد. به همين جهت مرسوم است براي كاهش حجم كدهاي تكراري، قسمت‌هاي تكراري كد فوق را در يك كلاس پايه قرار مي‌دهند:

using System.ComponentModel;

namespace Refactoring.Day6.ExtractSuperclass.After
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void RaisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler == null) return;
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

و سپس از آن ارث بري مي‌كنند:

namespace Refactoring.Day6.ExtractSuperclass.After
{
    public class User : ViewModelBase
    {
        string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                if (_name == value) return;
                _name = value;
                RaisePropertyChanged("Name");
            }
        }
    }
}


به اين ترتيب اين كلاس پايه در ده‌ها و صدها كلاس قابل استفاده خواهد بود، بدون اينكه مجبور شويم مرتبا يك سري كد تكراري «اجباري» را copy/paste كنيم.

مثالي ديگر:
اگر با ORM هاي Code first كار كنيد، نياز است تا ابتدا طراحي كار توسط كلاس‌هاي ساده دات نتي انجام شود؛ كه اصطلاحا به آن‌ها POCO يا Plain old CLR objects يا Plain old .NET Classes هم گفته مي‌شود. در بين اين كلاس‌ها، متداول است كه يك سري از خصوصيات، تكراري و مشترك باشد؛ مثلا تمام كلاس‌ها تاريخ ثبت ركورد را هم داشته باشند به همراه نام كاربر و مشخصاتي از اين دست. اينجا هم براي حذف كدهاي تكراري، يك Base class طراحي مي‌شود: (+)

۱۳۹۰/۰۷/۱۶

آشنايي با Refactoring - قسمت 5


يكي ديگر از تكنيك‌هاي Refactoring بسيار متداول، «حذف كدهاي تكراري» است. كدهاي تكراري هم عموما حاصل بي‌حوصلگي يا تنبلي هستند و برنامه نويس نياز دارد در زماني كوتاه، حجم قابل توجهي كد توليد كند؛ كه نتيجه‌اش مثلا به صورت زير خواهد شد:

using System;

namespace Refactoring.Day4.RemoveDuplication.Before
{
    public class PersonalRecord
    {
        public DateTime DateArchived { get; private set; }
        public bool Archived { get; private set; }

        public void ArchiveRecord()
        {
            Archived = true;
            DateArchived = DateTime.Now;
        }

        public void CloseRecord()
        {
            Archived = true;
            DateArchived = DateTime.Now;
        }
    }
}

Refactoring ما هم در اينجا عموما به انتقال كدهاي تكراري به يك متد مشترك خلاصه مي‌شود:

using System;

namespace Refactoring.Day4.RemoveDuplication.After
{
    public class PersonalRecord
    {
        public DateTime DateArchived { get; private set; }
        public bool Archived { get; private set; }

        public void ArchiveRecord()
        {
            switchToArchived();
        }

        public void CloseRecord()
        {
            switchToArchived();
        }

        private void switchToArchived()
        {
            Archived = true;
            DateArchived = DateTime.Now;
        }
    }
}


اهميت حذف كدهاي تكراري:
- اگر باگي در اين كدهاي تكراري يافت شود، همه را در سراسر برنامه بايد اصلاح كنيد (زيرا هم اكنون همانند يك ويروس به سراسر برنامه سرايت كرده‌است) و احتمال فراموشي يك قسمت هم ممكن است وجود داشته باشد.
- اگر نياز به بهبود يا تغييري در اين قسمت‌هاي تكراري وجود داشت، باز هم كار برنامه نويس به شدت زياد خواهد بود.

ابزارهاي كمكي:
واقعيت اين است كه در قطعه كد كوتاه فوق، يافتن قسمت‌هاي تكراري بسيار ساده بوده و با يك نگاه قابل تشخيص است؛ اما در برنامه‌هاي بزرگ خير. به همين منظور تعداد قابل توجهي برنامه‌ي كمكي جهت تشخيص كدهاي تكراري پروژه‌ها تابحال توليد شده‌اند؛ مانند CopyPasteKiller، Clone detective و غيره.

علاوه بر اين‌ها نگارش بعدي ويژوال استوديو (نگارش 11) حاوي ابزار Code Clone Detection توكاري است (+) و همچنين يك ليست قابل توجه ديگر را در اين زمينه در اين پرسش و پاسخ مي‌توانيد بيابيد: (+)

۱۳۹۰/۰۷/۱۵

آشنايي با Refactoring - قسمت 4



قسمت چهار آشنايي با Refactoring به معرفي روش «انتقال متدها» اختصاص دارد؛ انتقال متدها به مكاني بهتر. براي نمونه به كلاس‌هاي زير پيش از انجام عمل Refactoring دقت كنيد:

namespace Refactoring.Day3.MoveMethod.Before
{
    public class BankAccount
    {
        public int AccountAge { get; private set; }
        public int CreditScore { get; private set; }

        public BankAccount(int accountAge, int creditScore)
        {
            AccountAge = accountAge;
            CreditScore = creditScore;
        }
    }
}


namespace Refactoring.Day3.MoveMethod.Before
{
    public class AccountInterest
    {
        public BankAccount Account { get; private set; }

        public AccountInterest(BankAccount account)
        {
            Account = account;
        }

        public double InterestRate
        {
            get { return CalculateInterestRate(); }
        }

        public bool IntroductoryRate
        {
            get { return CalculateInterestRate() < 0.05; }
        }

        public double CalculateInterestRate()
        {
            if (Account.CreditScore > 800)
                return 0.02;

            if (Account.AccountAge > 10)
                return 0.03;

            return 0.05;
        }
    }
}


قسمت مورد نظر ما در اينجا، متد AccountInterest.CalculateInterest است. كلاس AccountInterest مرتبا نياز دارد كه از اطلاعات فيلدها و خواص كلاس BankAccount استفاده كند (نكته تشخيص نياز به اين نوع Refactoring). بنابراين بهتر است كه اين متد را به همان كلاس تعريف كننده‌ي فيلدها و خواص اصلي آن انتقال داد. پس از اين نقل و انتقالات خواهيم داشت:

namespace Refactoring.Day3.MoveMethod.After
{
    public class BankAccount
    {
        public int AccountAge { get; private set; }
        public int CreditScore { get; private set; }

        public BankAccount(int accountAge, int creditScore)
        {
            AccountAge = accountAge;
            CreditScore = creditScore;
        }

        public double CalculateInterestRate()
        {
            if (CreditScore > 800)
                return 0.02;

            if (AccountAge > 10)
                return 0.03;

            return 0.05;
        }
    }
}

namespace Refactoring.Day3.MoveMethod.After
{
    public class AccountInterest
    {
        public BankAccount Account { get; private set; }

        public AccountInterest(BankAccount account)
        {
            Account = account;
        }

        public double InterestRate
        {
            get { return Account.CalculateInterestRate(); }
        }

        public bool IntroductoryRate
        {
            get { return Account.CalculateInterestRate() < 0.05; }
        }
    }
}



به همين سادگي!

يك مثال ديگر:
در ادامه به دو كلاس خودرو و موتور خودروي زير دقت كنيد:

namespace Refactoring.Day4.MoveMethod.Ex2.Before
{
    public class CarEngine
    {
        public float LitersPerCylinder { set; get; }
        public int NumCylinders { set; get; }

        public CarEngine(int numCylinders, float litersPerCylinder)
        {
            NumCylinders = numCylinders;
            LitersPerCylinder = litersPerCylinder;
        }
    }
}


namespace Refactoring.Day4.MoveMethod.Ex2.Before
{
    public class Car
    {
        public CarEngine Engine { get; private set; }

        public Car(CarEngine engine)
        {
            Engine = engine;
        }

        public float ComputeEngineVolume()
        {
            return Engine.LitersPerCylinder * Engine.NumCylinders;
        }
    }
}

در اينجا هم متد Car.ComputeEngineVolume چندين‌بار به اطلاعات داخلي كلاس CarEngine دسترسي داشته است؛ بنابراين بهتر است اين متد را به جايي منتقل كرد كه واقعا به آن تعلق دارد:

namespace Refactoring.Day4.MoveMethod.Ex2.After
{
    public class CarEngine
    {
        public float LitersPerCylinder { set; get; }
        public int NumCylinders { set; get; }

        public CarEngine(int numCylinders, float litersPerCylinder)
        {
            NumCylinders = numCylinders;
            LitersPerCylinder = litersPerCylinder;
        }

        public float ComputeEngineVolume()
        {
            return LitersPerCylinder * NumCylinders;
        }
    }
}

namespace Refactoring.Day4.MoveMethod.Ex2.After
{
    public class Car
    {
        public CarEngine Engine { get; private set; }

        public Car(CarEngine engine)
        {
            Engine = engine;
        }
    }
}

بهبودهاي حاصل شده:
يكي ديگر از اصول برنامه نويسي شيء گرا "Tell, Don't Ask" است؛ كه در مثال‌هاي فوق محقق شده. به اين معنا كه: در برنامه نويسي رويه‌اي متداول، اطلاعات از قسمت‌هاي مختلف كد جاري جهت انجام عملي دريافت مي‌شود. اما در برنامه نويسي شيء گرا به اشياء گفته مي‌شود تا كاري را انجام دهند؛ نه اينكه از آن‌ها وضعيت يا اطلاعات داخلي‌اشان را جهت اخذ تصميمي دريافت كنيم. به وضوح، متد Car.ComputeEngineVolume پيش از Refactoring ، اصل كپسوله سازي اطلاعات كلاس CarEngine را زير سؤال برده است. بنابراين به اشياء بگوئيد كه چكار كنند و اجازه دهيد تا خودشان در مورد نحوه‌ي انجام آن، تصميم گيرنده نهايي باشند.

۱۳۹۰/۰۷/۱۴

آشنايي با Refactoring - قسمت 3



قسمت سوم آشنايي با Refactoring در حقيقت به تكميل قسمت قبل كه در مورد «استخراج متدها» بود اختصاص دارد و به مبحث «استخراج يك يا چند كلاس از متدها» يا Extract Method Object اختصاص دارد.
زمانيكه كار «استخراج متدها» را شروع مي‌كنيم، پس از مدتي به علت بالا رفتن تعداد متدهاي كلاس جاري، به آنچنان شكل و شمايل خوشايند و زيبايي دست پيدا نخواهيم كرد. همچنين اينبار بجاي متدي طولاني، با كلاسي طولاني سروكار خواهيم داشت. در اين حالت بهتر است از متدهاي استخراج شده مرتبط، يك يا چند كلاس جديد تهيه كنيم. به همين جهت به آن Extract Method Object مي‌گويند.
بنابراين مرحله‌ي اول كار با يك قطعه كد با كيفيت پايين، استخراج متدهايي كوچك‌تر و مشخص‌تر، از متدهاي طولاني آن است. مرحله بعد، كپسوله كردن اين متدها در كلاس‌هاي مجزا و مرتبط با آن‌ها مي‌باشد (logic segregation). بر اين اساس كه يكي از اصول ابتدايي شيء گرايي اين مورد است: هر كلاس بايد يك كار را انجام دهد (Single Responsibility Principle).
بنابراين اينبار از نتيجه‌ي حاصل از مرحله‌ي قبل شروع مي‌كنيم و عمليات Refactoring را ادامه خواهيم داد:

using System.Collections.Generic;

namespace Refactoring.Day2.ExtractMethod.After
{
    public class Receipt
    {
        private IList<decimal> _discounts;
        private IList<decimal> _itemTotals;

        public decimal CalculateGrandTotal()
        {
            _discounts = new List<decimal> { 0.1m };
            _itemTotals = new List<decimal> { 100m, 200m };

            decimal subTotal = CalculateSubTotal();
            subTotal = CalculateDiscounts(subTotal);
            subTotal = CalculateTax(subTotal);
            return subTotal;
        }

        private decimal CalculateTax(decimal subTotal)
        {
            decimal tax = subTotal * 0.065m;
            subTotal += tax;
            return subTotal;
        }

        private decimal CalculateDiscounts(decimal subTotal)
        {
            if (_discounts.Count > 0)
            {
                foreach (decimal discount in _discounts)
                    subTotal -= discount;
            }
            return subTotal;
        }

        private decimal CalculateSubTotal()
        {
            decimal subTotal = 0m;
            foreach (decimal itemTotal in _itemTotals)
                subTotal += itemTotal;
            return subTotal;
        }
    }
}

اين مثال، همان نمونه‌ي كامل شده‌ي كد نهايي قسمت قبل است. چند اصلاح هم در آن انجام شده است تا قابل استفاده و مفهوم‌تر شود. عموما متغيرهاي خصوصي يك كلاس را به صورت فيلد تعريف مي‌كنند؛ نه خاصيت‌هاي set و get دار. همچنين مثال قبل نياز به مقدار دهي اين فيلدها را هم داشت كه در اينجا انجام شده.
اكنون مي‌خواهيم وضعيت اين كلاس را بهبود ببخشيم و آن‌را از اين حالت بسته خارج كنيم:

using System.Collections.Generic;

namespace Refactoring.Day3.ExtractMethodObject.After
{
    public class Receipt
    {
        public IList<decimal> Discounts { get; set; }
        public decimal Tax { get; set; }
        public IList<decimal> ItemTotals { get; set; }

        public decimal CalculateGrandTotal()
        {
            return new ReceiptCalculator(this).CalculateGrandTotal();
        }
    }
}

using System.Collections.Generic;

namespace Refactoring.Day3.ExtractMethodObject.After
{
    public class ReceiptCalculator
    {
        Receipt _receipt;        

        public ReceiptCalculator(Receipt receipt)
        {
            _receipt = receipt;
        }

        public decimal CalculateGrandTotal()
        {
            decimal subTotal = CalculateSubTotal();
            subTotal = CalculateDiscounts(subTotal);
            subTotal = CalculateTax(subTotal);
            return subTotal;
        }

        private decimal CalculateTax(decimal subTotal)
        {
            decimal tax = subTotal * _receipt.Tax;
            subTotal += tax;
            return subTotal;
        }

        private decimal CalculateDiscounts(decimal subTotal)
        {
            if (_receipt.Discounts.Count > 0)
            {
                foreach (decimal discount in _receipt.Discounts)
                    subTotal -= discount;
            }
            return subTotal;
        }

        private decimal CalculateSubTotal()
        {
            decimal subTotal = 0m;
            foreach (decimal itemTotal in _receipt.ItemTotals)
                subTotal += itemTotal;
            return subTotal;
        }
    }
}

بهبودهاي حاصل شده نسبت به نگارش قبلي آن:
در اين مثال كل عمليات محاسباتي به يك كلاس ديگر منتقل شده است. كلاس ReceiptCalculator شيء‌ايي از نوع Receipt را در سازنده خود دريافت كرده و سپس محاسبات لازم را بر روي آن انجام مي‌دهد. همچنين فيلدهاي محلي آن تبديل به خواصي عمومي و قابل تغيير شده‌اند. در نگارش قبلي، تخفيف‌ها و ماليات و نحوه‌ي محاسبات به صورت محلي و در همان كلاس تعريف شده بودند. به عبارت ديگر با كدي سروكار داشتيم كه قابليت استفاده مجدد نداشت. نمي‌توانست نوع‌هاي مختلفي از Receipt را بپذيرد. نمي‌شد از آن در برنامه‌اي ديگر هم استفاده كرد. تازه شروع كرده بوديم به جدا سازي منطق‌هاي قسمت‌هاي مختلف محاسبات يك متد اوليه طولاني. همچنين اكنون كلاس ReceiptCalculator تنها عهده دار انجام يك عمليات مشخص است.
البته اگر به كلاس ReceiptCalculator قسمت سوم و كلاس Receipt قسمت دوم دقت كنيم، شايد آنچنان تفاوتي را نتوان حس كرد. اما واقعيت اين است كه كلاس Receipt قسمت دوم، تنها يك پيش نمايش مختصري از صدها متد موجود در آن است.


۱۳۹۰/۰۷/۱۳

آشنايي با Refactoring - قسمت 2


قسمت دوم آشنايي با Refactoring به معرفي روش «استخراج متدها» اختصاص دارد. اين نوع Refactoring بسيار ساده بوده و مزاياي بسياري را به همراه دارد؛ منجمله:
- بالا بردن خوانايي كد؛ از اين جهت كه منطق طولاني يك متد به متدهاي كوچكتري با نام‌هاي مفهوم شكسته مي‌شود.
- به اين ترتيب نياز به مستند سازي كدها نيز بسيار كاهش خواهد يافت. بنابراين در يك متد، هر جايي كه نياز به نوشتن كامنت وجود داشت، يعني بايد همينجا آن‌ قسمت را جدا كرده و در متد ديگري كه نام آن، همان خلاصه كامنت مورد نظر است، قرار داد.
- اين نوع جدا سازي منطق‌هاي پياده سازي قسمت‌هاي مختلف يك متد، در آينده نگهداري كد نهايي را نيز ساده‌تر كرده و انجام تغييرات بر روي آن را نيز تسهيل مي‌بخشد؛ زيرا اينبار بجاي هراس از دستكاري يك متد طولاني، با چند متد كوچك و مشخص سروكار داريم.

براي نمونه به مثال زير دقت كنيد:
using System.Collections.Generic;

namespace Refactoring.Day2.ExtractMethod.Before
{
    public class Receipt
    {
        private IList<decimal> Discounts { get; set; }
        private IList<decimal> ItemTotals { get; set; }

        public decimal CalculateGrandTotal()
        {
            // Calculate SubTotal
            decimal subTotal = 0m;
            foreach (decimal itemTotal in ItemTotals)
                subTotal += itemTotal;

            // Calculate Discounts
            if (Discounts.Count > 0)
            {
                foreach (decimal discount in Discounts)
                    subTotal -= discount;
            }

            // Calculate Tax
            decimal tax = subTotal * 0.065m;
            subTotal += tax;

            return subTotal;
        }
    }
}

همانطور كه از كامنت‌هاي داخل متد CalculateGrandTotal مشخص است، اين متد سه كار مختلف را انجام مي‌دهد؛ جمع اعداد، اعمال تخفيف، اعمال ماليات و نهايتا يك نتيجه را باز مي‌گرداند. بنابراين بهتر است هر عمل را به يك متد جداگانه و مشخص منتقل كرده و كامنت‌هاي ذكر شده را نيز حذف كنيم. نام يك متد بايد به اندازه‌ي كافي مشخص و مفهوم باشد و آنچنان نيازي به مستندات خاصي نداشته باشد:

using System.Collections.Generic;

namespace Refactoring.Day2.ExtractMethod.After
{
    public class Receipt
    {
        private IList<decimal> Discounts { get; set; }
        private IList<decimal> ItemTotals { get; set; }

        public decimal CalculateGrandTotal()
        {
            decimal subTotal = CalculateSubTotal();
            subTotal = CalculateDiscounts(subTotal);
            subTotal = CalculateTax(subTotal);
            return subTotal;
        }

        private decimal CalculateTax(decimal subTotal)
        {
            decimal tax = subTotal * 0.065m;
            subTotal += tax;
            return subTotal;
        }

        private decimal CalculateDiscounts(decimal subTotal)
        {
            if (Discounts.Count > 0)
            {
                foreach (decimal discount in Discounts)
                    subTotal -= discount;
            }
            return subTotal;
        }

        private decimal CalculateSubTotal()
        {
            decimal subTotal = 0m;
            foreach (decimal itemTotal in ItemTotals)
                subTotal += itemTotal;
            return subTotal;
        }
    }
}

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

ابزارهاي كمكي جهت پياده سازي روش «استخراج متدها»:

- ابزار Refactoring توكار ويژوال استوديو پس از انتخاب يك قطعه كد و سپس كليك راست و انتخاب گزينه‌ي Refactor->Extract method، اين عمليات را به خوبي مي‌تواند مديريت كند و در وقت شما صرفه جويي خواهد كرد.
- افزونه‌هاي ReSharper و همچنين CodeRush نيز چنين قابليتي را ارائه مي‌دهند؛ البته توانمندي‌هاي آن‌ها از ابزار توكار ياد شده بيشتر است. براي مثال اگر در ميانه كد شما جايي return وجود داشته باشد، گزينه‌ي Extract method ويژوال استوديو كار نخواهد كرد. اما ساير ابزارهاي ياده شده به خوبي از پس اين موارد و ساير موارد پيشرفته‌تر بر مي‌آيند.

نتيجه گيري:
نوشتن كامنت، داخل بدنه‌ي يك متد مزموم است؛ حداقل به دو دليل:
- ابزارهاي خودكار مستند سازي از روي كامنت‌هاي نوشته شده، از اين نوع كامنت‌ها صرفنظر خواهند كرد و در كتابخانه‌ي شما مدفون خواهند شد (يك كار بي‌حاصل).
- وجود كامنت در داخل بدنه‌ي يك متد، نمود آشكار ضعف شما در كپسوله سازي منطق مرتبط با آن قسمت است.


و ... «لطفا» اين نوع پياده سازي‌ها را خارج از فايل code behind هر نوع برنامه‌ي winform/wpf/asp.net و غيره قرار دهيد. تا حد امكان سعي كنيد اين مكان‌ها، استفاده كننده‌ي «نهايي» منطق‌هاي پياده سازي شده توسط كلاس‌هاي ديگر باشند؛ نه اينكه خودشان محل اصلي قرارگيري و ابتداي تعريف منطق‌هاي مورد نياز قسمت‌هاي مختلف همان فرم مورد نظر باشند. «لطفا» يك فرم درست نكنيد با 3000 سطر كد كه در قسمت code behind آن قرار گرفته‌اند. code behind را محل «نهايي» ارائه كار قرار دهيد؛ نه نقطه‌ي آغاز تعريف منطق‌هاي پياده سازي كار. اين برنامه نويسي چندلايه كه از آن صحبت مي‌شود، فقط مرتبط با كار با بانك‌هاي اطلاعاتي نيست. در همين مثال، كدهاي فرم برنامه، بايد نقطه‌ي نهايي نمايش عمليات محاسبه ماليات باشند؛ نه اينكه همانجا دوستانه يك قسمت ماليات حساب شود، يك قسمت تخفيف، يك قسمت جمع بزند، همانجا هم نمايش بدهد! بعد از يك هفته مي‌بينيد كه code behind فرم در حال انفجار است! شده 3000 سطر! بعد هم سؤال مي‌پرسيد كه چرا اينقدر ميل به «بازنويسي» سيستم اين اطراف زياد است! برنامه نويس حاضر است كل كار را از صفر بنويسد، بجاي اينكه با اين شاهكار بخواهد سرو كله بزند! هر چند يكي از روش‌هاي برخورد با اين نوع كدها جهت كاهش هراس نگهداري آن‌ها، شروع به Refactoring است.

۱۳۹۰/۰۷/۱۲

آشنايي با Refactoring - قسمت 1


كارهاي سورس باز قابل توجهي از برنامه نويس‌هاي ايراني يافت نمي‌شوند؛ عموما كارهاي ارائه شده در حد يك سري مثال يا كتابخانه‌هاي كوچك است و در همين حد. يا گاهي هم انگشت شمار پروژه‌هايي كامل. مثل يك وب سايت يا يك برنامه نصفه نيمه دبيرخانه و امثال آن. اين‌ها هم خوب است از ديدگاه به اشتراك گذاري اطلاعات، ايده‌ها و هم ... يك مزيت ديگر را هم دارد و آن اين است كه بتوان كيفيت عمومي كد نويسي را حدس زد.
اگر كيفيت كدها رو بررسي ‌كنيد به يك نتيجه‌ي كلي خواهيد رسيد: "عموم برنامه نويس‌هاي ايراني (حداقل اين‌هايي كه چند عدد كار سورس باز به اشتراك گذاشته‌اند) با مفهومي به نام Refactoring هيچگونه آشنايي ندارند". مثلا يك برنامه‌ي WinForm تهيه كرده‌اند و كل سورس برنامه همان چند عدد فرم برنامه است و هر فرم بالاي 3000 سطر كد دارد. دوستان عزيز! به اين مي‌گويند «فاجعه‌اي به نام كدنويسي!» صاحب اول و آخر اين نوع كدها خودتان هستيد! شايد به همين جهت باشد كه عمده‌ي پروژه‌هاي سورس باز پس از اينكه برنامه نويس اصلي از توسعه‌ي آن دست مي‌كشد، «مي‌ميرند». چون كسي جرات نمي‌كند به اين كدها دست بزند. مشخص نيست الان اين قسمت را كه تغيير دادم، كجاي برنامه به هم ريخت. تستي ندارند. ساختاري را نمي‌توان از آن‌ها دريافت. منطق قسمت‌هاي مختلف برنامه از هم جدا نشده است. برنامه يك فرم است با چند هزار سطر كد در يك فايل! كار شما شبيه به كد اسمبلي چند هزار سطري حاصل از decompile يك برنامه كه نبايد باشد!
به همين جهت قصد دارم يك سري «ساده» Refactoring را در اين سايت ارائه دهم. روي سادگي هم تاكيد كردم، چون اگر عموم برنامه نويس‌ها با همين موارد به ظاهر ساده آشنايي داشتند، كيفيت كد نويسي بهتري را مي‌شد در نتايج عمومي شده، شاهد بود.
اين مورد در راستاي نظر سنجي انجام شده هم هست؛ درخواست مقالات خالص سي شارپ در صدر آمار فعلي قرار دارد.



Refactoring چيست؟

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


قسمت اول - مجموعه‌ها را كپسوله كنيد

براي مثال كلاس‌هاي ساده زير را در نظر بگيريد:

namespace Refactoring.Day1.EncapsulateCollection
{
    public class OrderItem
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public int Amount { set; get; }
    }
}

using System.Collections.Generic;

namespace Refactoring.Day1.EncapsulateCollection
{    
    public class Orders
    {
        public List<OrderItem> OrderItems { set; get; }
    }
}

نكته اول: هر كلاس بايد در داخل يك فايل جدا قرار گيرد. «لطفا» يك فايل درست نكنيد با 50 كلاس داخل آن. البته اگر باز هم يك فايل باشد كه بتوان 50 كلاس را داخل آن مشاهده كرد كه چقدر هم عالي! نه اينكه يك فايل باشد تا بعدا 50 كلاس را با Refactoring از داخل آن بيرون كشيد!

قطعه كد فوق، يكي از روش‌هاي مرسوم كد نويسي است. مجموعه‌اي به صورت يك List عمومي در اختيار مصرف كننده قرار گرفته است. حال اجازه دهيد تا با استفاده از برنامه FxCop برنامه فوق را آناليز كنيم. يكي از خطاهايي را كه نمايش خواهد داد عبارت زير است:

Error, Certainty 95, for Do Not Expose Generic Lists

بله. ليست‌هاي جنريك را نبايد به همين شكل در اختيار مصرف كننده قرار داد؛ چون به اين صورت هر كاري را مي‌توانند با آن انجام دهند، مثلا كل آن را تعويض كنند، بدون اينكه كلاس تعريف كننده آن از اين تغييرات مطلع شود.
پيشنهاد FxCop اين است كه بجاي List از Collection يا IList و موارد مشابه استفاده شود. اگر اينكار را انجام دهيم اينبار به خطاي زير خواهيم رسيد:

Warning, Certainty 75, for Collection Properties Should Be ReadOnly

FxCop پيشنهاد مي‌دهد كه مجموعه تعريف شده بايد فقط خواندني باشد.

چكار بايد كرد؟
بجاي استفاده از List جهت ارائه مجموعه‌ها، از IEnumerable استفاده كنيد و اينبار متدهاي Add و Remove اشياء به آن‌را به صورت دستي تعريف نمائيد تا بتوان از تغييرات انجام شده بر روي مجموعه ارائه شده، در كلاس اصلي آن مطلع شد و امكان تعويض كلي آن‌را از مصرف كننده گرفت. براي مثال:

using System.Linq;
using System.Collections.Generic;

namespace Refactoring.Day1.EncapsulateCollection
{
    public class Orders
    {
        private int _orderTotal;
        private List<OrderItem> _orderItems;

        public IEnumerable<OrderItem> OrderItems
        {
            get { return _orderItems; }
        }

        public void AddOrderItem(OrderItem orderItem)
        {
            _orderTotal += orderItem.Amount;
            _orderItems.Add(orderItem);
        }

        public void RemoveOrderItem(OrderItem orderItem)
        {
            var order = _orderItems.Find(o => o == orderItem);
            if (order == null) return;

            _orderTotal -= orderItem.Amount;
            _orderItems.Remove(orderItem);
        }
    }
}


اكنون اگر برنامه را مجددا با fxCop آناليز كنيم، دو خطاي ذكر شده ديگر وجود نخواهند داشت. اگر اين تغييرات صورت نمي‌گرفت، امكان داشتن فيلد _orderTotal غير معتبري در كلاس Orders به شدت بالا مي‌رفت. زيرا مصرف كننده مجموعه OrderItems مي‌توانست به سادگي آيتمي را به آن اضافه يا از آن حذف كند، بدون اينكه كلاس Orders از آن مطلع شود يا اينكه بتواند عكس العمل خاصي را بروز دهد.