فصل سیزدهم: بازآرایی کدهای C# – شناسایی Code Smellها 🎯

شناسایی Code Smellها

در این فصل، ما به بررسی کدهای مشکل‌دار و روش بازآرایی (Refactoring) آن‌ها می‌پردازیم. در صنعت نرم‌افزار، به چنین کدهایی اصطلاحاً Code Smell گفته می‌شود. این نوع کدها کامپایل می‌شوند، اجرا می‌شوند و همان کاری را انجام می‌دهند که قرار است انجام دهند، اما مشکل آن‌ها این است که به مرور زمان خوانایی خود را از دست می‌دهند، پیچیده می‌شوند و نگهداری و توسعه آن‌ها دشوار می‌گردد.

چنین کدهایی باید در اولین فرصت بازآرایی شوند. در غیر این صورت، این مشکل به‌عنوان بدهی فنی (Technical Debt) باقی می‌ماند و در طولانی‌مدت می‌تواند پروژه را به نقطه شکست برساند، به‌طوری که مجبور شوید کل برنامه را از ابتدا طراحی و پیاده‌سازی کنید که هزینه‌بر خواهد بود.

بازآرایی (Refactoring) چیست؟
بازآرایی فرایند بازنویسی کد موجودی است که کار می‌کند، اما هدف این است که کد تمیزتر و بهتر شود. همان‌طور که پیش‌تر دیدید، کد تمیز (Clean Code) به‌راحتی خوانده می‌شود، نگهداری می‌شود و توسعه پیدا می‌کند.

در این فصل به موارد زیر خواهیم پرداخت:

پس از مطالعه این فصل، مهارت‌های زیر را به دست خواهید آورد:

ما بررسی خود را با Code Smellهای سطح برنامه آغاز می‌کنیم.


الزامات فنی 🛠️

برای این فصل به ابزارها و پیش‌نیازهای زیر احتیاج دارید:


Code Smellهای سطح برنامه

Code Smellهای سطح برنامه کدهای مشکل‌داری هستند که در تمام بخش‌های برنامه پراکنده‌اند و بر تمام لایه‌ها تأثیر می‌گذارند. فرقی نمی‌کند در کدام لایه از نرم‌افزار باشید؛ این مشکلات را بارها و بارها خواهید دید.

اگر این مشکلات را همین حالا برطرف نکنید، نرم‌افزار شما به مرور زمان دچار افت عملکرد شده و به سمت شکست پیش می‌رود.

در این بخش، به Code Smellهای سطح برنامه و روش‌های رفع آن‌ها می‌پردازیم. با Boolean Blindness شروع می‌کنیم.


Boolean Blindness (کوری بولیانی) 👀

کوری بولیانی به معنای از دست رفتن اطلاعات در متدها یا توابعی است که با مقادیر Boolean (true/false) کار می‌کنند.
استفاده از ساختارهای بهتر، رابط‌ها و کلاس‌هایی با وضوح بالاتر ایجاد می‌کند که داده‌ها را واضح‌تر نگه می‌دارند و تجربه کاری بهتری فراهم می‌کنند.

بیایید این مشکل را با یک مثال بررسی کنیم:

public void BookConcert(string concert, bool standing)
{
    if (standing)
    {
        // Issue standing ticket.
    }
    else
    {
        // Issue sitting ticket.
    }
}

این متد یک string برای نام کنسرت و یک Boolean برای مشخص کردن اینکه فرد ایستاده یا نشسته است، دریافت می‌کند. حالا این کد به این صورت فراخوانی می‌شود:

private void BooleanBlindnessConcertBooking()
{
    var booking = new ProblemCode.ConcertBooking();
    booking.BookConcert("Solitary Experiments", true);
}

اگر شخصی تازه‌وارد به این کد نگاه کند، آیا می‌تواند به‌صورت غریزی متوجه شود که true به چه معناست؟ مسلماً نه.
او برای فهمیدن موضوع مجبور است یا از IntelliSense استفاده کند یا به متد اصلی مراجعه کند.
این همان کوری بولیانی است.

راه‌حل چیست؟
یک راه‌حل ساده این است که به جای Boolean از enum استفاده کنیم.

ابتدا enum خود را ایجاد می‌کنیم:

[Flags]
internal enum TicketType
{
    Seated,
    Standing
}

این enum دو نوع بلیط را مشخص می‌کند: Seated (نشسته) و Standing (ایستاده).

حالا متد BookConcert() خود را بازآرایی می‌کنیم:

internal void BookConcert(string concert, TicketType ticketType)
{
    if (ticketType == TicketType.Seated)
    {
        // Issue seated ticket.
    }
    else
    {
        // Issue standing ticket.
    }
}

و حالا به این صورت فراخوانی می‌کنیم:

private void ClearSightedConcertBooking()
{
    var booking = new RefactoredCode.ConcertBooking();
    booking.BookConcert("Chrom", TicketType.Seated);
}

در این حالت، اگر شخص جدیدی به کد نگاه کند، فوراً متوجه می‌شود که در حال رزرو بلیط نشسته برای کنسرت گروه Chrom هستیم.

💥 انفجار ترکیبی (Combinatorial Explosion)

انفجار ترکیبی، نتیجه‌ی اجرای یک عمل مشابه توسط قسمت‌های مختلف کد، اما با ترکیب‌های متفاوتی از پارامترها است. بیایید به یک مثال که اعداد را جمع می‌کند نگاه کنیم:

public int Add(int x, int y)
{
    return x + y;
}
public double Add(double x, double y)
{
    return x + y;
}
public float Add(float x, float y)
{
    return x + y;
}

در اینجا، ما سه متد داریم که همگی عملیات جمع را انجام می‌دهند، اما نوع داده‌ی بازگشتی (return type) و پارامترها متفاوت هستند. آیا راه بهتری وجود دارد؟ بله ✅، با استفاده از Generics (جنریک‌ها). با کمک جنریک‌ها، شما می‌توانید تنها یک متد داشته باشید که قادر به کار کردن با انواع داده‌های مختلف است. پس ما از جنریک‌ها برای حل مسئله جمع استفاده می‌کنیم. این کار به ما اجازه می‌دهد یک متد واحد داشته باشیم که عدد صحیح (int)، عدد اعشاری (double) یا عدد اعشاری با دقت کمتر (float) را بپذیرد. بیایید به متد جدید نگاه کنیم:

