فصل سیزدهم: بازآرایی کدهای C# – شناسایی Code Smellها 🎯
شناسایی Code Smellها
در این فصل، ما به بررسی کدهای مشکلدار و روش بازآرایی (Refactoring) آنها میپردازیم. در صنعت نرمافزار، به چنین کدهایی اصطلاحاً Code Smell گفته میشود. این نوع کدها کامپایل میشوند، اجرا میشوند و همان کاری را انجام میدهند که قرار است انجام دهند، اما مشکل آنها این است که به مرور زمان خوانایی خود را از دست میدهند، پیچیده میشوند و نگهداری و توسعه آنها دشوار میگردد.
چنین کدهایی باید در اولین فرصت بازآرایی شوند. در غیر این صورت، این مشکل بهعنوان بدهی فنی (Technical Debt) باقی میماند و در طولانیمدت میتواند پروژه را به نقطه شکست برساند، بهطوری که مجبور شوید کل برنامه را از ابتدا طراحی و پیادهسازی کنید که هزینهبر خواهد بود.
بازآرایی (Refactoring) چیست؟
بازآرایی فرایند بازنویسی کد موجودی است که کار میکند، اما هدف این است که کد تمیزتر و بهتر شود. همانطور که پیشتر دیدید، کد تمیز (Clean Code) بهراحتی خوانده میشود، نگهداری میشود و توسعه پیدا میکند.
در این فصل به موارد زیر خواهیم پرداخت:
- شناسایی Code Smellهای سطح برنامه (Application-level) و روشهای رفع آنها
- شناسایی Code Smellهای سطح کلاس (Class-level) و روشهای رفع آنها
- شناسایی Code Smellهای سطح متد (Method-level) و روشهای رفع آنها
پس از مطالعه این فصل، مهارتهای زیر را به دست خواهید آورد:
- توانایی شناسایی انواع مختلف Code Smellها
- درک این موضوع که چرا کد بهعنوان Code Smell طبقهبندی شده است
- توانایی بازآرایی Code Smellها و تبدیل آنها به کد تمیز
ما بررسی خود را با Code Smellهای سطح برنامه آغاز میکنیم.
الزامات فنی 🛠️
برای این فصل به ابزارها و پیشنیازهای زیر احتیاج دارید:
- Visual Studio 2019
- PostSharp
- کدهای این فصل از طریق این لینک در دسترس است:
https://github.com/PacktPublishing/Clean-Code-in-C-/tree/master/CH13
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 تولید کنید.
از این ابزارها برای پیدا کردن بخشهای مشکلدار در کد استفاده کنید و روی آنها تمرکز کنید:
- پیچیدگی حلقوی را به ۱۰ یا کمتر کاهش دهید.
- عمق ارثبری تمام اشیا را به حداکثر ۱ برسانید.
- هر کلاس فقط وظایفی را انجام دهد که برای آن طراحی شده است.
- متدها را کوچک نگه دارید (بهطور متوسط حدود ۱۰ خط کد).
- لیستهای طولانی پارامترها را با اشیای پارامتر جایگزین کنید.
- اگر پارامترهای خروجی زیادی دارید، متد را طوری بازنویسی کنید که یک Tuple یا یک شیء برگرداند.
- در موارد چندنخی (Multithreading)، اطمینان حاصل کنید که کد Thread-safe باشد.
- از اشیای Immutable به جای Mutable استفاده کنید.
همچنین به آیکونهای Quick Tips توجه کنید. این آیکونها معمولاً پیشنهادهای Refactoring یککلیکی برای خط کد موردنظر ارائه میدهند. توصیه میکنم از آنها استفاده کنید.
📦 توده داده (Data Clump)
توده داده زمانی رخ میدهد که فیلدهای مشابه را در کلاسهای مختلف و لیست پارامترها میبینید که معمولاً الگوی نامگذاری مشابهی دارند. این معمولاً نشانهی این است که یک کلاس در سیستم وجود ندارد. کاهش پیچیدگی سیستم با شناسایی کلاس گمشده و عمومیسازی آن بهدست میآید.
از کوچک بودن کلاس نترسید و هیچوقت فکر نکنید که یک کلاس کوچک بیاهمیت است. اگر برای سادهسازی کد به یک کلاس نیاز دارید، آن را اضافه کنید. ✨
کامنتهای خوشبوکننده (Deodorant Comments) 📝
وقتی یک کامنت با جملات زیبا و مثبت سعی دارد کدی ضعیف یا بد را توجیه کند، به آن کامنت خوشبوکننده گفته میشود. ❌ اگر کد بد است، باید بازآرایی (Refactor) شود تا خوب شود و سپس کامنت حذف شود. اگر نمیدانید چگونه بازآرایی کنید، از دیگران کمک بگیرید. اگر کسی برای کمک در دسترس نیست، کد خود را در Stack Overflow قرار دهید. برنامهنویسان بسیار ماهری در آن سایت هستند که میتوانند به شما کمک کنند، فقط قوانین انتشار را رعایت کنید!
کد تکراری (Duplicate Code) 🔁
کد تکراری، کدی است که بیش از یکبار در پروژه ظاهر شده است. مشکلاتی که از تکرار کد بهوجود میآیند:
-
هزینه نگهداری بالا: هر بار که اشکالی در کد رفع میکنید، زمان و هزینه صرف میشود.
- یک باگ = هزینه × ۱
- همان باگ در ۱۰ جای دیگر = هزینه × ۱۰
-
خستگی و ملال برنامهنویس هنگام اصلاح یک مشکل در چند نقطه مختلف.
-
احتمال از قلم افتادن برخی از تکرارها در زمان رفع اشکال.
راهحل چیست؟ بازآرایی کد برای حذف تکرارها. سادهترین روش، انتقال کد به یک کلاس جدید قابلاستفاده مجدد در پروژه است.
مزیت دیگر این است که میتوانید کدهای قابلاستفاده مجدد را در یک کتابخانه (Class Library) قرار دهید تا سایر پروژهها هم از آن بهره ببرند.
امروزه بهترین انتخاب، استفاده از کتابخانههای .NET Standard است، چون این کتابخانهها در تمامی پلتفرمها در دسترساند: Windows، Linux، macOS، iOS و Android.
روشهای دیگر برای کاهش کدهای تکراری:
- برنامهنویسی جنبهگرا (Aspect-Oriented Programming – AOP): در این روش، کدهای تکراری به یک Aspect منتقل میشوند و هنگام کامپایل به کد اصلی اضافه میگردند. در نتیجه، متد فقط شامل منطق تجاری است و کدهای جانبی پنهان میمانند.
- الگوی دکوراتور (Decorator Pattern): همانطور که در فصل قبل دیدید، این الگو میتواند عملیات یک کلاس را تزئین کند و امکان اضافه کردن رفتار جدید بدون تغییر در کد اصلی را فراهم میکند. مثال ساده: پیچیدن عملیات در یک بلوک try/catch که در فصل 11 توضیح داده شد.
از دست رفتن هدف یا نیت (Lost Intent) ❓
وقتی نتوانید بهراحتی هدف یا مقصود کد را درک کنید، یعنی نیت کد از بین رفته است.
گامها برای رفع این مشکل:
-
بررسی فضای نام (Namespace) و نام کلاس: باید هدف کلاس را نشان دهد.
-
بررسی محتوای کلاس: بهدنبال کدی باشید که در جای نامناسبی قرار دارد. آنها را شناسایی و به جای درست منتقل کنید.
-
بررسی متدها:
- آیا هر متد فقط یک کار را بهخوبی انجام میدهد یا چند کار را نهچندان خوب؟
- اگر پاسخ دوم است، بازآرایی کنید.
- در متدهای بزرگ، کدهای قابلاستخراج را به متدهای کوچکتر منتقل کنید.
هدف نهایی: کد کلاس باید مثل یک کتاب خوانا باشد. بازآرایی کنید تا نیت کد شفاف شود و هر کلاس فقط کاری را انجام دهد که برای آن طراحی شده است.
نکته: ابزارهای معرفیشده در فصل 12 (استفاده از ابزارها برای بهبود کیفیت کد) را فراموش نکنید.
مبحث بعدی: بوی بد کد – تغییرات مکرر متغیرها (Mutation of Variables) خواهد بود.
تغییرات مکرر متغیرها (The Mutation of Variables) 🔄
تغییرات مکرر متغیرها یعنی متغیرها بهگونهای هستند که فهمیدن و استدلال دربارهی آنها دشوار است. این باعث میشود که بازآرایی (Refactor) آنها سخت شود.
یک متغیر قابل تغییر (Mutable Variable)، متغیری است که چندین بار توسط عملیات مختلف تغییر میکند. این مسئله استدلال دربارهی دلیل تغییر مقدار متغیر را دشوار میکند. علاوه بر این، چون متغیر توسط عملیات مختلف تغییر میکند، استخراج بخشهایی از کد به متدهای کوچکتر و خواناتر نیز سختتر میشود. همچنین، متغیرهای قابل تغییر ممکن است نیازمند بررسیهای بیشتری باشند که پیچیدگی کد را افزایش میدهد.
راهکارها
- بخشهای کوچک کد را به متدهای جداگانه استخراج کنید.
- اگر کد شامل انشعابها و حلقههای متعدد است، ببینید آیا راه سادهتری برای کاهش پیچیدگی وجود دارد یا خیر.
- اگر از چندین مقدار out استفاده میکنید، در نظر بگیرید که یک شیء یا Tuple برگردانید.
- هدف این است که قابلیت تغییر متغیر را کاهش دهید تا بتوان راحتتر دربارهی آن استدلال کرد، بدانید مقدار متغیر از کجا میآید و چرا آن مقدار را دارد.
- هرچه متد کوچکتر باشد، تشخیص محل و دلیل تنظیم متغیر آسانتر است.
مثال عملی
[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) بهره میبرد.
همانطور که میبینید:
- هیچ حلقهای وجود ندارد
- هیچ متغیر محلی در حال تغییر نیست
با کامپایل و اجرای برنامه، خروجی مشابه نسخه قبلی خواهد بود، اما کد سادهتر، خواناتر و بدون تغییر مکرر متغیر است.
هر دو نسخه کد خروجی یکسانی تولید میکنند. ✅
شما متوجه خواهید شد که هر دو نسخه کد، [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، رابطهای مختلف سیستم را متحد کنید. 🔗
کلاس 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()
را فراخوانی کرده و کد را اجرا کنید، خروجی زیر را مشاهده خواهید کرد: 🎯
همانطور که در تصویر مشاهده میکنید، متدی که اجرا میشود 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 مشکلاتی هستند که به صورت محلی در یک کلاس رخ میدهند. مشکلات رایج شامل مواردی مانند:
- Cyclomatic Complexity و عمق ارثبری
- High Coupling
- Low Cohesion
هدف شما هنگام نوشتن یک کلاس این است که آن را کوچک و کاربردی نگه دارید. متدها باید واقعاً در کلاس باشند و کوچک باشند. فقط کاری را در کلاس انجام دهید که لازم است – نه بیشتر و نه کمتر.
سعی کنید 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 باشند، عمومی شوند.
- همه پیادهسازیهای دیگر که نیازی به عمومی بودن ندارند، با استفاده از Access Modifiers مناسب مخفی شوند.
مقادیر دادهای نباید Public باشند. آنها باید Private باشند و تنها از طریق Constructors، Methods و Properties قابل تغییر و از طریق Properties قابل دسترسی باشند.
The Large Class (God Object) 🏰
کلاس بزرگ، یا همان God Object، همهچیز برای همه بخشهای سیستم است. این یک کلاس حجیم و غیرقابل مدیریت است که بیش از حد کار انجام میدهد.
- حتی اگر نام کلاس و فضای نام آن واضح باشد، هنگام مطالعه کد، هدف کلاس ممکن است گم شود.
ویژگیهای یک کلاس خوب:
- نام کلاس نشاندهنده هدف آن باشد و در Namespace مناسب قرار گیرد.
- محتوای کلاس مطابق استانداردهای کدنویسی شرکت باشد.
- متدها تا حد امکان کوچک باشند و پارامترهای متد حداقل باشد.
- تنها متدهایی که متعلق به کلاس هستند، در آن قرار گیرند.
- متغیرها، Propertyها و متدهایی که متعلق به کلاس نیستند، حذف و در فایل و فضای نام مناسب قرار گیرند.
برای کوچک و متمرکز نگه داشتن کلاسها:
- اگر نیازی به Inheritance نیست، از آن استفاده نکنید.
- اگر کلاس پنج متد دارد و تنها یک متد استفاده میشود، آن متد را به کلاس Reusable منتقل کنید.
- Single Responsibility Principle را رعایت کنید: یک کلاس تنها یک مسئولیت داشته باشد.
The Lazy Class (Freeloader / Lazy Object) 🛋️
یک کلاس Freeloading، کلاسهایی هستند که تقریباً هیچ کاری مفید انجام نمیدهند.
- هنگام مواجهه با چنین کلاسهایی، میتوانید محتویات آنها را با کلاسهای دیگر که هدف مشابهی دارند، ادغام کنید.
- همچنین میتوانید سلسلهمراتب ارثبری را کاهش دهید. عمق ایدهآل ارثبری ۱ است.
- برای کلاسهای خیلی کوچک، Inline Class را در نظر بگیرید.
The Middleman Class 🧑💼
یک کلاس Middleman تنها عملکرد را به اشیای دیگر واگذار میکند.
- در چنین مواردی میتوان Middleman را حذف کرد و مستقیماً با اشیایی که مسئولیت را انجام میدهند، کار کرد.
- اگر نمیتوان کلاس را حذف کرد، آن را با کلاسهای موجود ادغام کنید و طراحی کلی را بررسی کنید تا تعداد کلاسها و کد کاهش یابد.
The Orphan Class of Variables and Constants 📦
داشتن یک کلاس تنها برای نگهداری Variables و Constants در بخشهای مختلف برنامه، کار مناسبی نیست.
- چنین کلاسهایی باعث میشوند متغیرها معنای واقعی خود را از دست بدهند.
- بهتر است Constants و Variables به بخشهایی منتقل شوند که استفاده میشوند.
- اگر توسط چند کلاس استفاده میشوند، در فایل مربوط به Root Namespace قرار گیرند.
Primitive Obsession 🧩
کدی که از مقادیر Primitive به جای اشیا برای مقاصدی مانند Range Values و رشتههای قالببندی شده (مثلاً کارت اعتباری، کد پستی، شماره تلفن) استفاده میکند، دچار Primitive Obsession است.
- نشانه دیگر: استفاده از Constants برای نام فیلدها یا ذخیره نامناسب اطلاعات در Constants.
Refused Bequest ❌
زمانی که یک کلاس از کلاس دیگری ارث میبرد اما از همه متدهای آن استفاده نمیکند، این Refused Bequest نامیده میشود.
- دلیل رایج: زیرکلاس کاملاً متفاوت از کلاس پایه است.
- مثال: کلاس پایه Building توسط انواع مختلف ساختمان استفاده میشود، اما یک شی Car از Building ارث میبرد. واضح است که این اشتباه است.
- راه حل: بررسی کنید که آیا کلاس پایه واقعاً لازم است یا خیر. اگر لازم است، آن را ایجاد و ارثبری کنید؛ در غیر این صورت، عملکرد مورد نظر را به کلاس مناسب اضافه کنید.
Speculative Generality 🔮
کلاسی که با عملکردی برنامهنویسی شده که اکنون نیاز نیست اما ممکن است در آینده نیاز شود، دچار Speculative Generality است.
- این کد در واقع Dead Code است و باعث افزایش بار نگهداری و کدهای اضافی میشود.
- بهتر است چنین کلاسهایی حذف شوند.
Tell, Don’t Ask 🗣️
اصل Tell, Don’t Ask به ما میگوید که دادهها باید با متدهایی که روی آن دادهها عمل میکنند، Bundle شوند.
- اشیاء نباید داده بخواهند و سپس عملیات انجام دهند.
- آنها باید منطق شیء را برای انجام کار روی دادههای خودش Tell کنند.
اگر اشیائی وجود دارند که منطق دارند و از اشیاء دیگر داده میگیرند، منطق و دادهها را در یک کلاس ترکیب کنید.
Temporary Fields ⏳
Temporary Fields متغیرهای عضوی هستند که برای کل طول عمر یک شیء نیاز نیستند.
- میتوان با بازسازی کد، این متغیرهای موقت و متدهایی که روی آنها عمل میکنند را به کلاس جداگانه منتقل کرد.
- نتیجه، کدی واضحتر و سازمانیافتهتر خواهد بود.
Method-level Smells 🛠️
Method-level code smells مشکلاتی هستند که در خود متد وجود دارند. متدها نیرو محرکه نرمافزار هستند و میتوانند عملکرد آن را خوب یا ضعیف کنند.
- متدها باید سازماندهیشده باشند و فقط کاری را انجام دهند که انتظار میرود—نه بیشتر و نه کمتر.
- مهم است که انواع مشکلات ناشی از متدهای ضعیف را بشناسیم و بدانیم چگونه آنها را اصلاح کنیم.
The Black Sheep Method 🐑
یک متد Black Sheep در میان متدهای یک کلاس، به طور قابل توجهی متفاوت است.
-
هنگام مواجهه با چنین متدی، باید آن را عینی بررسی کنید:
- نام متد چیست؟
- هدف متد چیست؟
-
سپس تصمیم بگیرید که متد را حذف و آن را در جایی که واقعاً متعلق است، قرار دهید.
Cyclomatic Complexity 🔄
زمانی که یک متد تعداد زیادی حلقه و شاخه دارد، به آن Cyclomatic Complexity گفته میشود.
- این مشکل همچنین یک Class-level code smell است.
- برای کاهش مشکلات شاخهها میتوان Switch و If Statements را بازسازی کرد.
- حلقهها را میتوان با LINQ جایگزین کرد که علاوه بر خوانایی، ویژگی Functional بودن نیز دارد.
Contrived Complexity 🤯
اگر یک متد بهطور غیرضروری پیچیده باشد و قابلیت سادهسازی داشته باشد، این پیچیدگی Contrived Complexity نامیده میشود.
- متد را ساده کنید تا محتوای آن برای انسان خوانا و قابل فهم باشد.
- سپس متد را بازسازی و اندازه آن را به حداقل خطوط عملیاتی کاهش دهید.
Dead Code ⚰️
متدی که وجود دارد اما استفاده نمیشود، Dead Code است.
- این شامل Constructors، Properties، Parameters و Variables نیز میشود.
- باید شناسایی و حذف شوند.
Excessive Data Return 📦
زمانی که یک متد بیش از حد داده برمیگرداند، به گونهای که تمام کلاینتها به آن نیاز ندارند، این مشکل Excessive Data Return نامیده میشود.
- تنها دادههای مورد نیاز باید برگردانده شوند.
- اگر گروههای مختلفی با نیازهای متفاوت وجود دارند، بهتر است متدهای جداگانه بنویسید که تنها دادههای مورد نیاز هر گروه را برگردانند.
Feature Envy 👀
متدی که زمان زیادی را صرف دسترسی به دادهها در اشیاء دیگر میکند، دارای Feature Envy است.
- متد باید کوچک باشد و عملکرد اصلی آن در همان متد متمرکز باشد.
- اگر کار بیشتری در متدهای دیگر انجام میدهد، باید بخشی از کد به متد جداگانه منتقل شود.
Identifier Size 🏷️
Identifiers میتوانند بسیار کوتاه یا طولانی باشند.
-
باید توصیفی و مختصر باشند.
-
هنگام نامگذاری متغیرها، Context و مکان آن را در نظر بگیرید:
- در یک حلقه محلی، یک حرف ممکن است کافی باشد.
- در سطح کلاس، نام قابل فهم انسانی لازم است.
-
از نامهای مبهم یا فاقد Context خودداری کنید.
Inappropriate Intimacy 🤝
متدهایی که بیش از حد به جزئیات پیادهسازی در متدها یا کلاسهای دیگر وابسته هستند، دچار Inappropriate Intimacy هستند.
- این متدها باید بازسازی شوند و در صورت نیاز حذف شوند.
- متدها و فیلدها را به جایی منتقل کنید که واقعاً نیاز به استفاده دارند یا به کلاس جداگانهای استخراج کنید.
- Inheritance میتواند جایگزین Delegation شود زمانی که زیرکلاس به کلاس پایه وابسته است.
Long Lines (God Lines) 📏
خطوط طولانی کد خواندن و درک را سخت میکنند.
- این باعث سختی دیباگ و بازسازی کد میشود.
- در صورت امکان، خطوط را به گونهای فرمت کنید که هر Period یا کد بعد از Comma در خط جدید قرار گیرد.
- همچنین باید کد را بازسازی کنید تا کوتاهتر و خواناتر شود.
Lazy Methods 💤
یک متد Lazy کار بسیار کمی انجام میدهد.
- ممکن است کار خود را به متدهای دیگر واگذار کند یا تنها متدی در کلاس دیگری را فراخوانی کند.
- در چنین مواردی، ممکن است بهتر باشد متد حذف و کد مورد نیاز در جای مناسب قرار گیرد.
- میتوان از Inline Function مانند Lambda نیز استفاده کرد.
Long Methods (God Methods) 📚
یک متد Long از حد خود فراتر رفته است.
- این متدها ممکن است هدف خود را از دست بدهند و کارهای بیشتری از انتظار انجام دهند.
- با IDE میتوان بخشهایی از متد را انتخاب و با Extract Method یا Extract Class به متد یا کلاس جداگانه منتقل کرد.
- یک متد باید تنها مسئول انجام یک وظیفه باشد.
Long Parameter Lists (Too Many Parameters) 📋
- سه پارامتر یا بیشتر به عنوان Long Parameter List شناخته میشوند.
- برای رفع این مشکل میتوان پارامترها را با یک Method Call جایگزین کرد یا از یک Parameter Object استفاده کرد.
Message Chains 🔗
- Message Chain زمانی رخ میدهد که یک متد، شیئی را فراخوانی میکند که آن هم شیء دیگری را فراخوانی میکند و این زنجیره ادامه مییابد.
- این زنجیرهها قانون Law of Demeter را میشکنند: یک کلاس باید تنها با همسایه نزدیک خود ارتباط برقرار کند.
- برای رفع مشکل، وضعیت و رفتار مورد نیاز را به نزدیکترین محل استفاده منتقل کنید.
Middleman Method 🧩
- وقتی متدی تنها کار را به متدهای دیگر واگذار میکند، به آن Middleman Method گفته میشود.
- چنین متدی میتواند بازسازی و حذف شود.
- اگر بخشی از عملکرد ضروری است، آن را با جایی که استفاده میشود ادغام کنید.
Oddball Solutions 🎯
- وقتی چند متد یک کار را انجام میدهند اما هر کدام روش متفاوتی دارند، Oddball Solution رخ داده است.
- بهترین متد را انتخاب کنید و فراخوانیهای متدهای دیگر را با آن جایگزین کنید، سپس متدهای اضافی را حذف کنید.
- در نهایت، تنها یک متد برای انجام کار باقی میماند که قابل استفاده مجدد است.
Speculative Generality 🔮
- متدی که در هیچجا استفاده نمیشود، Speculative Generality دارد.
- اساساً این کد Dead Code است و باید از سیستم حذف شود.
- این نوع کد باعث افزایش هزینه نگهداری و افزایش حجم کد میشود.
Summary 📚
در این فصل با انواع Code Smells آشنا شدیم و روشهای رفع آنها از طریق Refactoring را بررسی کردیم.
سه دسته اصلی Code Smells:
- Application-level code smells: بر تمام لایههای برنامه تأثیر دارند.
- Class-level code smells: در سطح کلاس مشکل ایجاد میکنند.
- Method-level code smells: تنها متدها را تحت تأثیر قرار میدهند.
Application-level code smells شامل:
- Boolean Blindness
- Combinatorial Explosion
- Contrived Complexity
- Data Clump
- Deodorant Comments
- Duplicate Code
- Lost Intent
- Mutation of Variables
- Oddball Solutions
- Shotgun Surgery
- Solution Sprawl
- Uncontrolled Side Effects
Class-level code smells شامل:
- Cyclomatic Complexity
- Divergent Change
- Downcasting
- Excessive Literal Use
- Feature Envy
- Inappropriate Intimacy
- Indecent Exposure
- Large Object (God Object)
- Lazy Class / Freeloader / Lazy Object
- Middleman
- Orphan Class of Variables and Constants
- Primitive Obsession
- Refused Bequest
- Speculative Generality
- Tell, Don’t Ask
- Temporary Fields
Method-level code smells شامل:
- Black Sheep
- Cyclomatic Complexity
- Contrived Complexity
- Dead Code
- Feature Envy
- Identifier Size
- Inappropriate Intimacy
- Long Lines (God Lines)
- Lazy Method
- Long Method (God Method)
- Long Parameter List (Too Many Parameters)
- Message Chains
- Middleman
- Oddball Solutions
- Speculative Generality
در فصل بعد، بررسی Refactoring کد را با استفاده از ReSharper ادامه خواهیم داد.
Questions ❓
- سه دسته اصلی Code Smell کدامند؟
- انواع Application-level Code Smells را نام ببرید.
- انواع Class-level Code Smells را نام ببرید.
- انواع Method-level Code Smells را نام ببرید.
- چه نوع بازسازیهایی میتوان برای رفع Code Smells انجام داد؟
- Cyclomatic Complexity چیست؟
- چگونه میتوان Cyclomatic Complexity را کاهش داد؟
- Contrived Complexity چیست؟
- چگونه میتوان Contrived Complexity را رفع کرد؟
- Combinatorial Explosion چیست؟
- چگونه میتوان Combinatorial Explosion را رفع کرد؟
- هنگام مواجهه با Deodorant Comments چه باید کرد؟
- اگر کد بد دارید و نمیدانید چگونه آن را اصلاح کنید، چه باید کرد؟
- بهترین مکان برای پرسش و پاسخ درباره مسائل برنامهنویسی کجاست؟
- چگونه میتوان Long Parameter List را کاهش داد؟
- چگونه میتوان یک Large Method را بازسازی کرد؟
- حداکثر طول یک متد تمیز چقدر است؟
- Cyclomatic Complexity برنامه باید در چه بازهای باشد؟
- مقدار ایدهآل برای Depth of Inheritance چقدر است؟
- Speculative Generality چیست و چه باید کرد؟
- هنگام مواجهه با Oddball Solution چه اقدامی باید انجام داد؟
- هنگام مواجهه با Temporary Field چه بازسازیهایی انجام میدهید؟
- Data Clump چیست و چه باید کرد؟
- کد بد Refused Bequest را توضیح دهید.
- Message Chains چه قانونی را نقض میکنند؟
- چگونه باید Message Chains را بازسازی کرد؟
- Feature Envy چیست؟
- چگونه میتوان Feature Envy را حذف کرد؟
- از چه الگویی میتوان برای جایگزینی Switch Statements که اشیاء برمیگردانند، استفاده کرد؟
- چگونه میتوان If Statements که اشیاء برمیگردانند را بازسازی کرد؟
- Solution Sprawl چیست و چگونه باید رفع شود؟
- اصل Tell, Don’t Ask! را توضیح دهید.
- اصل Tell, Don’t Ask! چگونه نقض میشود؟
- علائم Shotgun Surgery چیست و چگونه باید رفع شود؟
- Lost Intent چیست و چه باید کرد؟
- چگونه حلقهها را بازسازی کنیم و چه فوایدی دارد؟
- Divergent Change چیست و چگونه بازسازی میشود؟
مطالعه بیشتر 📖:
- Refactoring - Improving the Design of Existing Code by Martin Fowler & Kent Beck
- refactoring.guru – سایت خوب برای Design Patterns و Code Smells
- dofactory.com – سایت عالی برای C# Design Patterns