فصل اول: استانداردها و اصول کدنویسی در سی شارپ 🖥️
هدف اصلی استانداردها و اصول کدنویسی در C# این است که برنامهنویسان در حرفه خود پیشرفت کنند و کدی بنویسند که کارایی بالاتری داشته باشد و نگهداری آن آسانتر باشد.
در این فصل، به بررسی نمونههایی از کدهای خوب در مقایسه با کدهای بد میپردازیم. این موضوع ما را به سمت بحث در مورد اینکه چرا به استانداردهای کدنویسی، اصول و متدولوژیها نیاز داریم، هدایت میکند. سپس به قراردادهای نامگذاری، نوشتن توضیحات (Comments) و فرمتبندی سورس کد (شامل کلاسها، متدها و متغیرها) میپردازیم.
یک برنامه بزرگ ممکن است درک و نگهداری آن دشوار باشد. برای برنامهنویسان تازهکار، شناخت کد و درک عملکرد آن میتواند کاری دلهرهآور باشد. همچنین کار گروهی روی چنین پروژههایی سختتر میشود و از دیدگاه تست نرمافزار نیز مشکلاتی ایجاد میشود.
به همین دلیل، بررسی خواهیم کرد که چگونه با استفاده از ماژولار بودن (Modularity) میتوان برنامهها را به ماژولهای کوچکتر تقسیم کرد که همگی با هم کار کنند تا یک راهحل کامل، قابل تست، قابل توسعه توسط تیمهای مختلف بهصورت همزمان و بهراحتی خواندنی و قابل مستندسازی ارائه دهند.
در پایان فصل، با برخی از راهنماهای طراحی نرمافزار آشنا میشویم، از جمله:
KISS، YAGNI، DRY، SOLID و تیغ اوکام (Occam's Razor).
موضوعات تحت پوشش در این فصل:
- ضرورت استانداردهای کدنویسی، اصول و متدولوژیها
- قراردادهای نامگذاری و روشها
- توضیحات و فرمتبندی کد
- ماژولار بودن
- KISS (Keep It Simple, Stupid)
- YAGNI (You Aren't Gonna Need It)
- DRY (Don't Repeat Yourself)
- SOLID (اصول طراحی شیگرا)
- تیغ اوکام (Occam's Razor)
اهداف آموزشی این فصل:
- درک اینکه کد بد چگونه بر پروژهها تأثیر منفی میگذارد.
- درک اینکه کد خوب چه تأثیر مثبتی بر پروژهها دارد.
- درک اینکه چگونه استانداردهای کدنویسی کیفیت کد را بهبود میدهند و چگونه میتوان آنها را اعمال کرد.
- درک اینکه اصول کدنویسی چگونه کیفیت نرمافزار را ارتقا میدهند.
- درک اینکه متدولوژیها چگونه به توسعه کد تمیز کمک میکنند.
- پیادهسازی استانداردهای کدنویسی.
- انتخاب راهحلهایی با حداقل فرضیات ممکن.
- کاهش تکرار کد و نوشتن کد بر اساس اصول SOLID.
الزامات فنی 🔧
برای کار با کدهای این فصل، باید Visual Studio 2019 Community Edition یا نسخههای بالاتر را دانلود و نصب کنید. میتوانید این IDE را از لینک زیر دریافت کنید:
https://visualstudio.microsoft.com/
کدهای این کتاب در لینک زیر قرار دارند:
https://github.com/PacktPublishing/Clean-Code-in-C-
تمام کدها در قالب یک سولوشن (Solution) قرار داده شدهاند و هر فصل بهصورت یک پوشه در همان سولوشن است. کدهای مربوط به هر فصل در پوشه همان فصل قرار دارند.
نکته: هنگام اجرای یک پروژه، فراموش نکنید که آن را بهعنوان Startup Project تنظیم کنید.
کد خوب در مقابل کد بد ⚖️
هر دو نوع کد خوب و کد بد کامپایل میشوند؛ این اولین نکته مهم است که باید بدانید. نکته دوم این است که کد بد به دلیل خاصی بد است و به همان ترتیب کد خوب نیز به دلیل خاصی خوب است.
بیایید در جدول زیر به برخی از این دلایل نگاه کنیم:
این فهرست مفصل نیست؟ 🤔
بله، واقعاً فهرست کاملی است، درست است؟ در بخشهای بعدی، به بررسی این ویژگیها و تفاوتهای بین کد خوب و کد بد و تأثیر آنها بر عملکرد کد شما میپردازیم.
کد بد 🚨
در ادامه، نگاهی کوتاه به هر یک از روشهای بد کدنویسی که پیشتر لیست کردیم میاندازیم و توضیح میدهیم که هر یک از این روشها چگونه بر کیفیت کد شما تأثیر میگذارد.
تورفتگی (Indentation) نامناسب
تورفتگی نامناسب باعث میشود که کد بهسختی خوانده شود، بهخصوص زمانی که متدها بزرگ باشند. برای اینکه کد برای انسانها خوانا باشد، نیاز به تورفتگی درست داریم.
وقتی کد تورفتگی مناسبی ندارد، تشخیص اینکه کدام بخش کد به کدام بلوک تعلق دارد بسیار دشوار میشود.
- نکته: بهصورت پیشفرض، Visual Studio 2019 هنگام بستن پرانتزها و آکولادها، کد شما را بهدرستی فرمت و تورفتگیدهی میکند. اما گاهی بهطور عمدی کد را اشتباه فرمت میکند تا توجه شما را به یک استثنا یا خطا جلب کند.
- اگر از یک ویرایشگر متنی ساده استفاده کنید، باید این کار را دستی انجام دهید.
کدی که بهطور نادرست تورفتگی دارد، زمان زیادی برای اصلاح میگیرد و باعث هدررفت وقت برنامهنویس میشود؛ در حالی که میتوانست بهآسانی از این مشکل جلوگیری شود.
مثال زیر را ببینید:
public void DoSomething()
{
for (var i = 0; i < 1000; i++)
{
var productCode = $"PRC000{i}";
//...implementation
}
}
کد بالا ظاهر چندان خوبی ندارد، اما هنوز خوانا است. با این حال، هر چه تعداد خطوط بیشتر شود، خواندن و درک آن سختتر خواهد شد.
همچنین، وقتی تورفتگی درست نباشد، پیدا کردن آکولاد یا پرانتز بسته نشده بسیار سخت میشود چون بهراحتی نمیتوان فهمید کدام بلوک بسته نشده است.
کامنتهایی که بدیهیات را بیان میکنند
بعضی از برنامهنویسها از کامنتهایی که بدیهیات را بیان میکنند واقعاً عصبانی میشوند چون آنها را بیهوده یا حتی توهینآمیز میدانند.
در بحثهای برنامهنویسی که حضور داشتهام، بارها شنیدهام که برنامهنویسها میگویند کامنتگذاری را دوست ندارند و معتقدند که کد باید خودش گویا باشد.
من کاملاً این احساس را درک میکنم؛ اگر بتوانید کدی بنویسید که بدون کامنت مثل یک کتاب خوانا باشد، آن کد واقعاً خوب است.
بهعنوان مثال:
public int _value; // This is used for storing integer values.
در اینجا نیازی نیست بنویسید این متغیر برای ذخیره مقادیر عدد صحیح (int) استفاده میشود، چون نوع داده خودش این موضوع را مشخص کرده است.
چنین کامنتهایی فقط وقت و انرژی را هدر میدهند و باعث شلوغی بیمورد در کد میشوند.
کامنتهایی که کد بد را توجیه میکنند ⚠️
ممکن است زمان محدودی برای انجام یک کار داشته باشید، اما کامنتهایی مانند:
// I know this code sucks but hey at least it works!
کاملاً غیرحرفهای و غیرقابل قبول هستند. این نوع کامنتها بیتوجهی به حرفهای بودن را نشان میدهند و میتوانند همکاران برنامهنویس را ناراضی کنند.
اگر واقعاً مجبور هستید چیزی سریع آماده شود، بهتر است یک تیکت بازآرایی (Refactor Ticket) ایجاد کنید و آن را در قالب یک کامنت TODO اضافه کنید:
// TODO: PBI23154 Refactor Code to meet company coding practices
سپس شما یا دیگر برنامهنویسانی که روی Technical Debt کار میکنند، میتوانید این مورد را بررسی و کد را بازآرایی کنید.
مثال دیگر:
int value = GetDataValue(); // This sometimes causes a divide by zero error. Don't know why!
این کامنت واقعاً بد است. ممنون که اطلاع دادید خطا رخ میدهد، اما آیا تیکت باگ ایجاد کردهاید؟ آیا تلاش کردهاید مشکل را پیدا و اصلاح کنید؟ اگر هیچکس روی آن کد کار نکند، چگونه متوجه وجود کد مشکلدار خواهند شد؟
حداقل کاری که باید انجام دهید این است که یک کامنت TODO اضافه کنید تا در Task List ظاهر شود و توسعهدهندگان از آن مطلع شده و روی آن کار کنند.
خطوط کد کامنتشده ❌
اگر خطوطی از کد را برای آزمایش کامنت میکنید، مشکلی ندارد. اما اگر قرار است کد جایگزین استفاده شود، قبل از Check-in، خطوط کامنتشده را حذف کنید.
یک یا دو خط کامنتشده مشکلی ایجاد نمیکند، اما چندین خط کامنتشده حواسپرتکننده است و نگهداری کد را دشوار میکند و حتی میتواند باعث سردرگمی شود:
/* No longer used as has been replaced by DoSomethingElse().
public void DoSomething()
{
// ...implementation...
}
*/
اگر کد جایگزین شده و دیگر نیاز نیست، آن را حذف کنید. اگر از کنترل نسخه (Version Control) استفاده میکنید و نیاز دارید دوباره به متد دسترسی پیدا کنید، همیشه میتوانید تاریخچه فایل را بررسی کرده و متد را بازیابی کنید.
سازماندهی نادرست Namespaceها 📂
هنگام استفاده از Namespaceها، کدهایی را که باید در جای دیگری باشند، اضافه نکنید. این کار یافتن کد درست را سخت یا غیرممکن میکند، بهخصوص در کدهای بزرگ.
مثال:
namespace MyProject.TextFileMonitor
{
public class Program { ... }
public class DateTime { ... }
public class FileMonitorService { ... }
public class Cryptography { ... }
}
در مثال بالا، همه کلاسها در یک Namespace قرار دارند، اما میتوان با افزودن سه Namespace جدید کد را بهتر سازماندهی کرد:
- MyProject.TextFileMonitor.Core: کلاسهای اصلی که اعضای پرکاربرد را تعریف میکنند، مانند کلاس
DateTime
. - MyProject.TextFileMonitor.Services: تمام کلاسهایی که نقش سرویس دارند، مانند
FileMonitorService
. - MyProject.TextFileMonitor.Security: تمام کلاسهای مربوط به امنیت، شامل کلاس
Cryptography
.
قراردادهای نامگذاری نامناسب 🚫
در گذشته و در دوران Visual Basic 6، از Hungarian Notation استفاده میکردیم. من آن را وقتی به Visual Basic 1.0 مهاجرت کردم، بهکار بردم. اما امروزه استفاده از Hungarian Notation ضروری نیست و حتی باعث میشود کد زشت به نظر برسد.
بهجای نامهایی مانند:
lblName, txtName, btnSave
روش مدرن استفاده از نامهایی مانند:
NameLabel, NameTextBox, SaveButton
است.
-
استفاده از نامهای رمزی یا نامهایی که با هدف کد همخوانی ندارند، خواندن کد را دشوار میکند.
مثال:ihridx
به معنای Human Resources Index و نوع داده آن integer است! -
از نامهایی مانند
mystring
,myint
, وmymethod
اجتناب کنید؛ این نامها واقعاً هیچ کارایی ندارند. -
از خط فاصله (_) بین کلمات مانند
Bad_Programmer
استفاده نکنید. این کار باعث استرس بصری برای توسعهدهندگان میشود و خواندن کد را سخت میکند. کافیست خط فاصله را حذف کنید. -
از یک قرارداد نامگذاری یکسان برای متغیرهای سطح کلاس و سطح متد استفاده نکنید؛ این کار تشخیص دامنه متغیرها را دشوار میکند.
- متغیرها: از Camel Case استفاده کنید، مثال:
alienSpawn
- متد، کلاس، Struct و Interface: از Pascal Case استفاده کنید، مثال:
EnemySpawnGenerator
- متغیرها: از Camel Case استفاده کنید، مثال:
-
برای تمایز بین متغیرهای محلی (داخل Constructor یا متد) و متغیرهای عضو کلاس (قرار گرفته در بالای کلاس خارج از متدها)، متغیرهای عضو را با یک underscore (_) پیشوندگذاری کنید. این روش در محل کار من نیز بهخوبی جواب داده و توسعهدهندگان آن را دوست دارند.
کلاسهایی که چند کار انجام میدهند ❌
یک کلاس باید تنها یک وظیفه داشته باشد.
کلاسی که هم به دیتابیس وصل میشود، داده میگیرد، آن را پردازش میکند، گزارش تولید میکند، دادهها را به گزارش اختصاص میدهد، گزارش را نمایش میدهد، ذخیره و چاپ میکند و خروجی میگیرد، بیش از حد کار انجام میدهد. این کلاس باید به کلاسهای کوچکتر و منظمتر تقسیم شود.
کلاسهای چندوظیفهای خواندن را دشوار و دلهرهآور میکنند. اگر با چنین کلاسهایی برخورد کردید:
- عملکردها را در Regions مرتب کنید.
- سپس کد داخل آن Regions را به کلاسهای جدید منتقل کنید که تنها یک کار انجام دهند.
مثال یک کلاس چندوظیفهای:
public class DbAndFileManager
{
#region Database Operations
public void OpenDatabaseConnection() { throw new NotImplementedException(); }
public void CloseDatabaseConnection() { throw new NotImplementedException(); }
public int ExecuteSql(string sql) { throw new NotImplementedException(); }
public SqlDataReader SelectSql(string sql) { throw new NotImplementedException(); }
public int UpdateSql(string sql) { throw new NotImplementedException(); }
public int DeleteSql(string sql) { throw new NotImplementedException(); }
public int InsertSql(string sql) { throw new NotImplementedException(); }
#endregion
#region File Operations
public string ReadText(string filename) { throw new NotImplementedException(); }
public void WriteText(string filename, string text) { throw new NotImplementedException(); }
public byte[] ReadFile(string filename) { throw new NotImplementedException(); }
public void WriteFile(string filename, byte[] binaryData) { throw new NotImplementedException(); }
#endregion
}
- همانطور که مشاهده میکنید، این کلاس دو وظیفه اصلی دارد: عملیات دیتابیس و عملیات فایل.
- کد داخل Regions مرتب و نامگذاری شده است، اما اصل Single Responsibility Principle (SRP) نقض شده است.
بازآرایی کلاسها 🔄
- کد دیتابیس را استخراج و به کلاس جداگانهای به نام
DatabaseManager
منتقل میکنیم:
using System;
using System.Data.SqlClient;
namespace CH01_CodingStandardsAndPrinciples.GoodCode.Data
{
public class DatabaseManager
{
#region Database Operations
public void OpenDatabaseConnection() { throw new NotImplementedException(); }
public void CloseDatabaseConnection() { throw new NotImplementedException(); }
public int ExecuteSql(string sql) { throw new NotImplementedException(); }
public SqlDataReader SelectSql(string sql) { throw new NotImplementedException(); }
public int UpdateSql(string sql) { throw new NotImplementedException(); }
public int DeleteSql(string sql) { throw new NotImplementedException(); }
public int InsertSql(string sql) { throw new NotImplementedException(); }
#endregion
}
}
- کد فایل سیستم نیز به کلاس
FileManager
در Namespace مناسب منتقل میشود:
using System;
namespace CH01_CodingStandardsAndPrinciples.GoodCode.FileSystem
{
public class FileManager
{
#region File Operations
public string ReadText(string filename) { throw new NotImplementedException(); }
public void WriteText(string filename, string text) { throw new NotImplementedException(); }
public byte[] ReadFile(string filename) { throw new NotImplementedException(); }
public void WriteFile(string filename, byte[] binaryData) { throw new NotImplementedException(); }
#endregion
}
}
با این کار، کلاسهایی که بیش از حد کار انجام میدهند شناسایی و بازآرایی شدند تا هر کلاس تنها یک وظیفه مشخص داشته باشد.
در ادامه، همین فرآیند را برای متدهایی که چند کار انجام میدهند نیز تکرار خواهیم کرد.
متدهایی که چند کار انجام میدهند ⚠️
من بارها خودم را در متدهایی با سطوح متعدد تورفتگی گم کردهام، جایی که متد چندین کار مختلف را در همان تورفتگیها انجام میدهد. ترکیبهای ممکن واقعاً سردرگمکننده بودند.
میخواستم کد را بازآرایی کنم تا نگهداری آسانتر شود، اما برنامهنویس ارشد مانع شد. به وضوح میتوانستم ببینم که متد میتوانست کوچکتر شود اگر کد به متدهای مختلف منتقل میشد.
مثال
در این مثال، متد یک رشته (string) دریافت میکند، سپس آن رشته را رمزگذاری و رمزگشایی میکند. طول متد زیاد است تا نشان دهد چرا متدها باید کوتاه باشند:
public string security(string plainText)
{
try
{
byte[] encrypted;
using (AesManaged aes = new AesManaged())
{
ICryptoTransform encryptor = aes.CreateEncryptor(Key, IV);
using (MemoryStream ms = new MemoryStream())
using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter sw = new StreamWriter(cs))
sw.Write(plainText);
encrypted = ms.ToArray();
}
}
Console.WriteLine($"Encrypted data: {System.Text.Encoding.UTF8.GetString(encrypted)}");
using (AesManaged aesm = new AesManaged())
{
ICryptoTransform decryptor = aesm.CreateDecryptor(Key, IV);
using (MemoryStream ms = new MemoryStream(encrypted))
{
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
{
using (StreamReader reader = new StreamReader(cs))
plainText = reader.ReadToEnd();
}
}
}
Console.WriteLine($"Decrypted data: {plainText}");
}
catch (Exception exp)
{
Console.WriteLine(exp.Message);
}
Console.ReadKey();
return plainText;
}
همانطور که میبینید، این متد 10 خط کد دارد و خواندن آن دشوار است. همچنین بیش از یک کار انجام میدهد.
این کد میتواند به دو متد جداگانه تقسیم شود:
- متدی برای رمزگذاری رشته
- متدی برای رمزگشایی رشته
این مثال نشان میدهد چرا متدها نباید بیش از 10 خط باشند.
متدهای بیش از 10 خط 📝
- متدهای بزرگ خواندن و درک را دشوار میکنند و میتوانند باعث خطاهای سخت پیدا شدن شوند.
- مشکل دیگر این است که ممکن است متد هدف اصلی خود را از دست بدهد.
- شرایط بدتر زمانی رخ میدهد که متدهای بزرگ دارای قسمتهای جداشده با کامنتها و Regions باشند.
اگر برای خواندن متد نیاز به اسکرول کردن زیاد دارید، یعنی متد خیلی طولانی است و میتواند باعث استرس برنامهنویس و سوءتفاهم شود، که در نهایت ممکن است کد یا هدف آن را خراب کند.
روش مناسب:
- متدها را تا حد امکان کوچک نگه دارید.
- با این حال، حس مشترک را بهکار ببرید تا از کوتاهی بیش از حد متدها جلوگیری شود.
- کلید تعادل مناسب این است که هدف متد واضح و بهصورت مختصر پیادهسازی شود.
کد قبلی نمونهای عالی است که نشان میدهد چرا متدها باید کوچک باشند.
- متدهای کوچک خواندن و درک آسانی دارند.
- معمولاً اگر کد شما بیش از 10 خط شود، احتمالاً بیش از هدف خود کار انجام میدهد.
- اطمینان حاصل کنید که نام متدها هدفشان را بیان کند، مانند:
OpenDatabaseConnection()
CloseDatabaseConnection()
و متدها از هدفشان منحرف نشوند.
در ادامه، به بررسی پارامترهای متدها میپردازیم.
متدهایی با بیش از دو پارامتر ⚠️
متدهایی که پارامترهای زیادی دارند معمولاً کمی دشوار و دست و پا گیر میشوند.
- علاوه بر سخت بودن خواندن، به راحتی ممکن است مقداری به پارامتر اشتباه ارسال شود و امنیت نوع دادهها (type safety) نقض شود.
- تست این متدها پیچیدهتر میشود، زیرا ترکیبهای بیشتری برای تست وجود دارد و ممکن است یک سناریو را از دست بدهید که در محیط تولید مشکل ایجاد کند.
استفاده از Exception برای کنترل جریان برنامه 🚫
استفاده از Exceptionها برای کنترل جریان برنامه میتواند هدف کد را پنهان کند و به نتایج غیرمنتظره منجر شود.
اگر کد شما برای انتظار یک یا چند Exception برنامهریزی شده است، یعنی طراحی شما اشتباه است.
مثالی رایج: Business Rule Exceptions (BREs)
- یک متد عملی را انجام میدهد و انتظار دارد که Exception پرتاب شود.
- جریان برنامه براساس پر شدن یا نشدن Exception تعیین میشود.
مثال استفاده از BRE برای کنترل جریان برنامه:
public void BreFlowControlExample(BusinessRuleException bre)
{
switch (bre.Message)
{
case "OutOfAcceptableRange":
DoOutOfAcceptableRangeWork();
break;
default:
DoInAcceptableRangeWork();
break;
}
}
- متد BusinessRuleException میگیرد.
- بسته به پیام داخل Exception، یا متد
DoOutOfAcceptableRangeWork()
و یاDoInAcceptableRangeWork()
فراخوانی میشود.
کنترل جریان با منطق Boolean ✅
یک روش بهتر استفاده از Boolean برای کنترل جریان است. مثال:
public void BetterFlowControlExample(bool isInAcceptableRange)
{
if (isInAcceptableRange)
DoInAcceptableRangeWork();
else
DoOutOfAcceptableRangeWork();
}
-
در این متد، یک مقدار Boolean به آن ارسال میشود.
-
این مقدار تعیین میکند کدام مسیر اجرا شود:
- اگر شرط در محدوده قابل قبول باشد،
DoInAcceptableRangeWork()
فراخوانی میشود. - در غیر این صورت،
DoOutOfAcceptableRangeWork()
اجرا میشود.
- اگر شرط در محدوده قابل قبول باشد،
کدی که سخت خوانده میشود 📜
کدهایی مانند Lasagna و Spaghetti واقعاً خواندن و دنبال کردن آنها دشوار است.
- متدهای با نام نامناسب نیز میتوانند هدف متد را پنهان کنند.
- اگر متدها بزرگ باشند و متدهای مرتبط با آنها توسط متدهای نامرتبط جدا شوند، فهم کد دشوارتر میشود.
Lasagna code 🥘
- به آن Indirection هم گفته میشود و به معنی لایههای انتزاعی است، جایی که چیزی با نام به جای عمل اشاره میشود.
- این لایهبندی در Object-Oriented Programming (OOP) زیاد استفاده میشود و مؤثر است.
- اما هر چه indirection بیشتر شود، کد پیچیدهتر و فهم آن برای برنامهنویسان جدید دشوارتر میشود.
- باید تعادل بین لایهبندی و سهولت درک کد برقرار شود.
Spaghetti code 🍝
- به معنی کد درهم و به هم پیچیده با اتصال زیاد و چسبندگی کم است.
- چنین کدی نگهداری، بازآرایی، گسترش و طراحی مجدد را دشوار میکند.
- مزیت آن این است که خواندن آن نسبتاً آسان است زیرا برنامه به روش رویهای نوشته شده است.
مثال شخصی:
- من به عنوان یک برنامهنویس تازهکار، روی یک برنامه GIS در VB6 کار میکردم که به شرکتها فروخته میشد.
- کد بسیار پیچیده و بزرگ بود و گروه قبلی تلاش کرده بودند آن را بازطراحی کنند و موفق نشده بودند.
- درس من: هنگام بازطراحی نرمافزار، به هیچ عنوان کد موجود را نگاه نکنید.
روش درست:
- همه کارهایی که برنامه انجام میدهد را روی کاغذ بنویسید.
- ویژگیها را گروهبندی کنید.
- یک لیست از نیازمندیها، وظایف، تستها و معیارهای پذیرش بسازید.
- سپس برنامه را براساس مشخصات نوشته شده پیادهسازی کنید.
کدی که به شدت به هم وابسته است 🔗
کدی که تنگاتنگ به هم وابسته است:
- تست کردن آن دشوار است،
- گسترش یا تغییر آن سخت است،
- و استفاده مجدد از کدی که به بخشهای دیگر وابسته است، دشوار میشود.
مثال tight coupling:
- وقتی در پارامتر یک کلاس Concrete را مستقیماً ارجاع میدهید به جای ارجاع به یک Interface.
- تغییرات در کلاس Concrete، کلاس ارجاعدهنده را مستقیم تحت تأثیر قرار میدهد.
مثال عملی:
- یک کلاس اتصال به دیتابیس برای مشتریای که از SQL Server استفاده میکند، دارید.
- اگر مشتری جدید بخواهد از Oracle استفاده کند، کلاس Concrete باید برای آن مشتری خاص تغییر کند.
- در نتیجه، دو نسخه از کد ایجاد میشود و هر چه تعداد مشتریان بیشتر شود، نگهداری کد تقریباً غیرممکن و بسیار پرهزینه میشود.
راهحل:
- به جای ارجاع مستقیم به کلاس Concrete، از Interface استفاده کنید و یک Database Factory بسازید که Object اتصال مورد نیاز را تولید کند.
- رشته اتصال (Connection String) توسط مشتری در فایل پیکربندی تنظیم شده و به Factory داده میشود.
- Factory کلاس Concrete مناسب را تولید میکند که Interface اتصال به دیتابیس را پیادهسازی میکند.
مثال بد از کد tightly coupled:
public class Database
{
private SqlServerConnection _databaseConnection;
public Database(SqlServerConnection databaseConnection)
{
_databaseConnection = databaseConnection;
}
}
- همانطور که میبینید، کلاس Database مستقیماً وابسته به SQL Server است و برای استفاده از هر نوع دیتابیس دیگری نیاز به تغییر سختکد دارد.
- در فصلهای بعدی Refactoring کد با مثالهای عملی پوشش داده خواهد شد.
چسبندگی کم (Low Cohesion) 🧩
- Low cohesion شامل کدی نامرتبط است که وظایف مختلف را با هم انجام میدهد.
- مثال: یک کلاس Utility که متدهای متفاوتی برای کار با تاریخ، متن، اعداد، ورودی/خروجی فایل، اعتبارسنجی داده و رمزگذاری/رمزگشایی دارد.
اشیایی که در حافظه باقی میمانند 🕸️
- اشیایی که در حافظه باقی میمانند میتوانند Memory Leak ایجاد کنند.
دلایل رایج:
-
Static variables
- متغیرهای استاتیک که به اشیایی ارجاع میدهند، توسط Garbage Collector جمعآوری نمیشوند.
- هر چیزی که GC Root باشد، توسط جمعآورنده زباله علامتگذاری میشود که جمعآوری نشود.
-
Anonymous methods
- وقتی یک متد ناشناس، اعضای کلاس را Capture میکند، Instance کلاس زنده میماند تا متد ناشناس وجود دارد.
-
کد unmanaged (COM)
- اگر اشیای managed و unmanaged آزاد نشوند و حافظه به طور صریح آزاد نشود، Memory Leak ایجاد میشود.
-
Cache بدون محدودیت
- کدی که Cache را بدون weak references یا حذف موارد استفاده نشده ذخیره میکند، نهایتاً باعث اتمام حافظه میشود.
-
Threadهای بدون پایان
- اگر ارجاعات به اشیاء در Threadی ایجاد شود که هرگز پایان نمییابد، حافظه هدر میرود.
-
Event subscriptions غیر ناشناس
- وقتی Eventها هنوز به کلاسها متصل هستند، اشیاء در حافظه باقی میمانند.
- لغو ثبت (unsubscribe) ضروری است تا Memory Leak رخ ندهد.
استفاده از متد Finalize()
⚰️
-
Finalizers میتوانند منابعی که توسط اشیاء به درستی آزاد نشدهاند را آزاد کنند و از Memory Leak جلوگیری کنند.
-
اما چند مشکل دارند:
-
زمان اجرای آنها مشخص نیست.
- Garbage Collector آنها را همراه با تمام وابستگانشان به نسل بعدی ارتقا میدهد و تا زمانی که GC تصمیم بگیرد، جمعآوری نمیشوند.
- این ممکن است باعث شود اشیاء برای مدت طولانی در حافظه باقی بمانند.
-
استفاده از finalizers میتواند باعث Out-of-Memory Exception شود، مخصوصاً اگر اشیاء سریعتر از سرعت Garbage Collection ساخته شوند.
-
Over-engineering 🏗️
- Over-engineering میتواند یک کابوس واقعی باشد.
- دلیل اصلی: وقتی شما یک سیستم بزرگ را میبینید و سعی میکنید آن را درک کنید، بفهمید چه چیزی کجا قرار میگیرد و چگونه از آن استفاده کنید، فرآیندی زمانبر و گیجکننده است.
- این موضوع وقتی شدیدتر میشود که مستندات وجود ندارد، شما تازه وارد سیستم شدهاید و حتی افرادی که مدت طولانیتری از سیستم استفاده کردهاند، نمیتوانند به سوالات شما پاسخ دهند.
- میتواند باعث استرس زیاد شود، به خصوص وقتی برای کار روی پروژه مهلت مشخصی دارید.
یادگیری اصل KISS 🧩
-
مثالی از تجربه شخصی:
- مجبور بودم تست یک وب اپلیکیشن را بنویسم که JSON را از یک سرویس میگیرد، اجازه میدهد کودک تستی انجام دهد و سپس نمره را به سرویس دیگری ارسال میکند.
- به جای استفاده از OOP، SOLID یا DRY طبق سیاست شرکت، از KISS و برنامهنویسی رویهای با Events استفاده کردم و کار را در زمان بسیار کوتاهی انجام دادم.
- به دلیل این کار مجازات شدم و مجبور شدم با استفاده از سیستم تست داخلی شرکت دوباره بازنویسی کنم.
-
توضیح:
- نسخه اول من نیازهای کسبوکار را برآورده میکرد و مستقل بود.
- نسخه دوم مطابق با نیازهای فنی تیم توسعه بود و زمان ساخت آن هفتهها طول کشید، چون اجازه تغییر آن را نداشتم.
- هر پروژهای که از مهلت مقرر فراتر رود، هزینه بیشتری برای کسبوکار ایجاد میکند.
- نکته مهم: نسخه اول سادهتر و راحتتر برای درک بود، در حالی که نسخه بازنویسی شده پیچیدهتر شد.
💡 نتیجهگیری: همیشه لازم نیست که OOP، SOLID و DRY را بهصورت سختگیرانه دنبال کنید. گاهی ساده بودن و استفاده از KISS بهترین راه است. در نهایت، حتی زیباترین سیستم OOP در پشت صحنه به کد رویهای (Procedural) تبدیل میشود که کامپیوتر راحتتر میفهمد!
نبود Region در کلاسهای بزرگ 📚
- کلاسهای بزرگ با Regionهای زیاد سخت خوانده میشوند، مخصوصاً وقتی متدهای مرتبط کنار هم قرار نگرفتهاند.
- Region برای گروهبندی اعضای مشابه در یک کلاس بزرگ بسیار مفید است.
- اما اگر از آنها استفاده نکنید، فایدهای ندارد!
کد با هدف گمشده (Lost-intention code) ❌
- وقتی یک کلاس چندین کار مختلف انجام میدهد، چطور میتوان فهمید هدف اصلی آن چیست؟
- مثال: اگر دنبال یک متد تاریخ هستید و آن را در کلاس فایل در فضای نام I/O پیدا میکنید، آیا آنجا جای درستش است؟ خیر.
- این کار برای سایر توسعهدهندگان که با کد شما آشنا نیستند، سختی پیدا کردن متدها را ایجاد میکند.
مثال کد بد:
public class MyClass
{
public void MyMethod()
{
// ...implementation...
}
public DateTime AddDates(DateTime date1, DateTime date2)
{
//...implementation...
}
public Product GetData(int id)
{
//...implementation...
}
}
مشکلات:
-
نام کلاس چیزی نمیگوید و هدف کلاس مشخص نیست.
-
متد
MyMethod
مشخص نیست چه کاری انجام میدهد. -
کلاس هم تاریخها را مدیریت میکند و هم دادههای محصول را دریافت میکند – عدم رعایت اصل تک مسئولیتی (SRP).
AddDates
باید در کلاس مدیریت تاریخ باشد.GetData
باید در ViewModel مربوط به محصول باشد.
افشای مستقیم اطلاعات (Directly exposing information) ⚠️
- کلاسهایی که اطلاعات را به صورت مستقیم در معرض میگذارند، کد را به شدت به هم وابسته میکنند و احتمال بروز خطا را بالا میبرند.
- اگر بخواهید نوع دادهای را تغییر دهید، مجبورید آن را در همه جا تغییر دهید.
- مثال بد:
public class Product
{
public int Id;
public int Name;
public int Description;
public string ProductCode;
public decimal Price;
public long UnitsInStock;
}
مشکلات:
- تغییر نوع
UnitsInStock
ازlong
بهint
نیازمند تغییر در همه جا است. - اگر بخواهید قوانین اعتبارسنجی برای
ProductCode
اضافه کنید، نمیتوانید چون مقدار مستقیماً توسط کلاس فراخوانده شده تغییر میکند.
کد خوب (Good Code) ✅
Indentation مناسب (Proper indentation)
- Indentation مناسب باعث میشود کد راحتتر خوانده شود.
- با توجه به تورفتگیها مشخص است که بلوکها کجا شروع و تمام میشوند.
مثال:
public void DoSomething()
{
for (var i = 0; i < 1000; i++)
{
var productCode = $"PRC000{i}";
//...implementation
}
}
- در این مثال ساده، کد خوانا و مرتب است و شروع و پایان هر بلوک مشخص است.
- رعایت تورفتگی صحیح باعث کاهش اشتباهات و افزایش خوانایی و نگهداری آسانتر کد میشود.
کامنتهای معنادار (Meaningful Comments) 📝
- کامنتهای معنادار هدف برنامهنویس را بیان میکنند.
- چنین کامنتهایی زمانی مفید هستند که کد درست است اما ممکن است برای برنامهنویس جدید یا حتی همان برنامهنویس بعد از چند هفته به سختی قابل فهم باشد.
- این کامنتها به فهم سریعتر و کاهش اشتباهات کمک میکنند.
کامنتهای مستندسازی API (API Documentation Comments) 📄
- یک API خوب، مستندسازی واضح و قابل پیگیری دارد.
- کامنتهای XML میتوانند برای تولید مستندات HTML استفاده شوند.
- مستندات خوب باعث میشود توسعهدهندگان بیشتری از API شما استفاده کنند.
مثال:
/// <summary>
/// Create a new <see cref="KustoCode"/> instance from the text and globals.
/// Does not perform semantic analysis.
/// </summary>
/// <param name="text">The code text</param>
/// <param name="globals">
/// The globals to use for parsing and semantic analysis.
/// Defaults to <see cref="GlobalState.Default"/>
/// </param>
public static KustoCode Parse(string text, GlobalState globals = null) {
// ...implementation...
}
- این مثال از پروژه Kusto Query Language، نمونهای عالی از کامنت مستندسازی API است.
سازماندهی مناسب با استفاده از Namespaceها 📂
-
کد مرتب و قرار گرفته در Namespaceهای مناسب باعث صرفهجویی زمان توسعهدهندگان میشود.
-
مثال: اگر دنبال کلاسها و متدهای مرتبط با تاریخ و زمان هستید:
- Namespace:
DateTime
- کلاس:
Time
→ متدهای مرتبط با زمان - کلاس:
Date
→ متدهای مرتبط با تاریخ
- Namespace:
-
این روش کمک میکند تا کد قابل فهم، قابل نگهداری و سریعتر پیدا شود.
قوانین نامگذاری خوب (Good Naming Conventions) ✨
- رعایت قراردادهای نامگذاری مایکروسافت برای C# توصیه میشود.
- PascalCase برای: Namespaceها، کلاسها، اینترفیسها، Enumها، و متدها
- camelCase برای: نام متغیرها و آرگومانها
- پیشوند _ برای متغیرهای عضو کلاس
مثال:
using System;
using System.Text.RegularExpressions;
namespace CompanyName.ProductName.RegEx
{
/// <summary>
/// An extension class for providing regular expression extensions methods.
/// </summary>
public static class RegularExpressions
{
private static string _preprocessed;
public static string RegularExpression { get; set; }
public static bool IsValidEmail(this string email)
{
// Email address: RFC 2822 Format.
// Matches a normal email address. Does not check the top-level domain.
// Requires the "case insensitive" option to be ON.
var exp = @"\A(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)@(?:[a-z0-9](?:[a-z0-9-][a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\Z";
bool isEmail = Regex.IsMatch(email, exp, RegexOptions.IgnoreCase);
return isEmail;
}
}
}
- این مثال نشاندهنده نامگذاری مناسب برای Namespace، کلاسها، متغیرهای عضو، پارامترها و متغیرهای محلی است.
کلاسها و متدهای تکوظیفهای 🎯
- کلاس خوب: فقط یک کار انجام میدهد و هدف آن واضح است.
- متد خوب: فقط یک کار انجام میدهد، مثال: نه رمزنگاری و جایگزینی رشته همزمان.
- متدهای تکوظیفهای معمولاً کوتاه، قابل خواندن و هدفمند هستند.
طول متدها و تعداد پارامترها 📏
- متدها: بهتر است کمتر از 10 خط و ترجیحاً زیر 4 خط باشند.
- پارامترها: ایدهآل بدون پارامتر، ولی 1 یا 2 پارامتر قابل قبول است.
- اگر بیش از دو پارامتر نیاز است، بهتر است یک شیء پاس داده شود تا کد خوانا و قابل پیگیری باشد.
استفاده صحیح از Exceptionها ⚠️
-
از Exception برای کنترل جریان برنامه استفاده نکنید.
-
شرایط معمول که ممکن است Exception ایجاد کنند، باید به گونهای مدیریت شوند که Exception رخ ندهد.
-
برای بازیابی یا آزادسازی منابع، از try/catch/finally استفاده کنید.
-
هنگام گرفتن Exception، از نوع خاص آن Exception استفاده کنید تا اطلاعات دقیقتری برای لاگ یا مدیریت خطا داشته باشید.
-
در مواقعی که نمیتوان از Exceptionهای پیشفرض .NET استفاده کرد، Exception سفارشی بسازید:
-
پسوند کلاس Exception با
Exception
ختم شود. -
سه سازنده داشته باشد:
Exception()
– استفاده از مقادیر پیشفرضException(string)
– دریافت پیامException(string, Exception)
– دریافت پیام و Inner Exception
-
-
اگر لازم است Exception پرتاب شود، کدهای خطا برنگردانید، بلکه Exception با اطلاعات معنیدار بازگردانید.
کد قابل خواندن 📝
-
هر چه کد قابل خواندنتر باشد، کار توسعهدهندگان راحتتر است.
-
کد خوانا:
- یادگیری و توسعه را آسان میکند
- نگهداری توسط افراد جدید سادهتر است
- احتمال خطا و ناامن بودن کد کمتر است
کد کموابسته (Loosely Coupled) 🔗
- کد کموابسته راحتتر تست، بازسازی و استفاده مجدد میشود.
- مثال اصلاح شده از کلاس Database با Interface:
public class Database
{
private IDatabaseConnection _databaseConnection;
public Database(IDatabaseConnection databaseConnection)
{
_databaseConnection = databaseConnection;
}
}
- مزیت: فقط کلاسهای مربوط به یک نوع پایگاه داده تحت تاثیر تغییرات قرار میگیرند و هزینه نگهداری کاهش مییابد.
انسجام بالا (High Cohesion) 🎯
- عملکردهای مرتبط باید در یک مکان گروهبندی شوند.
- مثال:
System.Diagnostics
فقط شامل کدهای مربوط به تشخیص خطا است، و نه Collections یا FileSystem.
آزادسازی تمیز منابع (Clean Disposal) 🧹
- کلاسهای disposable باید همیشه با
Dispose()
منابع خود را آزاد کنند. - استفاده از using statement بهترین روش است، چون پس از خروج از بلوک، شیء بهصورت خودکار Dispose میشود:
using (var unitOfWork = new UnitOfWork())
{
// Perform unit of work here.
}
// unitOfWork بهصورت خودکار Dispose شده است
- نیازی به فراخوانی دستی
Dispose()
نیست؛using
این کار را خودکار انجام میدهد.
اجتناب از استفاده از Finalize() ☢️
- هنگام کار با منابع unmanaged، بهتر است اینترفیس IDisposable را پیادهسازی کنید و از
Finalize()
استفاده نکنید. - Finalize() تضمینی برای زمان اجرا ندارد و ممکن است به ترتیب یا زمان مورد انتظار اجرا نشود.
- بهتر و قابل اعتمادتر است که منابع unmanaged را در متد
Dispose()
آزاد کنید.
سطح مناسب انتزاع (Abstraction) 🎛️
- سطح مناسب انتزاع زمانی است که تنها بخشهایی که نیاز به نمایش دارند، برای لایه بالاتر افشا شوند و جزئیات پیادهسازی گم نشوند.
- اگر در جزئیات پیادهسازی گم میشوید → Over-abstracted
- اگر چند نفر باید همزمان روی یک کلاس کار کنند → Under-abstracted
- در هر دو حالت، بازسازی (Refactoring) لازم است تا سطح انتزاع درست شود.
استفاده از Regions در کلاسهای بزرگ 📂
- Regions برای گروهبندی اعضا و متدها در کلاسهای بزرگ بسیار مفید هستند و میتوان آنها را Collapse/Expand کرد.
- مزیت: خواندن کلاسهای بزرگ سادهتر میشود و نیازی به پرش مداوم بین متدها نیست.
- روش پیشنهادی: متدهایی که با هم مرتبط هستند را در یک Region قرار دهید تا هنگام کار با کد، راحت بتوانید آنها را باز و بسته کنید.
ضرورت استانداردها، اصول و متدولوژیها 🏗️
-
اکثر نرمافزارهای امروز توسط تیمهای چند نفره توسعه داده میشوند.
-
هر برنامهنویس سبک و ایدئولوژی خاص خود را دارد، اما رعایت استانداردها و اصول مشترک، کار تیمی را ساده میکند.
-
Coding standards: قوانین «باید» و «نباید» که باید رعایت شوند.
- ابزارهایی مثل FxCop یا بررسی دستی توسط همتیمیها قابل اجرا هستند.
- در عمل، وقتی فشار زمانبندی زیاد است، ممکن است استانداردها کنار گذاشته شوند و مشکلات بعداً به عنوان Technical Debt در لیست باگها اصلاح شوند.
-
مزیت رعایت استانداردها:
- کد یکپارچه و خوانا
- راحتی در گسترش و نگهداری
- کاهش احتمال خطا و پیدا کردن سریعتر خطاها
-
نمونه منابع استانداردهای مایکروسافت و عمومی:
رعایت استانداردها باعث میشود کد یکپارچه، قابل خواندن و کمتر خطادار باشد.
اصول کدنویسی 🧩
- Coding principles مجموعهای از دستورالعملها برای نوشتن کد با کیفیت، تست و دیباگ آن و نگهداری کد هستند.
- اصول میتوانند بین برنامهنویسان و تیمها متفاوت باشند، اما حتی برای یک برنامهنویس تنها، تعریف و پایبندی به اصول شخصی مفید است.
- اگر در تیم کار میکنید، توافق روی یک مجموعه اصول و استانداردها باعث میشود کار با کد مشترک آسانتر شود.
برخی اصول رایج:
-
SOLID:
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution
- Interface Segregation Principle
- Dependency Inversion Principle
-
YAGNI: You Ain’t Gonna Need It
-
KISS: Keep It Simple, Stupid
-
DRY: Don’t Repeat Yourself
متدولوژیهای کدنویسی 🛠️
-
Coding methodologies فرایند توسعه نرمافزار را به فازهای از پیش تعریفشده تقسیم میکنند، هر فاز شامل مراحل مشخصی است.
-
هدف: سادهسازی مسیر از مفهوم اولیه تا استقرار و نگهداری.
-
مثالهایی که در این کتاب آموزش داده میشوند:
- Test-Driven Development (TDD)
- Behavioral-Driven Development (BDD) با SpecFlow
- Aspect-Oriented Programming (AOP) با PostSharp
استانداردهای کدنویسی 📏
-
بهتر است از Coding Conventions مایکروسافت برای C# استفاده شود:
Microsoft C# Coding Conventions -
رعایت این استانداردها باعث میشود:
- کد یک فرمت رسمی و پذیرفتهشده داشته باشد
- تمرکز برنامهنویسان روی خواندن کد باشد نه روی چیدمان آن
- بهترین شیوهها (Best Practices) رعایت شود
مدولار بودن کد (Modularity) 🧱
-
برنامههای بزرگ را به ماژولهای کوچک تقسیم کنید:
- آسانتر برای تست و نگهداری
- قابل استفاده مجدد
- مستقل از سایر ماژولها
-
مدولار کردن شامل اسمفیسها (Namespaces) و فولدرها میشود:
-
یک Namespace فقط شامل کد مرتبط با نام خود باشد. مثال:
FileSystem
→ کلاسها و تایپهای مربوط به فایل و دایرکتوریData
→ فقط تایپهای مربوط به داده و منابع داده
-
مزیت: کد کوچک و مدولار راحتتر خوانده و درک میشود و باعث فهم بهتر و استفاده آسانتر توسط توسعهدهندگان میشود.
KISS – Keep It Simple, Stupid ⚡
-
حتی اگر شما برنامهنویس نابغهای هستید، کد باید ساده و قابل فهم برای انسان باشد.
-
کد پیچیده باعث میشود حتی برنامهنویسان با تجربه دچار سردرگمی شوند و زمان زیادی برای فهم آن صرف کنند.
-
هنگام طراحی سیستمها، همیشه از خود بپرسید:
- آیا واقعاً نیاز به پیچیدگی مانند Microservices داریم؟
- آیا پروژه موجود بیش از حد پیچیده شده است؟
- کمترین تعداد اجزای لازم برای نوشتن یک راهکار پایدار، قابل نگهداری و مقیاسپذیر چیست؟
سادگی کد به خوانایی، نگهداری آسان و کاهش فشار ذهنی توسعهدهندگان کمک میکند.
YAGNI 🛑
-
YAGNI (You Ain’t Gonna Need It) یک اصل در دنیای برنامهنویسی چابک است.
-
برنامهنویس نباید هیچ کدی را اضافه کند مگر آنکه واقعاً نیاز باشد.
-
روش کار:
- نوشتن تستهای شکستخورده بر اساس طراحی
- نوشتن حداقل کد برای پاس شدن تستها
- ریفکتور کردن کد برای حذف تکرار و بهبود ساختار
-
هدف اصلی: جلوگیری از Over-engineering و اضافه کردن پیچیدگی غیرضروری به نرمافزار.
-
نکته مهم: کد آموزشی و آزمایشی را در پروژههای جداگانه نگه دارید، نه در پروژه اصلی.
DRY 🔄
- Don’t Repeat Yourself یعنی از تکرار کد جلوگیری کنید.
- اگر یک قطعه کد در چند جای سیستم تکرار شده، باید آن را ریفکتور کنید و در یک Helper Class یا Library قرار دهید.
- مزیت: کاهش احتمال خطا و جلوگیری از اصلاح ناقص کد در مکانهای مختلف.
SOLID ⚖️
مجموعهای از ۵ اصل طراحی برای ساخت نرمافزار قابل فهم و قابل نگهداری:
- Single Responsibility Principle (SRP): هر کلاس یا متد فقط یک مسئولیت داشته باشد و عناصر مرتبط با آن در کنار هم باشند.
- Open/Closed Principle (OCP): کلاسها باید برای توسعه باز و برای تغییر بسته باشند.
- Liskov Substitution Principle (LSP): هر شیء پایه باید بتواند هر کلاس مشتقشدهای را بدون تغییر رفتار جایگزین کند.
- Interface Segregation Principle (ISP): به جای یک اینترفیس بزرگ، چند اینترفیس کوچک بسازید تا کلاسها فقط متدهای مورد نیازشان را پیادهسازی کنند.
- Dependency Inversion Principle (DIP): ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشند؛ هر دو باید به انتزاعها وابسته باشند.
نکته: همیشه متغیرها را با نوع انتزاعی (Interface یا Abstract Class) تعریف کنید، نه کلاسهای Concrete.
Occam’s Razor 🔍
-
اصل تیغ اوکام میگوید: موجودیتها را بدون ضرورت تکثیر نکنید.
-
سادهترین راهحل معمولاً درستترین است.
-
در توسعه نرمافزار:
- از فرضیات غیرضروری پرهیز کنید
- راهحل با کمترین فرضیات و کوچکترین تعداد عناصر را انتخاب کنید
-
مزیت: کاهش خطا، پیچیدگی کمتر و پروژههای قابل نگهداریتر.
خلاصه فصل ۱ 📘
در این فصل، با کد خوب و کد بد آشنا شدیم و اهمیت نوشتن کد خوب را بررسی کردیم. همچنین، به لینک Microsoft C# Coding Conventions اشاره شد تا بتوانید بهترین استانداردهای مایکروسافت را دنبال کنید.
همچنین به طور خلاصه با برخی متدولوژیها و اصول برنامهنویسی آشنا شدید:
- DRY: از تکرار کد جلوگیری کنید
- KISS: کد ساده و قابل فهم بنویسید
- SOLID: اصول طراحی برای کد قابل نگهداری
- YAGNI: تنها کد لازم را بنویسید
- Occam’s Razor: سادهترین راهحل معمولاً درستترین است
مزایای مدولار کردن کد با استفاده از namespace و assembly:
- تیمهای مستقل میتوانند روی ماژولهای مستقل کار کنند
- افزایش قابلیت نگهداری و بازاستفاده کد
در فصل بعد، به Peer Code Reviews پرداخته خواهد شد که با وجود اینکه گاهی ناخوشایند است، به حفظ کیفیت کد و رعایت استانداردهای شرکت کمک میکند.
سوالات پیشنهادی برای مرور ✏️
- نتایج کد بد چیست؟
- نتایج کد خوب چیست؟
- مزایای نوشتن کد مدولار چیست؟
- کد DRY چیست؟
- چرا باید هنگام نوشتن کد KISS رعایت کرد؟
- مخفف SOLID چیست؟
- YAGNI را توضیح دهید.
- Occam’s Razor چیست؟
منابع پیشنهادی برای مطالعه بیشتر 📚
- Adaptive Code: Agile coding with design patterns and SOLID principles – Gary McLean Hall
- Hands-On Design Patterns with C# and .NET Core – Jeffrey Chilberto & Gaurav Aroraa
- Building Maintainable Software, C# Edition – Rob van der Leek, Pascal van Eck, Gijs Wijnholds, Sylvan Rigal, Joost Visser
- فهرست Anti-Patterns در ویکیبوک: Anti-Patterns
- اطلاعات و مثالهای Design Patterns: Software Design Patterns