public T Add<T>(T x, T y)
{
    dynamic a = x;
    dynamic b = y;
    return a + b;
}

این متد جنریک با یک نوع مشخص برای T فراخوانی می‌شود. عملیات جمع را انجام می‌دهد و نتیجه را بازمی‌گرداند. فقط یک نسخه از متد برای انواع مختلف داده‌های .NET که امکان جمع شدن دارند کافی است. برای فراخوانی این کد با مقادیر int، double و float، به این صورت عمل می‌کنیم:

var addition = new RefactoredCode.Maths();
addition.Add<int>(1, 2);
addition.Add<double>(1.2, 3.4);
addition.Add<float>(5.6f, 7.8f);

ما به‌تازگی سه متد را حذف کرده و جایگزین آن‌ها را یک متد واحد کرده‌ایم که همان وظیفه را انجام می‌دهد. 🎯


🧩 پیچیدگی ساختگی (Contrived Complexity)

زمانی که می‌توانید کدی با معماری ساده پیاده‌سازی کنید، اما به‌جای آن یک معماری پیشرفته و نسبتاً پیچیده اجرا می‌کنید، به آن پیچیدگی ساختگی می‌گویند. متأسفانه، من شخصاً مجبور به کار روی چنین سیستم‌هایی بوده‌ام و باید بگویم این موضوع واقعاً دردسرساز و استرس‌آور است.

در چنین سیستم‌هایی معمولاً اتفاقات زیر رخ می‌دهد:

👨‍💻 توصیه من به همه معماران نرم‌افزار فوق‌هوشمند این است:
Keep It Simple, Stupid (KISS) – «ساده نگه‌دار، احمقانه».

به خاطر داشته باشید که دوران استخدام‌های دائمی و شغل‌های مادام‌العمر دیگر کمتر شده است. امروزه بسیاری از برنامه‌نویسان به‌دنبال درآمد بیشتر هستند تا وفاداری طولانی‌مدت به کسب‌وکار. بنابراین، با توجه به اینکه درآمد شرکت به نرم‌افزار وابسته است، باید سیستمی داشته باشید که:

از خودتان بپرسید:

من نمی‌توانم به اندازه کافی بر اهمیت KISS تأکید کنم. تنها دلیلی که برخی افراد سیستم‌های پیچیده و بدون مستندات می‌سازند، وابسته کردن کسب‌وکار به خودشان است. این کار اشتباه است. تجربه من نشان داده که هرچه سیستم پیچیده‌تر باشد، سریع‌تر شکست می‌خورد و نیاز به بازنویسی دارد. ❌


🛠 کاهش پیچیدگی و بهبود کیفیت کد

در فصل ۱۲ (Using Tools to Improve Code Quality) یاد گرفتید که چگونه از ابزارهای Visual Studio 2019 برای کشف Cyclomatic Complexity و Depth of Inheritance استفاده کنید. همچنین یاد گرفتید چگونه نمودار وابستگی (Dependency Diagram) با استفاده از ReSharper تولید کنید.

از این ابزارها برای پیدا کردن بخش‌های مشکل‌دار در کد استفاده کنید و روی آن‌ها تمرکز کنید:

همچنین به آیکون‌های Quick Tips توجه کنید. این آیکون‌ها معمولاً پیشنهادهای Refactoring یک‌کلیکی برای خط کد موردنظر ارائه می‌دهند. توصیه می‌کنم از آن‌ها استفاده کنید.


📦 توده داده (Data Clump)

توده داده زمانی رخ می‌دهد که فیلدهای مشابه را در کلاس‌های مختلف و لیست پارامترها می‌بینید که معمولاً الگوی نام‌گذاری مشابهی دارند. این معمولاً نشانه‌ی این است که یک کلاس در سیستم وجود ندارد. کاهش پیچیدگی سیستم با شناسایی کلاس گمشده و عمومی‌سازی آن به‌دست می‌آید.

از کوچک بودن کلاس نترسید و هیچ‌وقت فکر نکنید که یک کلاس کوچک بی‌اهمیت است. اگر برای ساده‌سازی کد به یک کلاس نیاز دارید، آن را اضافه کنید.

کامنت‌های خوشبوکننده (Deodorant Comments) 📝

وقتی یک کامنت با جملات زیبا و مثبت سعی دارد کدی ضعیف یا بد را توجیه کند، به آن کامنت خوشبوکننده گفته می‌شود. ❌ اگر کد بد است، باید بازآرایی (Refactor) شود تا خوب شود و سپس کامنت حذف شود. اگر نمی‌دانید چگونه بازآرایی کنید، از دیگران کمک بگیرید. اگر کسی برای کمک در دسترس نیست، کد خود را در Stack Overflow قرار دهید. برنامه‌نویسان بسیار ماهری در آن سایت هستند که می‌توانند به شما کمک کنند، فقط قوانین انتشار را رعایت کنید!


کد تکراری (Duplicate Code) 🔁

کد تکراری، کدی است که بیش از یک‌بار در پروژه ظاهر شده است. مشکلاتی که از تکرار کد به‌وجود می‌آیند:

راه‌حل چیست؟ بازآرایی کد برای حذف تکرارها. ساده‌ترین روش، انتقال کد به یک کلاس جدید قابل‌استفاده مجدد در پروژه است.

مزیت دیگر این است که می‌توانید کدهای قابل‌استفاده مجدد را در یک کتابخانه (Class Library) قرار دهید تا سایر پروژه‌ها هم از آن بهره ببرند.
امروزه بهترین انتخاب، استفاده از کتابخانه‌های .NET Standard است، چون این کتابخانه‌ها در تمامی پلتفرم‌ها در دسترس‌اند: Windows، Linux، macOS، iOS و Android.

روش‌های دیگر برای کاهش کدهای تکراری:


از دست رفتن هدف یا نیت (Lost Intent)

