۱۳۹۰/۰۷/۱۴

آشنايي با 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 قسمت دوم، تنها يك پيش نمايش مختصري از صدها متد موجود در آن است.