۱۳۹۰/۱۰/۲۱

مروري بر كدهاي كلاس 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 را مي‌شود به عنوان يك پيشنياز در نظر گرفت؛ عمومي است و همه گير. كسي هم از شنيدن نام آن تعجب نخواهد كرد. كتاب(هاي) آموزشي هم در مورد آن زياد هست.
و ...