وقتی نتوانید به‌راحتی هدف یا مقصود کد را درک کنید، یعنی نیت کد از بین رفته است.

گام‌ها برای رفع این مشکل:

  1. بررسی فضای نام (Namespace) و نام کلاس: باید هدف کلاس را نشان دهد.

  2. بررسی محتوای کلاس: به‌دنبال کدی باشید که در جای نامناسبی قرار دارد. آن‌ها را شناسایی و به جای درست منتقل کنید.

  3. بررسی متدها:

    • آیا هر متد فقط یک کار را به‌خوبی انجام می‌دهد یا چند کار را نه‌چندان خوب؟
    • اگر پاسخ دوم است، بازآرایی کنید.
    • در متدهای بزرگ، کدهای قابل‌استخراج را به متدهای کوچک‌تر منتقل کنید.

هدف نهایی: کد کلاس باید مثل یک کتاب خوانا باشد. بازآرایی کنید تا نیت کد شفاف شود و هر کلاس فقط کاری را انجام دهد که برای آن طراحی شده است.

نکته: ابزارهای معرفی‌شده در فصل 12 (استفاده از ابزارها برای بهبود کیفیت کد) را فراموش نکنید.

مبحث بعدی: بوی بد کد – تغییرات مکرر متغیرها (Mutation of Variables) خواهد بود.

تغییرات مکرر متغیرها (The Mutation of Variables) 🔄

تغییرات مکرر متغیرها یعنی متغیرها به‌گونه‌ای هستند که فهمیدن و استدلال درباره‌ی آن‌ها دشوار است. این باعث می‌شود که بازآرایی (Refactor) آن‌ها سخت شود.

یک متغیر قابل تغییر (Mutable Variable)، متغیری است که چندین بار توسط عملیات مختلف تغییر می‌کند. این مسئله استدلال درباره‌ی دلیل تغییر مقدار متغیر را دشوار می‌کند. علاوه بر این، چون متغیر توسط عملیات مختلف تغییر می‌کند، استخراج بخش‌هایی از کد به متدهای کوچک‌تر و خواناتر نیز سخت‌تر می‌شود. همچنین، متغیرهای قابل تغییر ممکن است نیازمند بررسی‌های بیشتری باشند که پیچیدگی کد را افزایش می‌دهد.

راهکارها


مثال عملی

[InstrumentationAspect]
public class Mutant
{
    public int IntegerSquaredSum(List<int> integers)
    {
        var squaredSum = 0;
        foreach (var integer in integers)
        {
            squaredSum += integer * integer;
        }
        return squaredSum;
    }
}

این متد، یک لیست از اعداد صحیح (integers) دریافت می‌کند، سپس روی هر عدد حلقه می‌زند، مربع آن را محاسبه و به متغیر squaredSum اضافه می‌کند. توجه کنید که در هر تکرار حلقه، مقدار متغیر محلی تغییر می‌کند.


نسخه بازآرایی شده و بهبود یافته با LINQ

[InstrumentationAspect]
public class Function
{
    public int IntegerSquaredSum(List<int> integers)
    {
        return integers.Sum(integer => integer * integer);
    }
}

در نسخه جدید، از LINQ استفاده شده است. همان‌طور که در فصل‌های قبل یاد گرفتید، LINQ از برنامه‌نویسی تابعی (Functional Programming) بهره می‌برد.
همان‌طور که می‌بینید:

با کامپایل و اجرای برنامه، خروجی مشابه نسخه قبلی خواهد بود، اما کد ساده‌تر، خواناتر و بدون تغییر مکرر متغیر است.

Conventions-UsedThis-Book

هر دو نسخه کد خروجی یکسانی تولید می‌کنند. ✅

شما متوجه خواهید شد که هر دو نسخه کد، [InstrumentationAspect] روی آن‌ها اعمال شده است. این Aspect را در فصل ۱۲، «پرداختن به مسائل Cross-Cutting» به کتابخانه قابل استفاده مجدد خود اضافه کردیم. وقتی کد را اجرا می‌کنید، یک پوشه Logs در پوشه Debug خواهید یافت. فایل Profile.log را در Notepad باز کنید و خروجی زیر را مشاهده خواهید کرد:

Method: IntegerSquaredSum, Start Time: 01/07/2020 11:41:43
Method: IntegerSquaredSum, Stop Time: 01/07/2020 11:41:43, Duration: 00:00:00.0005489
Method: IntegerSquaredSum, Start Time: 01/07/2020 11:41:43
Method: IntegerSquaredSum, Stop Time: 01/07/2020 11:41:43, Duration: 00:00:00.0000027

خروجی نشان می‌دهد که متد ProblemCode.IntegerSquaredSum() کندترین نسخه بوده و اجرای آن ۵۴۸.۹ نانوثانیه طول کشیده است. در حالی که متد RefactoredCode.IntegerSquaredSum() بسیار سریع‌تر بوده و تنها ۲.۷ نانوثانیه زمان برده است. ⏱️

با بازسازی حلقه و استفاده از LINQ، از تغییر متغیر محلی جلوگیری کردیم. همچنین زمان پردازش محاسبه را ۵۴۶.۲ نانوثانیه کاهش دادیم. چنین بهبودی بسیار کوچک است و با چشم انسان قابل تشخیص نیست. اما اگر چنین محاسباتی روی داده‌های بزرگ انجام شود، تفاوت قابل توجهی احساس خواهد شد.

حالا به «راه‌حل عجیب» می‌پردازیم. 🌀

راه‌حل عجیب

وقتی یک مشکل در کد منبع به روش‌های متفاوتی حل شده باشد، به آن «راه‌حل عجیب» گفته می‌شود. این موضوع می‌تواند به دلیل سبک برنامه‌نویسی متفاوت برنامه‌نویسان مختلف و نبود استانداردهای مشخص رخ دهد. همچنین ممکن است به دلیل ناآگاهی از سیستم اتفاق بیفتد، به طوری که برنامه‌نویس متوجه نشود یک راه‌حل از قبل وجود دارد.

