۱۳۹۰/۰۷/۱۲

آشنايي با 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 از آن مطلع شود يا اينكه بتواند عكس العمل خاصي را بروز دهد.