۱۳۹۰/۰۶/۰۴

كمپين ضد IF !


بكارگيري بيش از حد If و خصوصا Switch برخلاف اصول طراحي شيءگرا است؛ تا اين حد كه يك كمپين ضد IF هم وجود دارد!



البته سايت فوق بيشتر جنبه تبليغي براي سمينارهاي گروه مذكور را دارد تا اينكه جنبه‌ي آموزشي/خود آموزي داشته باشد.

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

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

namespace CircularDependencies
{
    public enum AggregateFunc
    {
        Sum,
        Avg
    }

    public class AggregateFuncCalculator
    {
        public decimal Calculate(IList<decimal> list, AggregateFunc func)
        {
            switch (func)
            {
                case AggregateFunc.Sum:
                    return getSum(list);
                case AggregateFunc.Avg:
                    return getAvg(list);
                default:
                    return 0m;
            }
        }

        private decimal getAvg(IList<decimal> list)
        {
            if (list == null || !list.Any()) return 0;
            return list.Sum() / list.Count;
        }

        private decimal getSum(IList<decimal> list)
        {
            if (list == null || !list.Any()) return 0;
            return list.Sum();
        }
    }
}

در كلاس AggregateFuncCalculator يك متد Calculate داريم كه توسط آن قرار است روي list دريافتي يك سري عمليات انجام شود. عمليات پشتيباني شده هم توسط يك enum معرفي شده؛ براي مثال اينجا فقط جمع و ميانگين پشتيباني مي‌شوند.
و مشكل طراحي اين كلاس، همان switch است كه برخلاف اصول طراحي شيء‌گرا مي‌باشد. يكي از اصول طراحي شيءگرا بر اين مبنا است كه:
يك كلاس بايد جهت تغيير، بسته اما جهت توسعه، باز باشد.

يعني چي؟
داستان طراحي Aggregate functions كه فقط به جمع و ميانگين خلاصه نمي‌شود. امروز مي‌گويند واريانس چطور؟ فردا خواهند گفت حداقل و حداكثر چطور؟ پس فردا ...
به عبارتي اين كلاس جهت تغيير بسته نيست و هر روز بايد بر اساس نيازهاي جديد دستكاري شود.

چكار بايد كرد؟
آيا مي‌توانيد در كلاس AggregateFuncCalculator يك الگوي تكراري را تشخيص دهيد؟ الگوي تكراري موجود، محاسبات بر روي يك ليست است. پس مي‌شود بر اساس آن يك اينترفيس عمومي را تعريف كرد:

public interface IAggregateFunc
{
     decimal Calculate(IList<decimal> list);
}

اكنون هر كدام از پياده سازي‌هاي موجود در كلاس AggregateFuncCalculator را به يك كلاس جدا منتقل خواهيم كرد تا يك اصل ديگر طراحي شيءگرا نيز محقق شود:
هر كلاس بايد تنها يك كار را انجام دهد.

public class Sum : IAggregateFunc
{
        public decimal Calculate(IList<decimal> list)
        {
            if (list == null || !list.Any()) return 0;
            return list.Sum();
        }
}

public class Avg : IAggregateFunc
{
        public decimal Calculate(IList<decimal> list)
        {
            if (list == null || !list.Any()) return 0;
            return list.Sum() / list.Count;
        }
}

تا اينجا 2 هدف مهم حاصل شده است:
- كم كم كلاس AggregateFuncCalculator دارد خلوت مي‌شود. قرار است هر كلاس يك كار را بيشتر انجام ندهد.
- برنامه از بسته بودن جهت توسعه هم خارج شده است (يكي ديگر از اصول طراحي شيءگرا). اگر تعاريف توابع محاسباتي را تماما در يك كلاس قرار دهيم صاحب اول و آخر آن كتابخانه خودمان خواهيم بود. اين كلاس بسته است جهت تغيير. اما با معرفي IAggregateFunc، من امروز 2 تابع را تعريف كرد‌ه‌ام، شما فردا توابع خاص خودتان را تعريف كنيد. باز هم برنامه كار خواهد كرد. نيازي نيست تا من هر روز يك نگارش جديد از كتابخانه را ارائه دهم كه در آن فقط يك تابع ديگر اضافه شده است.

اكنون يكي از چندين و چند روش بازنويسي كلاس AggregateFuncCalculator به صورت زير مي‌تواند باشد

public class AggregateFuncCalculator
{
        public decimal Calculate(IList<decimal> list, IAggregateFunc func)
        {
            return func.Calculate(list);
        }
}

بله! ديگر سوئيچي در كار نيست. اين كلاس تنها يك كار را انجام مي‌دهد. همچنين ديگر نيازي به تغيير هم ندارد (محاسبات از آن خارج شده) و باز است جهت توسعه (شما نگارش‌هاي دلخواه IAggregateFunc ديگر خود را توسعه داده و استفاده كنيد).