یک روش برای بازسازی راه‌حل‌های عجیب این است که یک کلاس جدید بنویسید که رفتاری را که به روش‌های مختلف تکرار شده، در خود جای دهد. رفتار را به تمیزترین و کارآمدترین شکل ممکن به کلاس اضافه کنید. سپس، راه‌حل‌های عجیب را با رفتار بازسازی شده جدید جایگزین کنید.

همچنین می‌توانید با استفاده از Adapter Pattern، رابط‌های مختلف سیستم را متحد کنید. 🔗

Conventions-UsedThis-Book

کلاس Target رابط دامنه‌محوری است که توسط Client استفاده می‌شود. یک رابط موجود که نیاز به تطبیق دارد، Adaptee نامیده می‌شود. کلاس Adapter، کلاس Adaptee را به کلاس Target تطبیق می‌دهد. و در نهایت، کلاس Client با اشیائی که مطابق با رابط Target هستند، ارتباط برقرار می‌کند. حالا بیایید Adapter Pattern را پیاده‌سازی کنیم.

یک کلاس جدید به نام Adaptee اضافه کنید:

public class Adaptee
{
    public void AdapteeOperation()
    {
        Console.WriteLine($"AdapteeOperation() has just executed.");
    }
}

کلاس Adaptee بسیار ساده است. این کلاس شامل یک متد به نام AdapteeOperation() است که یک پیام را روی کنسول چاپ می‌کند.

حالا کلاس Target را اضافه کنید:

public class Target
{
    public virtual void Operation()
    {
        Console.WriteLine("Target.Operation() has executed.");
    }
}

کلاس Target نیز بسیار ساده است و شامل یک متد مجازی به نام Operation() است که یک پیام روی کنسول چاپ می‌کند.

اکنون کلاس Adapter را اضافه می‌کنیم که کلاس‌های Target و Adaptee را به هم متصل می‌کند:

public class Adapter : Target
{
    private readonly Adaptee _adaptee = new Adaptee();

    public override void Operation()
    {
        _adaptee.AdapteeOperation();
    }
}

کلاس Adapter از کلاس Target ارث‌بری می‌کند. سپس یک متغیر عضو برای نگهداری شیء Adaptee ایجاد و مقداردهی اولیه می‌کنیم. سپس یک متد داریم که متد Operation() بازنویسی‌شده کلاس Target است.

در نهایت، کلاس Client را اضافه می‌کنیم:

public class Client
{
    public void Operation()
    {
        Target target = new Adapter();
        target.Operation();
    }
}

کلاس Client شامل یک متد به نام Operation() است. این متد یک شیء Adapter جدید ایجاد کرده و آن را به یک متغیر از نوع Target اختصاص می‌دهد. سپس متد Operation() را روی متغیر Target فراخوانی می‌کند.

اگر متد new Client().Operation() را فراخوانی کرده و کد را اجرا کنید، خروجی زیر را مشاهده خواهید کرد: 🎯

Conventions-UsedThis-Book

همان‌طور که در تصویر مشاهده می‌کنید، متدی که اجرا می‌شود Adaptee.AdapteeOperation() است. ✅

حالا که با موفقیت یاد گرفتید چگونه Adapter Pattern را برای حل راه‌حل‌های عجیب پیاده‌سازی کنید، به سراغ موضوع Shotgun Surgery می‌رویم.


Shotgun Surgery 🔫

ایجاد یک تغییر واحد که نیاز به تغییر در چندین کلاس داشته باشد، Shotgun Surgery نامیده می‌شود. این مسئله گاهی به دلیل بازسازی بیش از حد کد و مواجهه با تغییرات متنوع رخ می‌دهد. این نوع Code Smell احتمال ایجاد باگ‌ها را افزایش می‌دهد، مانند باگ‌هایی که به دلیل از دست رفتن یک فرصت رخ می‌دهند. همچنین احتمال Merge Conflict افزایش می‌یابد، زیرا کد در بخش‌های زیادی نیاز به تغییر دارد و برنامه‌نویسان ممکن است روی تغییرات یکدیگر قدم بگذارند. کد آن‌قدر پیچیده است که باعث ایجاد Overload شناختی در برنامه‌نویسان می‌شود. برنامه‌نویسان جدید نیز به دلیل طبیعت نرم‌افزار، Curve یادگیری شیب‌داری دارند.

تاریخچه کنترل نسخه، تاریخچه تغییرات ایجاد شده در نرم‌افزار را ارائه می‌دهد. این تاریخچه می‌تواند به شما کمک کند تا هر بار که یک قابلیت جدید اضافه می‌شود یا با یک باگ مواجه می‌شوید، تمام بخش‌هایی که تغییر کرده‌اند را شناسایی کنید. بعد از شناسایی این بخش‌ها، می‌توانید تغییرات را به یک منطقه محلی‌تر از کد منتقل کنید. این‌گونه، وقتی نیاز به تغییر دارید، تنها روی یک بخش تمرکز می‌کنید و نه چندین بخش. این کار نگهداری پروژه را بسیار آسان‌تر می‌کند.

کدهای تکراری می‌توانند کاندید مناسبی برای بازسازی و قرارگیری در یک کلاس با نام مناسب و در Namespace صحیح باشند. همچنین به تمام لایه‌های مختلف برنامه نگاه کنید: آیا واقعاً همه آن‌ها لازم هستند؟ آیا می‌توان ساده‌سازی کرد؟ در یک برنامه مبتنی بر پایگاه داده، آیا واقعاً نیاز است که DTO، DAO، اشیاء دامنه و غیره داشته باشیم؟ آیا می‌توان دسترسی به پایگاه داده را ساده‌تر کرد؟ این‌ها تنها چند ایده برای کاهش اندازه کد و در نتیجه کاهش تعداد بخش‌هایی هستند که باید تغییر کنند.


Coupling و Cohesion 🔗

سطح Coupling و Cohesion را بررسی کنید. Coupling باید به حداقل مطلق برسد. یک روش برای رسیدن به این هدف، تزریق وابستگی‌ها از طریق Constructors، Properties، و Methods است. وابستگی‌های تزریق‌شده باید از نوع یک Interface مشخص باشند.

یک مثال ساده را کدنویسی می‌کنیم. ابتدا یک Interface به نام IService اضافه کنید:

public interface IService
{
    void Operation();
}

این Interface شامل یک متد به نام Operation() است. حالا یک کلاس به نام Dependency اضافه کنید که IService را پیاده‌سازی می‌کند:

public class Dependency : IService
{
    public void Operation()
    {
        Console.WriteLine("Dependency.Operation() has executed.");
    }
}

کلاس Dependency Interface IService را پیاده‌سازی می‌کند و در متد Operation() یک پیام روی کنسول چاپ می‌شود.

سپس کلاس LooselyCoupled را اضافه کنید:

public class LooselyCoupled
{
    private readonly IService _service;

    public LooselyCoupled(IService service)
    {
        _service = service;
    }

    public void DoWork()
    {
        _service.Operation();
    }
}

همان‌طور که مشاهده می‌کنید، Constructor یک نوع IService دریافت کرده و آن را در یک متغیر عضو ذخیره می‌کند. فراخوانی DoWork() متد Operation() داخل نوع IService را صدا می‌زند.

کلاس LooselyCoupled واقعاً به‌صورت loosely coupled است و به راحتی تست می‌شود. ✅
کاهش Coupling باعث می‌شود کلاس‌ها راحت‌تر تست شوند. با حذف کدی که در کلاس قرار ندارد و قرار دادن آن در جای مناسب، خوانایی، نگهداری، و توسعه‌پذیری برنامه بهبود می‌یابد. همچنین Curve یادگیری برای افراد جدید کمتر می‌شود و احتمال ایجاد باگ در هنگام نگهداری یا توسعه جدید کاهش می‌یابد.


Solution Sprawl 🌱

پیاده‌سازی یک مسئولیت واحد در متدها، کلاس‌ها و حتی کتابخانه‌های مختلف باعث Solution Sprawl می‌شود. این موضوع باعث می‌شود کد خواندن و فهمیدن دشوار شود و نگهداری و توسعه آن مشکل شود.

برای حل مشکل، پیاده‌سازی مسئولیت واحد را در همان کلاس قرار دهید. این‌گونه کد در یک مکان قرار دارد و وظیفه خود را انجام می‌دهد. نتیجه این است که کد خوانا، قابل فهم، قابل نگهداری و توسعه می‌شود.


Uncontrolled Side Effects ⚠️

Uncontrolled Side Effects مسائلی هستند که در محیط تولید ظاهر می‌شوند، زیرا تست‌های کیفیت قادر به شناسایی آن‌ها نیستند. برای حل این مشکل، تنها گزینه این است که کد را بازسازی کنید تا کاملاً قابل تست باشد و بتوانید مقادیر متغیرها را هنگام Debug مشاهده کنید تا مطمئن شوید درست تنظیم شده‌اند.

یک مثال، Passing by Reference است. تصور کنید دو Thread یک شیء Person را با مرجع به یک متد منتقل می‌کنند که آن را تغییر می‌دهد. یک Side Effect این است که مگر اینکه مکانیزم Lock صحیحی وجود داشته باشد، هر Thread می‌تواند شیء Person Thread دیگر را تغییر دهد و داده‌ها را نامعتبر کند. نمونه‌ای از Mutable Objects را در فصل ۸، Threading and Concurrency دیدید.


این پایان بررسی Application-Level Code Smells بود. حالا به Class-Level Code Smells می‌پردازیم. 🏷️


Class-Level Code Smells 🏛️

Class-Level Code Smells مشکلاتی هستند که به صورت محلی در یک کلاس رخ می‌دهند. مشکلات رایج شامل مواردی مانند:

هدف شما هنگام نوشتن یک کلاس این است که آن را کوچک و کاربردی نگه دارید. متدها باید واقعاً در کلاس باشند و کوچک باشند. فقط کاری را در کلاس انجام دهید که لازم است – نه بیشتر و نه کمتر.

سعی کنید Dependency کلاس را حذف کنید و کلاس‌ها را قابل تست بسازید. کدهایی که باید در جای دیگری باشند، به مکان مناسب منتقل کنید. در این بخش، به Class-Level Code Smells و روش بازسازی آن‌ها می‌پردازیم، با شروع از Cyclomatic Complexity.


Cyclomatic Complexity 🔄

وقتی یک کلاس شامل تعداد زیادی Branch و Loop باشد، Cyclomatic Complexity آن افزایش می‌یابد.

هدف بازسازی کاهش Cyclomatic Complexity به بازه ۱-۱۰ است. ابتدا با جایگزینی Switch و If شروع کنید.

جایگزینی Switch Statement با Factory Pattern 🏭

در این بخش خواهید دید چگونه یک Switch Statement را با Factory Pattern جایگزین کنیم.

ابتدا نیاز به یک Enum برای گزارش‌ها داریم:

[Flags]
public enum Report
{
    StaffShiftPattern,
    EndofMonthSalaryRun,
    HrStarters,
    HrLeavers,
    EndofMonthSalesFigures,
    YearToDateSalesFigures
}

ویژگی [Flags] به ما امکان می‌دهد نام Enum را استخراج کنیم. Enum Report فهرستی از گزارش‌ها را ارائه می‌دهد.

حالا Switch Statement خود را اضافه می‌کنیم:

public void RunReport(Report report)
{
    switch (report)
    {
        case Report.EndofMonthSalaryRun:
            Console.WriteLine("Running End of Month Salary Run Report.");
            break;
        case Report.EndofMonthSalesFigures:
            Console.WriteLine("Running End of Month Sales Figures Report.");
            break;
        case Report.HrLeavers:
            Console.WriteLine("Running HR Leavers Report.");
            break;
        case Report.HrStarters:
            Console.WriteLine("Running HR Starters Report.");
            break;
        case Report.StaffShiftPattern:
            Console.WriteLine("Running Staff Shift Pattern Report.");
            break;
        case Report.YearToDateSalesFigures:
            Console.WriteLine("Running Year to Date Sales Figures Report.");
            break;
        default:
            Console.WriteLine("Report unrecognized.");
            break;
    }
}

متد ما یک Report می‌گیرد و سپس تصمیم می‌گیرد کدام گزارش اجرا شود.

وقتی من در سال ۱۹۹۹ به عنوان یک برنامه‌نویس VB6 تازه‌کار شروع کردم، مسئول ایجاد یک Report Generator از صفر برای شرکت‌هایی مانند Thomas Cook، ANZ، BNZ، Vodafone و چند شرکت بزرگ دیگر بودم.
گزارش‌های زیادی وجود داشت و من مسئول نوشتن یک Case Statement عظیم بودم که حتی از این نمونه بزرگ‌تر بود. اما سیستم من به خوبی کار می‌کرد. با این حال، با استانداردهای امروزی، روش‌های بهتری برای اجرای همین کد وجود دارد و من کارها را به شکل متفاوتی انجام می‌دادم. ⚡


حالا بیایید از Factory Method استفاده کنیم تا گزارش‌ها را بدون Switch Statement اجرا کنیم.

ابتدا یک فایل به نام IReportFactory اضافه کنید:

public interface IReportFactory
{
    void Run();
}

این Interface تنها یک متد به نام Run() دارد. این متد توسط کلاس‌های پیاده‌ساز برای اجرای گزارش‌ها استفاده می‌شود.

یک کلاس گزارش اضافه می‌کنیم به نام StaffShiftPatternReport که IReportFactory را پیاده‌سازی می‌کند:

public class StaffShiftPatternReport : IReportFactory
{
    public void Run()
    {
        Console.WriteLine("Running Staff Shift Pattern Report.");
    }
}

کلاس StaffShiftPatternReport Interface IReportFactory را پیاده‌سازی کرده و متد Run() پیام را روی صفحه چاپ می‌کند.

سپس یک کلاس ReportRunner اضافه می‌کنیم:

public class ReportRunner
{
    public void RunReport(Report report)
    {
        var reportName =
 $"CH13_CodeRefactoring.RefactoredCode.{report}Report, CH13_CodeRefactoring";
        var factory = Activator.CreateInstance(
            Type.GetType(reportName) ?? throw new InvalidOperationException()
        ) as IReportFactory;
        factory?.Run();
    }
}

کلاس ReportRunner شامل متدی به نام RunReport است که یک پارامتر از نوع Report می‌گیرد.
با توجه به اینکه Report یک Enum با ویژگی [Flags] است، می‌توانیم نام گزارش را بدست آوریم و از آن برای ساخت نام کلاس گزارش استفاده کنیم. سپس با استفاده از کلاس Activator، یک نمونه از کلاس گزارش ایجاد می‌کنیم. اگر reportName هنگام گرفتن نوع null باشد، یک InvalidOperationException پرتاب می‌شود. سپس Factory به نوع IReportFactory تبدیل می‌شود و متد Run() روی آن فراخوانی می‌شود تا گزارش تولید شود.

این کد قطعاً بسیار بهتر از یک Switch Statement طولانی است. ✅
در ادامه، یاد خواهیم گرفت چگونه خوانایی بررسی‌های شرطی داخل یک If Statement را بهبود دهیم.

بهبود خوانایی بررسی‌های شرطی در If Statement

استفاده از If Statement می‌تواند اصول Single Responsibility Principle (SRP) و Open/Closed Principle (OCP) را نقض کند. به مثال زیر توجه کنید:

public string GetHrReport(string reportName)
{
    if (reportName.Equals("Staff Joiners Report"))
        return "Staff Joiners Report";
    else if (reportName.Equals("Staff Leavers Report"))
        return "Staff Leavers Report";
    else if (reportName.Equals("Balance Sheet Report"))
        return "Balance Sheet Report";
}

متد GetHrReport() سه مسئولیت دارد: گزارش Staff Joiners، Staff Leavers و Balance Sheet. این باعث نقض SRP می‌شود، زیرا متد باید تنها با گزارش‌های HR سروکار داشته باشد و در عین حال، گزارش‌های HR و Finance را بازمی‌گرداند. همچنین طبق OCP، هر بار که یک گزارش جدید نیاز باشد، باید این متد را گسترش دهیم.

برای حل این مشکل، متد را بازسازی می‌کنیم تا دیگر نیازی به If Statement نباشد. ابتدا یک کلاس جدید به نام ReportBase ایجاد کنید:

public abstract class ReportBase
{
    public abstract void Print();
}

کلاس ReportBase یک کلاس انتزاعی با متد انتزاعی Print() است. سپس کلاس NewStartersReport را اضافه می‌کنیم که از ReportBase ارث می‌برد:

internal class NewStartersReport : ReportBase
{
    public override void Print()
    {
        Console.WriteLine("Printing New Starters Report.");
    }
}

کلاس NewStartersReport متد Print() را override می‌کند و پیام را روی صفحه چاپ می‌کند.

حالا کلاس LeaversReport را اضافه کنید:

public class LeaversReport : ReportBase
{
    public override void Print()
    {
        Console.WriteLine("Printing Leavers Report.");
    }
}

همانند کلاس قبلی، این کلاس Print() را override می‌کند.

برای استفاده:

ReportBase newStarters = new NewStartersReport();
newStarters.Print();

ReportBase leavers = new LeaversReport();
leavers.Print();

هر دو گزارش از ReportBase ارث می‌برند، بنابراین می‌توان آن‌ها را به متغیر ReportBase اختصاص داد و متد Print() فراخوانی شد. این کد اکنون مطابق با SRP و OCP است. ✅


Divergent Change 🔄

وقتی نیاز دارید یک تغییر در یک مکان ایجاد کنید و متوجه شوید که باید چند متد نامرتبط را تغییر دهید، این Divergent Change نامیده می‌شود. این تغییرات معمولاً در یک کلاس رخ می‌دهند و ناشی از ساختار ضعیف کلاس هستند. کپی و پیست کردن کد نیز از دلایل ایجاد این مشکل است.

راه حل: کد مشکل‌ساز را به کلاس جداگانه منتقل کنید. اگر رفتار و وضعیت بین کلاس‌ها مشترک است، از Inheritance با Base Classes و Subclasses استفاده کنید.

مزایای اصلاح مشکلات مرتبط با Divergent Change:


Downcasting ⬇️

وقتی یک کلاس پایه به یکی از کلاس‌های فرزند خود cast می‌شود، به آن Downcasting گفته می‌شود. این یک Code Smell است، زیرا کلاس پایه نباید درباره کلاس‌های فرزند خود بداند.

مثال: کلاس پایه Animal. هر نوع حیوان می‌تواند از کلاس پایه ارث ببرد. اما یک حیوان فقط می‌تواند یک نوع باشد. برای مثال، Felines گربه‌سان‌ها هستند و Canines سگ‌سان‌ها. غیرمنطقی است که یک Feline را به Canine تبدیل کنیم و برعکس.

در نتیجه، هیچ‌گاه نباید Downcasting انجام داد. اما Upcasting انواع حیوانات مانند Monkeys و Camels به نوع Animal صحیح است، زیرا همه آن‌ها نوعی حیوان هستند.


Excessive Literal Use 💬

استفاده بیش از حد از Literals می‌تواند باعث خطاهای برنامه‌نویسی شود، مثلاً اشتباه نوشتن یک رشته. بهتر است Literals را به متغیرهای Constant اختصاص دهید. رشته‌ها نیز باید در Resource Files برای Localization قرار بگیرند، به‌ویژه اگر نرم‌افزار در مکان‌های مختلف جهان مستقر شود.


Feature Envy 👀

وقتی یک متد زمان زیادی را صرف پردازش کد در کلاس‌های دیگر می‌کند، این Feature Envy نامیده می‌شود. مثال بعدی در کلاس Authorization خواهد بود، اما ابتدا کلاس Authentication را ببینیم:

public class Authentication
{
    private bool _isAuthenticated = false;

    public void Login(ICredentials credentials)
    {
        _isAuthenticated = true;
    }

    public void Logout()
    {
        _isAuthenticated = false;
    }

    public bool IsAuthenticated()
    {
        return _isAuthenticated;
    }
}

کلاس Authentication مسئول ورود و خروج کاربران و تعیین وضعیت احراز هویت است.

سپس کلاس Authorization را اضافه می‌کنیم:

public class Authorization
{
    private Authentication _authentication;

    public Authorization(Authentication authentication)
    {
        _authentication = authentication;
    }

    public void Login(ICredentials credentials)
    {
        _authentication.Login(credentials);
    }

    public void Logout()
    {
        _authentication.Logout();
    }

    public bool IsAuthenticated()
    {
        return _authentication.IsAuthenticated();
    }

    public bool IsAuthorized(string role)
    {
        return IsAuthenticated() && role.Contains("Administrator");
    }
}

همان‌طور که مشاهده می‌کنید، کلاس Authorization بیش از حد کار انجام می‌دهد. برخی متدها فقط متدهای همان کلاس Authentication را فراخوانی می‌کنند که نمونه‌ای از Feature Envy است.

نسخه اصلاح‌شده کلاس Authorization:

public class Authorization
{
    private ProblemCode.Authentication _authentication;

    public Authorization(ProblemCode.Authentication authentication)
    {
        _authentication = authentication;
    }

    public bool IsAuthorized(string role)
    {
        return _authentication.IsAuthenticated() && role.Contains("Administrator");
    }
}

اکنون کلاس Authorization کوچک‌تر است و تنها کار لازم را انجام می‌دهد. دیگر Feature Envy وجود ندارد. ✅


در مرحله بعد، به Inappropriate Intimacy می‌پردازیم. 💡

Inappropriate Intimacy 🤝

یک کلاس زمانی دچار Inappropriate Intimacy می‌شود که بیش از حد به جزئیات پیاده‌سازی کلاس دیگری وابسته باشد.

کلاس‌ها نباید به یکدیگر وابسته باشند، زیرا این باعث Coupling می‌شود و می‌تواند Cohesion را کاهش دهد. یک کلاس باید تا حد امکان Self-contained باشد و از دیگر کلاس‌ها کمترین اطلاعات را داشته باشد.


Indecent Exposure ⚠️

زمانی که یک کلاس جزئیات داخلی خود را آشکار می‌کند، این Indecent Exposure نامیده می‌شود. این موضوع اصل Encapsulation در OOP را نقض می‌کند.

مقادیر داده‌ای نباید Public باشند. آن‌ها باید Private باشند و تنها از طریق Constructors، Methods و Properties قابل تغییر و از طریق Properties قابل دسترسی باشند.


The Large Class (God Object) 🏰

کلاس بزرگ، یا همان God Object، همه‌چیز برای همه بخش‌های سیستم است. این یک کلاس حجیم و غیرقابل مدیریت است که بیش از حد کار انجام می‌دهد.

ویژگی‌های یک کلاس خوب:

برای کوچک و متمرکز نگه داشتن کلاس‌ها:


The Lazy Class (Freeloader / Lazy Object) 🛋️

یک کلاس Freeloading، کلاس‌هایی هستند که تقریباً هیچ کاری مفید انجام نمی‌دهند.


The Middleman Class 🧑‍💼

یک کلاس Middleman تنها عملکرد را به اشیای دیگر واگذار می‌کند.


The Orphan Class of Variables and Constants 📦

داشتن یک کلاس تنها برای نگهداری Variables و Constants در بخش‌های مختلف برنامه، کار مناسبی نیست.


Primitive Obsession 🧩

کدی که از مقادیر Primitive به جای اشیا برای مقاصدی مانند Range Values و رشته‌های قالب‌بندی شده (مثلاً کارت اعتباری، کد پستی، شماره تلفن) استفاده می‌کند، دچار Primitive Obsession است.


Refused Bequest ❌

زمانی که یک کلاس از کلاس دیگری ارث می‌برد اما از همه متدهای آن استفاده نمی‌کند، این Refused Bequest نامیده می‌شود.


Speculative Generality 🔮

کلاسی که با عملکردی برنامه‌نویسی شده که اکنون نیاز نیست اما ممکن است در آینده نیاز شود، دچار Speculative Generality است.


Tell, Don’t Ask 🗣️

اصل Tell, Don’t Ask به ما می‌گوید که داده‌ها باید با متدهایی که روی آن داده‌ها عمل می‌کنند، Bundle شوند.

اگر اشیائی وجود دارند که منطق دارند و از اشیاء دیگر داده می‌گیرند، منطق و داده‌ها را در یک کلاس ترکیب کنید.

Temporary Fields ⏳

Temporary Fields متغیرهای عضوی هستند که برای کل طول عمر یک شیء نیاز نیستند.


Method-level Smells 🛠️

Method-level code smells مشکلاتی هستند که در خود متد وجود دارند. متدها نیرو محرکه نرم‌افزار هستند و می‌توانند عملکرد آن را خوب یا ضعیف کنند.


The Black Sheep Method 🐑

یک متد Black Sheep در میان متدهای یک کلاس، به طور قابل توجهی متفاوت است.


Cyclomatic Complexity 🔄

زمانی که یک متد تعداد زیادی حلقه و شاخه دارد، به آن Cyclomatic Complexity گفته می‌شود.


Contrived Complexity 🤯

اگر یک متد به‌طور غیرضروری پیچیده باشد و قابلیت ساده‌سازی داشته باشد، این پیچیدگی Contrived Complexity نامیده می‌شود.


Dead Code ⚰️

متدی که وجود دارد اما استفاده نمی‌شود، Dead Code است.


Excessive Data Return 📦

زمانی که یک متد بیش از حد داده برمی‌گرداند، به گونه‌ای که تمام کلاینت‌ها به آن نیاز ندارند، این مشکل Excessive Data Return نامیده می‌شود.


Feature Envy 👀

متدی که زمان زیادی را صرف دسترسی به داده‌ها در اشیاء دیگر می‌کند، دارای Feature Envy است.


Identifier Size 🏷️

Identifiers می‌توانند بسیار کوتاه یا طولانی باشند.


Inappropriate Intimacy 🤝

متدهایی که بیش از حد به جزئیات پیاده‌سازی در متدها یا کلاس‌های دیگر وابسته هستند، دچار Inappropriate Intimacy هستند.


Long Lines (God Lines) 📏

خطوط طولانی کد خواندن و درک را سخت می‌کنند.


Lazy Methods 💤

یک متد Lazy کار بسیار کمی انجام می‌دهد.


Long Methods (God Methods) 📚

یک متد Long از حد خود فراتر رفته است.

Long Parameter Lists (Too Many Parameters) 📋


Message Chains 🔗


Middleman Method 🧩


Oddball Solutions 🎯


Speculative Generality 🔮


Summary 📚

در این فصل با انواع Code Smells آشنا شدیم و روش‌های رفع آن‌ها از طریق Refactoring را بررسی کردیم.

سه دسته اصلی Code Smells:

  1. Application-level code smells: بر تمام لایه‌های برنامه تأثیر دارند.
  2. Class-level code smells: در سطح کلاس مشکل ایجاد می‌کنند.
  3. Method-level code smells: تنها متدها را تحت تأثیر قرار می‌دهند.

Application-level code smells شامل:

Class-level code smells شامل:

Method-level code smells شامل:

در فصل بعد، بررسی Refactoring کد را با استفاده از ReSharper ادامه خواهیم داد.


Questions ❓

  1. سه دسته اصلی Code Smell کدامند؟
  2. انواع Application-level Code Smells را نام ببرید.
  3. انواع Class-level Code Smells را نام ببرید.
  4. انواع Method-level Code Smells را نام ببرید.
  5. چه نوع بازسازی‌هایی می‌توان برای رفع Code Smells انجام داد؟
  6. Cyclomatic Complexity چیست؟
  7. چگونه می‌توان Cyclomatic Complexity را کاهش داد؟
  8. Contrived Complexity چیست؟
  9. چگونه می‌توان Contrived Complexity را رفع کرد؟
  10. Combinatorial Explosion چیست؟
  11. چگونه می‌توان Combinatorial Explosion را رفع کرد؟
  12. هنگام مواجهه با Deodorant Comments چه باید کرد؟
  13. اگر کد بد دارید و نمی‌دانید چگونه آن را اصلاح کنید، چه باید کرد؟
  14. بهترین مکان برای پرسش و پاسخ درباره مسائل برنامه‌نویسی کجاست؟
  15. چگونه می‌توان Long Parameter List را کاهش داد؟
  16. چگونه می‌توان یک Large Method را بازسازی کرد؟
  17. حداکثر طول یک متد تمیز چقدر است؟
  18. Cyclomatic Complexity برنامه باید در چه بازه‌ای باشد؟
  19. مقدار ایده‌آل برای Depth of Inheritance چقدر است؟
  20. Speculative Generality چیست و چه باید کرد؟
  21. هنگام مواجهه با Oddball Solution چه اقدامی باید انجام داد؟
  22. هنگام مواجهه با Temporary Field چه بازسازی‌هایی انجام می‌دهید؟
  23. Data Clump چیست و چه باید کرد؟
  24. کد بد Refused Bequest را توضیح دهید.
  25. Message Chains چه قانونی را نقض می‌کنند؟
  26. چگونه باید Message Chains را بازسازی کرد؟
  27. Feature Envy چیست؟
  28. چگونه می‌توان Feature Envy را حذف کرد؟
  29. از چه الگویی می‌توان برای جایگزینی Switch Statements که اشیاء برمی‌گردانند، استفاده کرد؟
  30. چگونه می‌توان If Statements که اشیاء برمی‌گردانند را بازسازی کرد؟
  31. Solution Sprawl چیست و چگونه باید رفع شود؟
  32. اصل Tell, Don’t Ask! را توضیح دهید.
  33. اصل Tell, Don’t Ask! چگونه نقض می‌شود؟
  34. علائم Shotgun Surgery چیست و چگونه باید رفع شود؟
  35. Lost Intent چیست و چه باید کرد؟
  36. چگونه حلقه‌ها را بازسازی کنیم و چه فوایدی دارد؟
  37. Divergent Change چیست و چگونه بازسازی می‌شود؟

مطالعه بیشتر 📖: