📖 سری آموزشی کتاب C# 12 in a Nutshell
چطور میتونیم برای کلاسهامون یه "قرارداد" تعریف کنیم و بگیم "هر کلاسی که میخواد با من کار کنه، باید این قابلیتها رو داشته باشه"؟
ابزار اصلی ما برای این کار در دنیای شیءگرایی، اینترفیس (Interface) هست. اینترفیسها ستون فقرات معماریهای تمیز، انعطافپذیر و تستپذیر هستن.
اینترفیس فقط رفتار (Behavior) رو تعریف میکنه، نه وضعیت (State). یعنی فقط امضای متدها و پراپرتیها رو داره، نه فیلدهای داده و نه بدنه پیادهسازی برای اعضاش.
تفاوتهای کلیدی با کلاس:
🔹 فقط توابع (متد، پراپرتی، ایونت، ایندکسر) را تعریف میکند، نه فیلدها.
🔹 اعضایش به صورت پیشفرض public و abstract هستند.
🔹 یک کلاس میتواند چندین اینترفیس را پیادهسازی کند (برخلاف وراثت از کلاس که فقط یکیه).
مثال (اینترفیس IEnumerator):
وقتی یه کلاس، اینترفیسی رو پیادهسازی میکنه، قول میده که برای تمام اعضای اون اینترفیس، یک پیادهسازی public ارائه بده.
حالا میتونید یک نمونه از Countdown رو در متغیری از نوع IEnumerator بریزید:
حالا اگه یه کلاس، دو تا اینترفیس رو پیادهسازی کنه که متدهایی با اسم یکسان ولی امضای متفاوت دارن، چی میشه؟ یا اگه بخوایم یه متد از اینترفیس رو از دید عمومی کلاس مخفی کنیم؟
اینجا پای پیادهسازی صریح (Explicit Implementation) به میدون باز میشه. در این حالت، شما اسم اینترفیس رو قبل از اسم متد میارید.
مثال (حل تداخل):
نکته حیاتی: ⚠️ متدی که به صورت صریح پیادهسازی شده، دیگه به صورت عمومی در دسترس نیست. برای صدا زدنش، باید اول آبجکت رو به اون اینترفیس خاص کست کنید:
اینترفیسها، اساس طراحیهای ماژولار، تستپذیر و مبتنی بر اصول SOLID هستن.
📜 قراردادهای کدنویسی در #C: راهنمای کامل اینترفیسها (Interfaces)
چطور میتونیم برای کلاسهامون یه "قرارداد" تعریف کنیم و بگیم "هر کلاسی که میخواد با من کار کنه، باید این قابلیتها رو داشته باشه"؟
ابزار اصلی ما برای این کار در دنیای شیءگرایی، اینترفیس (Interface) هست. اینترفیسها ستون فقرات معماریهای تمیز، انعطافپذیر و تستپذیر هستن.
1️⃣ اینترفیس چیست؟ یک قرارداد، بدون پیادهسازی
اینترفیس فقط رفتار (Behavior) رو تعریف میکنه، نه وضعیت (State). یعنی فقط امضای متدها و پراپرتیها رو داره، نه فیلدهای داده و نه بدنه پیادهسازی برای اعضاش.
تفاوتهای کلیدی با کلاس:
🔹 فقط توابع (متد، پراپرتی، ایونت، ایندکسر) را تعریف میکند، نه فیلدها.
🔹 اعضایش به صورت پیشفرض public و abstract هستند.
🔹 یک کلاس میتواند چندین اینترفیس را پیادهسازی کند (برخلاف وراثت از کلاس که فقط یکیه).
مثال (اینترفیس IEnumerator):
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
2️⃣ پیادهسازی اینترفیس: امضای قرارداد
وقتی یه کلاس، اینترفیسی رو پیادهسازی میکنه، قول میده که برای تمام اعضای اون اینترفیس، یک پیادهسازی public ارائه بده.
internal class Countdown : IEnumerator
{
int count = 11;
public bool MoveNext() => count-- > 0;
public object Current => count;
public void Reset() { throw new NotSupportedException(); }
}
حالا میتونید یک نمونه از Countdown رو در متغیری از نوع IEnumerator بریزید:
IEnumerator e = new Countdown();
while (e.MoveNext())
{
Console.Write(e.Current + " "); // 10 9 8 7 6 5 4 3 2 1 0
}
3️⃣ جادوی پیادهسازی صریح (Explicit Implementation) 🎭
حالا اگه یه کلاس، دو تا اینترفیس رو پیادهسازی کنه که متدهایی با اسم یکسان ولی امضای متفاوت دارن، چی میشه؟ یا اگه بخوایم یه متد از اینترفیس رو از دید عمومی کلاس مخفی کنیم؟
اینجا پای پیادهسازی صریح (Explicit Implementation) به میدون باز میشه. در این حالت، شما اسم اینترفیس رو قبل از اسم متد میارید.
مثال (حل تداخل):
interface I1 { void Foo(); }
interface I2 { int Foo(); }
public class Widget : I1, I2
{
public void Foo() // پیادهسازی پیشفرض برای I1
{
Console.WriteLine("Widget's implementation of I1.Foo");
}
// پیادهسازی صریح برای حل تداخل با I1.Foo
int I2.Foo()
{
Console.WriteLine("Widget's implementation of I2.Foo");
return 42;
}
}نکته حیاتی: ⚠️ متدی که به صورت صریح پیادهسازی شده، دیگه به صورت عمومی در دسترس نیست. برای صدا زدنش، باید اول آبجکت رو به اون اینترفیس خاص کست کنید:
Widget w = new Widget();
w.Foo(); // I1.Foo صدا زده میشه
((I1)w).Foo(); // I1.Foo صدا زده میشه
((I2)w).Foo(); // I2.Foo صدا زده میشه
🤔 حرف حساب و تجربه شما
اینترفیسها، اساس طراحیهای ماژولار، تستپذیر و مبتنی بر اصول SOLID هستن.
🔖 هشتگها:
#CSharp #DotNet #OOP #Interface #CleanCode
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی، وراثت و اینترفیسها رو جداگونه بررسی کردیم. اما وقتی این دو تا با هم ترکیب میشن، دنیایی از نکات ظریف و الگوهای پیشرفته به وجود میاد.
امروز میخوایم یاد بگیریم چطور یه عضو اینترفیس رو در سلسلهمراتب وراثت به درستی override کنیم.
به صورت پیشفرض، وقتی یه عضو اینترفیس رو پیادهسازی میکنید، اون sealed (مهر و موم شده) هست. برای اینکه به کلاسهای فرزند اجازه override کردنش رو بدید، باید اون رو صراحتاً virtual مشخص کنید. این کار به شما اجازه میده از قدرت کامل چندریختی (Polymorphism) استفاده کنید.
حالا فرض کنید کلاس پدر، متد رو virtual نکرده. یه راه برای "override" کردنش، بازپیادهسازی اینترفیس در کلاس فرزنده. این کار، پیادهسازی رو فقط وقتی که آبجکت از طریق خود اینترفیس صدا زده بشه، "هایجک" میکنه.
تلهی بزرگ: ☠️ اگه پیادهسازی در کلاس پدر به صورت ضمنی (public) باشه، این الگو باعث رفتار متناقض و خطرناک میشه!
بازپیادهسازی معمولاً یه راه حل ضعیف و نشانهی طراحی بده. دو الگوی خیلی بهتر برای طراحی کلاسهای توسعهپذیر وجود داره:
الگوی اول: اگه پیادهسازی ضمنیه، همیشه virtual ـش کنید (همون روش شماره ۱).
الگوی دوم (برای پیادهسازی صریح): پیادهسازی صریح (explicit) اینترفیس رو به یک متد protected virtual وصل کنید. این الگو، قدرت کامل رو به کلاسهای فرزند میده تا رفتار رو به صورت امن override کنن.
این الگوها، تفاوت بین یه کتابخونه قابل اعتماد و یه کتابخونه شکننده رو رقم میزنن. طراحی برای توسعهپذیری، نشانه یک معمار نرمافزار حرفهایه.
🧩 وراثت و اینترفیسها در #C: بازپیادهسازی و الگوهای حرفهای
تو پست قبلی، وراثت و اینترفیسها رو جداگونه بررسی کردیم. اما وقتی این دو تا با هم ترکیب میشن، دنیایی از نکات ظریف و الگوهای پیشرفته به وجود میاد.
امروز میخوایم یاد بگیریم چطور یه عضو اینترفیس رو در سلسلهمراتب وراثت به درستی override کنیم.
1️⃣ روش استاندارد: virtual و override
به صورت پیشفرض، وقتی یه عضو اینترفیس رو پیادهسازی میکنید، اون sealed (مهر و موم شده) هست. برای اینکه به کلاسهای فرزند اجازه override کردنش رو بدید، باید اون رو صراحتاً virtual مشخص کنید. این کار به شما اجازه میده از قدرت کامل چندریختی (Polymorphism) استفاده کنید.
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
public virtual void Undo() => Console.WriteLine("TextBox.Undo");
}
public class RichTextBox : TextBox
{
public override void Undo() => Console.WriteLine("RichTextBox.Undo");
}
// --- نتایج ---
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo
((TextBox)r).Undo(); // RichTextBox.Undo (رفتار یکپارچه و درست)2️⃣ تکنیک خطرناک: بازپیادهسازی
(Re-implementation) ⚠️
حالا فرض کنید کلاس پدر، متد رو virtual نکرده. یه راه برای "override" کردنش، بازپیادهسازی اینترفیس در کلاس فرزنده. این کار، پیادهسازی رو فقط وقتی که آبجکت از طریق خود اینترفیس صدا زده بشه، "هایجک" میکنه.
تلهی بزرگ: ☠️ اگه پیادهسازی در کلاس پدر به صورت ضمنی (public) باشه، این الگو باعث رفتار متناقض و خطرناک میشه!
public class TextBox : IUndoable
{
// این متد virtual نیست!
public void Undo() => Console.WriteLine("TextBox.Undo");
}
public class RichTextBox : TextBox, IUndoable // بازپیادهسازی اینترفیس
{
// اینجا new هم میتونستیم بذاریم
public void Undo() => Console.WriteLine("RichTextBox.Undo");
}
// --- نتایج متناقض و خطرناک ---
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo (هایجک شد)
((TextBox)r).Undo(); // TextBox.Undo (فاجعه! رفتار چندریختی شکست)
3️⃣ الگوهای حرفهای (جایگزینهای بهتر) ✅
بازپیادهسازی معمولاً یه راه حل ضعیف و نشانهی طراحی بده. دو الگوی خیلی بهتر برای طراحی کلاسهای توسعهپذیر وجود داره:
الگوی اول: اگه پیادهسازی ضمنیه، همیشه virtual ـش کنید (همون روش شماره ۱).
الگوی دوم (برای پیادهسازی صریح): پیادهسازی صریح (explicit) اینترفیس رو به یک متد protected virtual وصل کنید. این الگو، قدرت کامل رو به کلاسهای فرزند میده تا رفتار رو به صورت امن override کنن.
public class TextBox : IUndoable
{
// پیادهسازی صریح، کار را به یک متد مجازی و محافظتشده میسپارد
void IUndoable.Undo() => Undo();
protected virtual void Undo() => Console.WriteLine("TextBox.Undo");
}
public class RichTextBox : TextBox
{
protected override void Undo() => Console.WriteLine("RichTextBox.Undo");
}
🤔 حرف حساب و تجربه شما
این الگوها، تفاوت بین یه کتابخونه قابل اعتماد و یه کتابخونه شکننده رو رقم میزنن. طراحی برای توسعهپذیری، نشانه یک معمار نرمافزار حرفهایه.
🔖 هشتگها:
#CSharp #DotNet #OOP #Interface #Inheritance
8️⃣ نکته برای نوشتن کد تمیز 🧼
کد تمیز، کدی است که خواندن، نگهداری و درک آن آسان است.
نقطه شروع 🏁
من دوست دارم هنگام یادگیری مفاهیم جدید، با یک مشکل شروع کنم.
و هر چه مشکل گویاتر باشد، بهتر است.
بنابراین ما از یک کد با نوشتار ضعیف به عنوان نقطه شروع برای بازآرایی خود استفاده خواهیم کرد.
و در هر مرحله، من مشخص خواهم کرد که مشکل فعلی چیست و چگونه آن را برطرف خواهیم کرد.
این چیزی است که من وقتی به متد Process نگاه میکنم، میبینم:
nesting
تو در توی عمیق کد - دقیقاً ۴ سطح.
چکهای پیششرط (Precondition) یکی پس از دیگری اعمال میشوند.
پرتاب استثنا (Exception) برای نمایش یک شکست.
چگونه میتوانیم این را به کد تمیز تبدیل کنیم؟ 👎
public void Process(Order? order)
{
if (order != null)
{
if (order.IsVerified)
{
if (order.Items.Count > 0)
{
if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}
if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}
order.IsProcessed = true;
}
}
}
}
1️⃣: اصل بازگشت زودهنگام (Early Return Principle) 🚪
تا الان باید به طرز دردناکی واضح باشد که نسخه اولیه به دلیل دستورات if که چکهای پیششرط را اعمال میکنند، به شدت تودرتو است.
ما این مشکل را با استفاده از اصل بازگشت زودهنگام حل خواهیم کرد، که بیان میکند ما باید به محض برآورده شدن شرایط، از یک متد return کنیم.
در مورد متد Process، این به معنای حرکت از یک ساختار به شدت تودرتو به مجموعهای از guard clauses (شرطهای محافظ) است. 👍
public void Process(Order? order)
{
if (order is null)
{
return;
}
if (!order.IsVerified)
{
return;
}
if (order.Items.Count == 0)
{
return;
}
if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}
if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}
order.IsProcessed = true;
}
2️⃣: ادغام دستورات If برای بهبود خوانایی 🔄
اصل بازگشت زودهنگام، متد Process را خواناتر میکند.
اما نیازی نیست که یک guard clause پس از دیگری داشته باشیم.
بنابراین میتوانیم همه آنها را در یک دستور if ادغام کنیم.
رفتار متد Process بدون تغییر باقی میماند، اما ما مقدار زیادی کد اضافی را حذف میکنیم.
public void Process(Order? order)
{
if (order is null
!order.IsVerified
order.Items.Count == 0)
{
return;
}
if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}
if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}
order.IsProcessed = true;
}
3️⃣: استفاده از LINQ برای کد خلاصهتر ✨
یک بهبود سریع میتواند استفاده از LINQ برای خلاصهتر و گویاتر کردن کد باشد.
به جای بررسی Items.Count == 0، من ترجیح میدهم از متد Any در LINQ استفاده کنم.
شما میتوانید استدلال کنید که LINQ عملکرد بدتری دارد، اما من همیشه برای خوانایی بهینهسازی میکنم.
عملیات بسیار پرهزینهتری در یک اپلیکیشن نسبت به یک فراخوانی متد ساده وجود دارد.
public void Process(Order? order)
{
if (order is null
!order.IsVerified
!order.Items.Any()) // استفاده از Any()
{
return;
}
if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}
if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}
order.IsProcessed = true;
}
4️⃣: عبارت بولین با متد توصیفی 🗣
ادغام چندین شرط در یک دستور if به معنای نوشتن کد کمتر است، اما میتواند خوانایی را در شرایط پیچیده کاهش دهد.
با این حال، شما میتوانید این مشکل را برطرف کرده و خوانایی را با استفاده از یک متغیر یا متد با نام توصیفی بهبود ببخشید.
من استفاده از متدها را ترجیح میدهم، بنابراین متد IsProcessable را برای نمایش چک پیششرط معرفی خواهم کرد.
public void Process(Order? order)
{
if (!IsProcessable(order))
{
return;
}
if (order.Items.Count > 15)
{
throw new Exception(
"The order " + order.Id + " has too many items");
}
if (order.Status != "ReadyToProcess")
{
throw new Exception(
"The order " + order.Id + " isn't ready to process");
}
order.IsProcessed = true;
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
5️⃣: ترجیح دادن پرتاب استثناهای سفارشی (Custom Exceptions) 💥
حالا بیایید در مورد پرتاب کردن استثناها صحبت کنیم. من دوست دارم از استثناها فقط برای شرایط "استثنایی" استفاده کنم، و از آنها برای کنترل جریان در کدم استفاده نمیکنم.
با این حال، اگر شما میخواهید از استثناها برای کنترل جریان استفاده کنید، بهتر است از استثناهای سفارشی استفاده کنید.
شما میتوانید اطلاعات زمینهای ارزشمندی را معرفی کرده و دلیل پرتاب استثنا را بهتر توصیف کنید.
و اگر میخواهید این استثناها را به صورت سراسری مدیریت کنید، میتوانید یک کلاس پایه ایجاد کنید تا بتوانید استثناهای خاصی را catch کنید.
public void Process(Order? order)
{
if (!IsProcessable(order))
{
return;
}
if (order.Items.Count > 15)
{
throw new TooManyLineItemsException(order.Id);
}
if (order.Status != "ReadyToProcess")
{
throw new NotReadyForProcessingException(order.Id);
}
order.IsProcessed = true;
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
6️⃣: رفع اعداد جادویی (Magic Numbers) با ثابتها (Constants) 🔢
یک code smell رایج که من میبینم، استفاده از اعداد جادویی است.
آنها معمولاً به راحتی قابل تشخیص هستند زیرا برای بررسی اعمال شدن یک شرط عددی استفاده میشوند.
مشکل اعداد جادویی این است که هیچ معنایی ندارند.
استدلال در مورد کد دشوارتر و مستعد خطا میشود.
رفع اعداد جادویی باید سرراست باشد و یک راهحل، معرفی یک ثابت (constant) است.
const int MaxNumberOfLineItems = 15;
public void Process(Order? order)
{
if (!IsProcessable(order))
{
return;
}
if (order.Items.Count > MaxNumberOfLineItems)
{
throw new TooManyLineItemsException(order.Id);
}
if (order.Status != "ReadyToProcess")
{
throw new NotReadyForProcessingException(order.Id);
}
order.IsProcessed = true;
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
7️⃣: رفع رشتههای جادویی (Magic Strings) با Enumها 📜
مشابه اعداد جادویی، ما code smell رشتههای جادویی را داریم.
یک مورد استفاده معمول برای رشتههای جادویی، نمایش نوعی از وضعیت (state) است.
شما متوجه خواهید شد که ما مقدار Order.Status را با یک رشته جادویی مقایسه میکنیم تا بررسی کنیم آیا سفارش آماده پردازش است یا نه.
چند مشکل با رشتههای جادویی:
🔹 امکان اشتباه کردن (اشتباه تایپی) آسان است.
🔹 عدم وجود تایپ قوی (strong typing).
🔹 در برابر بازآرایی (refactoring) مقاوم نیستند.
بیایید یک enum به نام OrderStatus برای نمایش وضعیتهای ممکن ایجاد کنیم: 🏷
enum OrderStatus
{
Pending = 0,
ReadyToProcess = 1,
Processed = 2
}
و حالا باید از OrderStatus مناسب در چک استفاده کنیم:
const int MaxNumberOfLineItems = 15;
public void Process(Order? order)
{
if (!IsProcessable(order))
{
return;
}
if (order.Items.Count > MaxNumberOfLineItems)
{
throw new TooManyLineItemsException(order.Id);
}
if (order.Status != OrderStatus.ReadyToProcess)
{
throw new NotReadyForProcessingException(order.Id);
}
order.IsProcessed = true;
order.Status = OrderStatus.Processed;
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
8️⃣: از الگوی آبجکت نتیجه (Result Object Pattern) استفاده کنید 🎁
من گفتم که استفاده از استثناها برای کنترل جریان را ترجیح نمیدهم. اما چگونه میتوانیم این مشکل را برطرف کنیم؟
یک راهحل، استفاده از الگوی آبجکت نتیجه است.
شما میتوانید از یک کلاس جنریک Result برای نمایش انواع نتایج یا یک کلاس خاص مانند ProcessOrderResult استفاده کنید.
برای اینکه آبجکتهای نتیجه شما کپسوله شوند، مجموعهای از متدهای factory برای ایجاد نوع نتیجه مشخص، ارائه دهید.
public class ProcessOrderResult
{
private ProcessOrderResult(
ProcessOrderResultType type,
long orderId,
string message)
{
Type = type;
OrderId = orderId;
Message = message;
}
public ProcessOrderResultType Type { get; }
public long OrderId { get; }
public string? Message { get; }
public static ProcessOrderResult NotProcessable() =>
new(ProcessOrderResultType.NotProcessable, default, "Not processable");
public static ProcessOrderResult TooManyLineItems(long oderId) =>
new(ProcessOrderResultType.TooManyLineItems, orderId, "Too many items");
public static ProcessOrderResult NotReadyForProcessing(long oderId) =>
new(ProcessOrderResultType.NotReadyForProcessing, oderId, "Not ready");
public static ProcessOrderResult Success(long oderId) =>
new(ProcessOrderResultType.Success, orderId, "Success");
}
استفاده از یک enum مانند ProcessOrderResultType، مصرف کردن آبجکت نتیجه را با switch expressions آسانتر میکند. در اینجا enum برای نمایش ProcessOrderResult.Type آمده است:
public enum ProcessOrderResultType
{
NotProcessable = 0,
TooManyLineItems = 1,
NotReadyForProcessing = 2,
Success = 3
}
و حالا متد Process به این شکل در میآید:
const int MaxNumberOfLineItems = 15;
public ProcessOrderResult Process(Order? order)
{
if (!IsProcessable(order))
{
return ProcessOrderResult.NotProcessable();
}
if (order.Items.Count > MaxNumberOfLineItems)
{
return ProcessOrderResult.TooManyLineItems(order.Id);
}
if (order.Status != OrderStatus.ReadyToProcess)
{
return ProcessOrderResult.NotReadyForProcessing(order.Id);
}
order.IsProcessed = true;
order.Status = OrderStatus.Processed;
return ProcessOrderResult.Success(order.Id);
}
static bool IsProcessable(Order? order)
{
return order is not null &&
order.IsVerified &&
order.Items.Any();
}
`
در اینجا نحوه استفاده از یک enum برای ProcessOrderResult.Type به شما اجازه میدهد یک switch expression بنویسید:
var result = Process(order);
result.Type switch
{
ProcessOrderResultType.TooManyLineItems =>
Console.WriteLine($"Too many line items: {result.OrderId}"),
ProcessOrderResultType.NotReadyForProcessing =>
Console.WriteLine($"Not ready for processing {result.OrderId}"),
ProcessOrderResultType.Success =>
Console.WriteLine($"Processed successfully {result.OrderId}"),
_ => Console.WriteLine("Failed to process: {OrderId}", result.OrderId),
};
نکات پایانی 📝
تمام شد، ۸ نکته برای نوشتن کد تمیز:
1️⃣ اصل بازگشت زودهنگام
2️⃣ ادغام چندین دستور if
3️⃣ استفاده از LINQ برای اختصار
4️⃣ جایگزینی عبارت بولین با متد
5️⃣ ترجیح دادن پرتاب استثناهای سفارشی
6️⃣ جایگزینی اعداد جادویی با ثابتها
7️⃣ جایگزینی رشته جادویی با enumها
8️⃣ استفاده از الگوی آبجکت نتیجه
نوشتن کد تمیز، موضوعی از تمرین هدفمند و تجربه است.
اکثر مردم در مورد اصول کدنویسی تمیز میخوانند، اما تعداد کمی تلاش میکنند تا آنها را روزانه به کار ببرند.
اینجاست که شما میتوانید خود را متمایز کنید.
امیدوارم این مطلب مفید بوده باشد.
اقدام عملی امروز: 🚀
نگاهی به پروژه خود بیندازید و ببینید آیا برخی از اشتباهاتی که من در اینجا برجسته کردم را مرتکب میشوید یا نه. و سپس آنها را با استفاده از نکات کد تمیزی که با شما به اشتراک گذاشتم، برطرف کنید.
📖 سری آموزشی کتاب C# 12 in a Nutshell
یکی از اساسیترین سوالات در طراحی شیءگرا اینه: کی باید از یه class و وراثت استفاده کنیم و کی باید بریم سراغ interface؟
این فقط یه انتخاب سینتکسی نیست، یه تصمیم مهم معماریه. بیاید با یه قانون ساده و یه مثال عالی، این موضوع رو برای همیشه روشن کنیم.
به عنوان یک قانون کلی و کاربردی:
از class و وراثت استفاده کن:
وقتی تایپهای شما به صورت طبیعی، پیادهسازی مشترکی (shared implementation) دارند. یعنی کدهای مشترکی بینشون هست که میخواید از تکرارش جلوگیری کنید.
از interface استفاده کن:
وقتی تایپهای شما رفتار مشترکی (shared behavior) دارند، ولی هر کدوم پیادهسازی مستقل (independent implementation) و متفاوتی از اون رفتار رو ارائه میدن.
فرض کنید میخوایم موجودات مختلف رو مدلسازی کنیم.
مشکل (استفاده فقط از کلاس): 👎
یه "عقاب" هم "پرنده" است، هم "موجود پرنده" و هم "گوشتخوار". چون #C وراثت چندگانه از کلاسها رو پشتیبانی نمیکنه، کد زیر کامپایل نمیشه!
راه حل (ترکیب کلاس و اینترفیس): 👍
حالا قانون طلایی رو به کار میبریم. "پرنده" بودن احتمالاً یه سری پیادهسازی و ویژگی مشترک داره، پس
این مثال نشون میده که چطور با انتخاب درست بین کلاس و اینترفیس، میتونید ساختارهای پیچیده و در عین حال تمیز و انعطافپذیری طراحی کنید.
🏛 کلاس در برابر اینترفیس: کدام را و چه زمانی انتخاب کنیم؟
یکی از اساسیترین سوالات در طراحی شیءگرا اینه: کی باید از یه class و وراثت استفاده کنیم و کی باید بریم سراغ interface؟
این فقط یه انتخاب سینتکسی نیست، یه تصمیم مهم معماریه. بیاید با یه قانون ساده و یه مثال عالی، این موضوع رو برای همیشه روشن کنیم.
1️⃣ قانون طلایی: پیادهسازی مشترک در برابر رفتار مشترک ⚖️
به عنوان یک قانون کلی و کاربردی:
از class و وراثت استفاده کن:
وقتی تایپهای شما به صورت طبیعی، پیادهسازی مشترکی (shared implementation) دارند. یعنی کدهای مشترکی بینشون هست که میخواید از تکرارش جلوگیری کنید.
از interface استفاده کن:
وقتی تایپهای شما رفتار مشترکی (shared behavior) دارند، ولی هر کدوم پیادهسازی مستقل (independent implementation) و متفاوتی از اون رفتار رو ارائه میدن.
2️⃣ مثال عملی: دنیای حیوانات 🦅
فرض کنید میخوایم موجودات مختلف رو مدلسازی کنیم.
مشکل (استفاده فقط از کلاس): 👎
یه "عقاب" هم "پرنده" است، هم "موجود پرنده" و هم "گوشتخوار". چون #C وراثت چندگانه از کلاسها رو پشتیبانی نمیکنه، کد زیر کامپایل نمیشه!
abstract class Animal {}
abstract class Bird : Animal {}
abstract class FlyingCreature : Animal {}
abstract class Carnivore : Animal {}
// ❌ خطای کامپایل! یک کلاس نمیتونه از چند کلاس ارثبری کنه
class Eagle : Bird, FlyingCreature, Carnivore {}راه حل (ترکیب کلاس و اینترفیس): 👍
حالا قانون طلایی رو به کار میبریم. "پرنده" بودن احتمالاً یه سری پیادهسازی و ویژگی مشترک داره، پس
Bird یه class باقی میمونه. اما "پرواز کردن" یا "گوشتخوار بودن"، رفتارهایی هستن که موجودات مختلف (مثل حشره و پرنده) به شکلهای کاملاً متفاوتی انجام میدن. پس اینها بهترین کاندیدا برای اینترفیس هستن!// اینترفیسها فقط "رفتار" رو تعریف میکنن
interface IFlyingCreature { }
interface ICarnivore { }
abstract class Animal {}
// کلاسها "پیادهسازی" مشترک رو ارائه میدن
abstract class Bird : Animal {}
// ✅ حالا درسته!
// عقاب از کلاس Bird ارث میبره و دو اینترفیس رو پیادهسازی میکنه
class Eagle : Bird, IFlyingCreature, ICarnivore {}
🤔 حرف حساب و تجربه شما
این مثال نشون میده که چطور با انتخاب درست بین کلاس و اینترفیس، میتونید ساختارهای پیچیده و در عین حال تمیز و انعطافپذیری طراحی کنید.
🔖 هشتگها:
#CSharp #DotNet #OOP #SoftwareArchitecture #Interface #CleanCode
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی، یاد گرفتیم که کی باید از اینترفیس استفاده کنیم. اما داستان اینترفیسها به تعریف چند متد ختم نمیشه! در نسخههای مدرن #C، اینترفیسها به قابلیتهای فوقالعاده قدرتمندی مجهز شدن که قواعد بازی رو عوض میکنن.
امروز با این ابرقدرتها آشنا میشیم.
تصور کنید یه کتابخونه نوشتید و هزاران نفر دارن از اینترفیس ILogger شما استفاده میکنن. حالا اگه بخواید یه متد جدید به این اینترفیس اضافه کنید، کد تمام اون هزار نفر میشکنه!
Default Interface Members
این مشکل رو حل میکنه. شما میتونید یه متد جدید رو با پیادهسازی پیشفرض به اینترفیس اضافه کنید. این یعنی کلاسهایی که از اینترفیس شما استفاده میکنن، مجبور نیستن فوراً این متد جدید رو پیادهسازی کنن.
نکته کلیدی: ⚠️ این پیادهسازیهای پیشفرض، به صورت صریح (explicit) هستن. یعنی اگه کلاسی اون متد رو پیادهسازی نکنه، برای صدا زدنش باید حتماً آبجکت رو به خود اینترفیس کست کنید.
بله، درست خوندید! اینترفیسها در #C مدرن حتی میتونن اعضای استاتیک هم داشته باشن!
اعضای استاتیک غیرمجازی:
این اعضا (مثل فیلدها و متدهای استاتیک) برای تعریف مقادیر ثابت یا متدهای کمکی که به خود اینترفیس مرتبطن، عالین.
این یکی از پیشرفتهترین قابلیتهای #C هست و درهای دنیای جدیدی به اسم "چندریختی استاتیک" (Static Polymorphism) رو باز میکنه (مثلاً برای Generic Math). کلاسهایی که این اینترفیس رو پیادهسازی میکنن، باید اون عضو استاتیک رو هم پیادهسازی کنن.
یه نکته پرفورمنسی مهم: وقتی شما یه struct (که Value Type هست) رو به یک اینترفیسی که پیادهسازی کرده کست میکنید، عمل Boxing اتفاق میفته و اون struct روی هیپ کپی میشه. این کار هزینه داره!
این قابلیتهای مدرن، اینترفیسها رو از یه قرارداد ساده به یه ابزار طراحی فوقالعاده قدرتمند تبدیل کردن.
🚀 انقلاب اینترفیسها در #C مدرن: Default و Static Members
تو پست قبلی، یاد گرفتیم که کی باید از اینترفیس استفاده کنیم. اما داستان اینترفیسها به تعریف چند متد ختم نمیشه! در نسخههای مدرن #C، اینترفیسها به قابلیتهای فوقالعاده قدرتمندی مجهز شدن که قواعد بازی رو عوض میکنن.
امروز با این ابرقدرتها آشنا میشیم.
1️⃣ Default Interface Members (C# 8):
ارتقاء بدون شکستن کد! ✨
تصور کنید یه کتابخونه نوشتید و هزاران نفر دارن از اینترفیس ILogger شما استفاده میکنن. حالا اگه بخواید یه متد جدید به این اینترفیس اضافه کنید، کد تمام اون هزار نفر میشکنه!
Default Interface Members
این مشکل رو حل میکنه. شما میتونید یه متد جدید رو با پیادهسازی پیشفرض به اینترفیس اضافه کنید. این یعنی کلاسهایی که از اینترفیس شما استفاده میکنن، مجبور نیستن فوراً این متد جدید رو پیادهسازی کنن.
نکته کلیدی: ⚠️ این پیادهسازیهای پیشفرض، به صورت صریح (explicit) هستن. یعنی اگه کلاسی اون متد رو پیادهسازی نکنه، برای صدا زدنش باید حتماً آبجکت رو به خود اینترفیس کست کنید.
public interface ILogger
{
// متد جدید با پیادهسازی پیشفرض
void Log(string text) => Console.WriteLine(text);
void LogError(Exception ex); // متد قدیمی بدون پیادهسازی
}
public class MyLogger : ILogger
{
// این کلاس فقط مجبوره LogError رو پیادهسازی کنه
public void LogError(Exception ex) { /* ... */ }
}
// --- نحوه استفاده ---
var logger = new MyLogger();
// logger.Log("hi"); // ❌ خطای زمان کامپایل!
((ILogger)logger).Log("hi"); // ✅ درسته!
2️⃣ Static Interface Members:
ابزارهای کمکی و پلیمورفیسم استاتیک ⚙️
بله، درست خوندید! اینترفیسها در #C مدرن حتی میتونن اعضای استاتیک هم داشته باشن!
اعضای استاتیک غیرمجازی:
این اعضا (مثل فیلدها و متدهای استاتیک) برای تعریف مقادیر ثابت یا متدهای کمکی که به خود اینترفیس مرتبطن، عالین.
public interface ILogger
{
static string DefaultPrefix = "[INFO]: ";
void Log(string text) => Console.WriteLine(DefaultPrefix + text);
}
اعضای استاتیک مجازی/انتزاعی (از 11 #C):
این یکی از پیشرفتهترین قابلیتهای #C هست و درهای دنیای جدیدی به اسم "چندریختی استاتیک" (Static Polymorphism) رو باز میکنه (مثلاً برای Generic Math). کلاسهایی که این اینترفیس رو پیادهسازی میکنن، باید اون عضو استاتیک رو هم پیادهسازی کنن.
3️⃣ نکته فنی: اینترفیسها و Boxing
یه نکته پرفورمنسی مهم: وقتی شما یه struct (که Value Type هست) رو به یک اینترفیسی که پیادهسازی کرده کست میکنید، عمل Boxing اتفاق میفته و اون struct روی هیپ کپی میشه. این کار هزینه داره!
interface I { void Foo(); }
struct S : I { public void Foo() {} }
S s = new S();
s.Foo(); // بدون Boxing
I i = s; // ❌ Boxing اتفاق میفته!
i.Foo();🤔 حرف حساب و تجربه شما
این قابلیتهای مدرن، اینترفیسها رو از یه قرارداد ساده به یه ابزار طراحی فوقالعاده قدرتمند تبدیل کردن.
🔖 هشتگها:
#CSharp #DotNet #OOP #Interface #ModernCSharp
ساخت اتریبیوتهای سفارشی در #C 🏷
اتریبیوتها راهی برای افزودن متادیتا به کد شما فراهم میکنند. در این پست وبلاگ، ما به اصول اولیه اینکه اتریبیوتها چه هستند، چگونه پراپرتیهای اتریبیوت را تنظیم کنیم، و چگونه محل اعمال اتریبیوتها را پیکربندی کنیم، خواهیم پرداخت. در نهایت، به یک مثال عملی شیرجه خواهیم زد تا نشان دهیم چگونه میتوان از اتریبیوتهای سفارشی در اپلیکیشنها استفاده کرد.
اتریبیوت چیست؟ 🤔
اتریبیوتها در #C راهی برای افزودن اطلاعات اعلانی (declarative) به کد شما هستند. آنها متادیتا فراهم میکنند که میتوان از آن برای کنترل جنبههای مختلف رفتار برنامه شما در زمان اجرا یا زمان کامپایل استفاده کرد.
اتریبیوتها میتوانند به عناصر مختلف برنامه مانند موارد زیر اعمال شوند:
🔹 اسمبلیها، ماژولها، کلاسها، اینترفیسها، استراکتها، enumها
🔹 متدها، سازندهها، delegateها، ایونتها
🔹 پراپرتیها، فیلدها، پارامترها، پارامترهای جنریک، مقادیر بازگشتی
🔹 یا همه عناصر ذکر شده
شما میتوانید یک یا چند اتریبیوت را به این عناصر برنامه اعمال کنید.
شما هر روز هنگام ساخت اپلیکیشن در NET. از اتریبیوتها استفاده میکنید. برای مثال، اتریبیوتهای زیر را بسیار دیدهاید و استفاده کردهاید: 👇
[Serializable]
public class User
{
}
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
}
چگونه یک اتریبیوت سفارشی بسازیم؟ ✍️
اتریبیوت در #C کلاسی است که از کلاس پایه Attribute ارثبری میکند.
بیایید نگاهی به نحوه ساخت یک اتریبیوت سفارشی بیندازیم:
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CustomAttribute : Attribute
{
}
[Custom]
public class MyClass
{
}
نام کلاس اتریبیوت باید پسوند Attribute داشته باشد. هنگام اعمال به هر عنصری، این پسوند حذف میشود.
هنگام ایجاد یک کلاس اتریبیوت، شما از یک اتریبیوت داخلی برای مشخص کردن جایی که اتریبیوت میتواند اعمال شود، استفاده میکنید: 🎯
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true)]
public class CustomAttribute : Attribute
{
}
شما میتوانید چندین تارگت را با عملگر | ترکیب کنید تا مشخص کنید یک اتریبیوت کجا میتواند اعمال شود.
پارامتر Inherited نشان میدهد که آیا اتریبیوت سفارشی میتواند توسط کلاسهای مشتق شده به ارث برده شود یا نه. مقدار پیشفرض true است. 🧬
[AttributeUsage(AttributeTargets.Class, Inherited = true)]
public class CustomInheritedAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CustomNonInheritedAttribute : Attribute
{
}
هنگام اعمال اتریبیوت inherited به کلاس ParentA، آن توسط کلاس ChildA به ارث برده میشود:
[CustomInherited]
public class ParentA
{
}
public class ChildA : ParentA
{
}
در اینجا، کلاس ChildA یک اتریبیوت CustomInherited دارد. ✅
در حالی که در مثال زیر، هنگام استفاده از یک اتریبیوت non-inherited برای ParentB، آن به کلاس ChildB اعمال نمیشود: ❌
[CustomNonInherited]
public class ParentB
{
}
public class ChildB : ParentB
{
}
پراپرتیهای اتریبیوت ⚙️
پراپرتیهای اتریبیوت میتوانند یا الزامی (پارامترهای اجباری) یا غیرالزامی (پارامترهای اختیاری) باشند. اتریبیوتها میتوانند آرگومانها را به همان روشی که متدها و پراپرتیها میپذیرند، قبول کنند.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
public class CustomWithParametersAttribute : Attribute
{
public string RequiredProperty { get; }
public int OptionalProperty { get; set; }
public CustomWithParametersAttribute(string requiredProperty)
{
RequiredProperty = requiredProperty;
}
}
پراپرتیهای الزامی باید به عنوان پارامترهای سازنده اتریبیوت تعریف شوند. در حالی که غیرالزامیها نباید در سازنده تعریف شوند.
هنگام اعمال چنین اتریبیوتی به یک کلاس، شما باید مقادیر اتریبیوت را به ترتیبی که در سازنده تعریف شدهاند، تنظیم کنید. پراپرتیهای اختیاری باید با نامشان مشخص شوند: 👇
[CustomWithParameters("some text here", OptionalProperty = 5)]
public class ExampleClass
{
}مثال عملی از استفاده از اتریبیوتها 🚀
بیایید یک اتریبیوت سفارشی برای مشخص کردن نقشهای مجاز برای دسترسی به یک متد کنترلر ایجاد کنیم: 🛡
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class AuthorizeRolesAttribute : Attribute
{
public string[] Roles { get; }
public AuthorizeRolesAttribute(params string[] roles)
{
Roles = roles;
}
}
سپس، ما AuthorizeRolesAttribute را به متدهای یک کلاس اعمال میکنیم و نقشهایی را که مجاز به دسترسی به هر متد هستند، مشخص میکنیم:
public class AccountController
{
[AuthorizeRoles("Admin", "Manager")]
public void AdminOnlyAction()
{
Console.WriteLine("Admin or Manager can access this method.");
}
[AuthorizeRoles("User")]
public void UserOnlyAction()
{
Console.WriteLine("Only users can access this method.");
}
public void PublicAction()
{
Console.WriteLine("Everyone can access this method.");
}
}
برای استفاده از این اتریبیوت، میتوانیم از reflection 🔍 برای گرفتن اطلاعات در مورد اینکه چه اتریبیوتهایی به یک متد اعمال شدهاند و چه پراپرتیهایی دارند، استفاده کنیم:
public class RoleBasedAccessControl
{
public void ExecuteAction(object controller, string methodName, string userRole)
{
var method = controller.GetType().GetMethod(methodName);
var attribute = method?.GetCustomAttribute<AuthorizeRolesAttribute>();
if (attribute is null || attribute.Roles.Contains(userRole))
{
method?.Invoke(controller, null);
}
else
{
Console.WriteLine("Access denied. User does not have the required role.");
}
}
}
در اینجا ما یک متد را با نام از یک کلاس کنترلر میگیریم و بررسی میکنیم که آیا اتریبیوت AuthorizeRolesAttribute به متد اعمال شده است یا نه. اگر اعمال شده باشد، بررسی میکنیم که آیا یک کاربر میتواند به متد داده شده دسترسی داشته باشد یا نه.
در نهایت، میتوانیم منطق کنترل دسترسی مبتنی بر نقش را با نقشهای مختلف کاربر تست کنیم: 🧪
var controller = new AccountController();
var accessControl = new RoleBasedAccessControl();
Console.WriteLine("Testing with Admin role:");
accessControl.ExecuteAction(controller, nameof(AccountController.AdminOnlyAction), "Admin");
Console.WriteLine("\nTesting with User role:");
accessControl.ExecuteAction(controller, nameof(AccountController.UserOnlyAction), "User");
Console.WriteLine("\nTesting with Guest role:");
accessControl.ExecuteAction(controller, nameof(AccountController.AdminOnlyAction), "Guest");
Console.WriteLine("\nTesting public method with Guest role:");
accessControl.ExecuteAction(controller, nameof(AccountController.PublicAction), "Guest");
خروجی: 🖥
Testing with Admin role:
Admin or Manager can access this method.
Testing with User role:
Only users can access this method.
Testing with Guest role:
Access denied. User does not have the required role.
Testing public method with Guest role:
Everyone can access this method.
در این مثال، AuthorizeRolesAttribute برای مشخص کردن نقشهای مجاز برای دسترسی به هر متد در AccountController استفاده میشود. کلاس RoleBasedAccessControl این محدودیتها را با بررسی نقش کاربر در برابر نقشهای تعریف شده در اتریبیوت، اعمال میکند. این نشان میدهد که چگونه میتوان از اتریبیوتهای سفارشی به روشی عملی و مفید در یک سناریوی دنیای واقعی استفاده کرد. ✅
📖 سری آموزشی کتاب C# 12 in a Nutshell
وقتی میخواید یه مجموعه از مقادیر ثابت و مرتبط رو تعریف کنید (مثل جهتها، رنگها، یا وضعیتها)، اولین چیزی که باید به ذهنتون برسه Enum هست. Enumها به کد شما خوانایی و ایمنی نوع (Type Safety) میدن.
بیاید با تمام جزئیاتشون آشنا بشیم.
Enum
یک نوع داده ارزشی (Value Type) خاصه که به شما اجازه میده برای یک گروه از ثابتهای عددی، اسمهای معنادار تعریف کنید.
پشت صحنه: 🧐
هر عضو Enum یک مقدار عددی صحیح داره. به صورت پیشفرض:
نوع داده زیرین، int است.
مقادیر به ترتیب از 0 شروع میشن (Left=0, Right=1, Top=2, ...).
سفارشیسازی:
شما میتونید هم نوع داده زیرین و هم مقدار هر عضو رو خودتون مشخص کنید:
شما میتونید یه Enum رو به مقدار عددی زیرینش و برعکس، به صورت صریح (explicit) کست کنید.
نکته جالب: 💡 لیترال عددی
این مهمترین نکته در مورد Enumهاست. با اینکه Enumها به ما Type Safety میدن، ولی به خاطر قابلیت کست شدن از عدد، یه متغیر Enum میتونه یه مقدار عددی نامعتبر (خارج از مقادیر تعریف شده) رو تو خودش نگه داره!
این رفتار میتونه منطق if-else یا switch شما رو که انتظار مقادیر مشخصی رو دارن، کاملاً به هم بریزه.
برای اینکه مطمئن بشید یه مقدار Enum معتبره، میتونید از متد استاتیک Enum.IsDefined استفاده کنید.
درک این جزئیات و تلههای Enum، به شما کمک میکنه کدهای امنتر و قابل اعتمادتری بنویسید.
🏷 راهنمای کامل Enum در #C: فراتر از چند اسم ثابت
وقتی میخواید یه مجموعه از مقادیر ثابت و مرتبط رو تعریف کنید (مثل جهتها، رنگها، یا وضعیتها)، اولین چیزی که باید به ذهنتون برسه Enum هست. Enumها به کد شما خوانایی و ایمنی نوع (Type Safety) میدن.
بیاید با تمام جزئیاتشون آشنا بشیم.
1️⃣ Enum چیست و چه ساختاری دارد؟
Enum
یک نوع داده ارزشی (Value Type) خاصه که به شما اجازه میده برای یک گروه از ثابتهای عددی، اسمهای معنادار تعریف کنید.
public enum BorderSide { Left, Right, Top, Bottom }پشت صحنه: 🧐
هر عضو Enum یک مقدار عددی صحیح داره. به صورت پیشفرض:
نوع داده زیرین، int است.
مقادیر به ترتیب از 0 شروع میشن (Left=0, Right=1, Top=2, ...).
سفارشیسازی:
شما میتونید هم نوع داده زیرین و هم مقدار هر عضو رو خودتون مشخص کنید:
public enum BorderSide : byte // تغییر نوع به بایت
{
Left = 1,
Right, // مقدارش میشه 2 (یکی بیشتر از قبلی)
Top = 10,
Bottom // مقدارش میشه 11 (یکی بیشتر از قبلی)
}
2️⃣ تبدیلها (Conversions)
شما میتونید یه Enum رو به مقدار عددی زیرینش و برعکس، به صورت صریح (explicit) کست کنید.
BorderSide side = BorderSide.Left; // side = 1
int i = (int)side; // i = 1
BorderSide side2 = (BorderSide)i; // side2 = BorderSide.Left
نکته جالب: 💡 لیترال عددی
0 تنها عددیه که برای انتساب به Enum نیاز به کست نداره.3️⃣ تله بزرگ: Type-Safety و مقادیر نامعتبر! ⚠️
این مهمترین نکته در مورد Enumهاست. با اینکه Enumها به ما Type Safety میدن، ولی به خاطر قابلیت کست شدن از عدد، یه متغیر Enum میتونه یه مقدار عددی نامعتبر (خارج از مقادیر تعریف شده) رو تو خودش نگه داره!
// این کد کامپایل و اجرا میشه، ولی b حاوی یک مقدار بیمعنیه
BorderSide b = (BorderSide)12345;
Console.WriteLine(b); // خروجی عددی: 12345
این رفتار میتونه منطق if-else یا switch شما رو که انتظار مقادیر مشخصی رو دارن، کاملاً به هم بریزه.
4️⃣ راه حل: اعتبارسنجی با Enum.IsDefined ✅
برای اینکه مطمئن بشید یه مقدار Enum معتبره، میتونید از متد استاتیک Enum.IsDefined استفاده کنید.
BorderSide side = (BorderSide)12345;
bool isValid = Enum.IsDefined(typeof(BorderSide), side);
Console.WriteLine(isValid); // خروجی: False
🤔 حرف حساب و تجربه شما
درک این جزئیات و تلههای Enum، به شما کمک میکنه کدهای امنتر و قابل اعتمادتری بنویسید.
🔖 هشتگها:
#CSharp #DotNet #Enum #TypeSafety
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی با Enumهای استاندارد آشنا شدیم. اما اگه بخوایم یه متغیر بتونه ترکیبی از چند عضو Enum رو همزمان نگه داره چی؟ مثلاً دسترسیهای کاربر (خواندن، نوشتن، حذف کردن).
اینجاست که الگوی قدرتمند [Flags] وارد میشه.
[Flags]
یه اتریبیوته که به کامپایلر و ابزارها میگه اعضای این Enum رو میشه با هم مثل پرچمهای مختلف ترکیب کرد. این برای مدیریت گزینهها (options)، دسترسیها (permissions) یا هر وضعیتی که میتونه چند حالت همزمان داشته باشه، عالیه.
کلید اصلی این الگو، مقداردهی اعضا با توانهای ۲ هست (1, 2, 4, 8, 16, ...). این کار باعث میشه هر عضو، نماینده یک بیت منحصر به فرد در یک عدد باشه و بتونیم اونها رو بدون تداخل با هم ترکیب کنیم.
برای کار با Flags، از عملگرهای بیتی (Bitwise Operators) استفاده میکنیم:
🔹️ | (OR):
برای اضافه کردن یا ترکیب کردن چند فلگ.
🔹️ & (AND):
برای چک کردن اینکه آیا یه فلگ خاص در مجموعه وجود داره یا نه.
🔹️ ^ (XOR):
برای حذف کردن (toggle) یک فلگ اگه وجود داشته باشه.
اگه اتریبیوت
الگوی [Flags] یکی از اون ترفندهای کلاسیک و قدرتمند #C هست که به شما اجازه میده با حجم کمی از داده، اطلاعات زیادی رو به صورت بهینه مدیریت کنید.
🚩 الگوی [Flags] Enum در #C: مدیریت چندین گزینه با بیتها
تو پست قبلی با Enumهای استاندارد آشنا شدیم. اما اگه بخوایم یه متغیر بتونه ترکیبی از چند عضو Enum رو همزمان نگه داره چی؟ مثلاً دسترسیهای کاربر (خواندن، نوشتن، حذف کردن).
اینجاست که الگوی قدرتمند [Flags] وارد میشه.
1️⃣ [Flags] چیست و کی بهش نیاز داریم؟
[Flags]
یه اتریبیوته که به کامپایلر و ابزارها میگه اعضای این Enum رو میشه با هم مثل پرچمهای مختلف ترکیب کرد. این برای مدیریت گزینهها (options)، دسترسیها (permissions) یا هر وضعیتی که میتونه چند حالت همزمان داشته باشه، عالیه.
2️⃣ نحوه پیادهسازی: قدرت اعداد توان ۲
کلید اصلی این الگو، مقداردهی اعضا با توانهای ۲ هست (1, 2, 4, 8, 16, ...). این کار باعث میشه هر عضو، نماینده یک بیت منحصر به فرد در یک عدد باشه و بتونیم اونها رو بدون تداخل با هم ترکیب کنیم.
[Flags] // این اتریبیوت مهمه!
public enum BorderSides
{
None = 0,
Left = 1, // 2^0
Right = 2, // 2^1
Top = 4, // 2^2
Bottom = 8, // 2^3
// میتونیم ترکیبات رایج رو هم از قبل تعریف کنیم
LeftRight = Left | Right,
All = Left | Right | Top | Bottom
}
3️⃣ کار با Flags: عملگرهای بیتی
برای کار با Flags، از عملگرهای بیتی (Bitwise Operators) استفاده میکنیم:
🔹️ | (OR):
برای اضافه کردن یا ترکیب کردن چند فلگ.
🔹️ & (AND):
برای چک کردن اینکه آیا یه فلگ خاص در مجموعه وجود داره یا نه.
🔹️ ^ (XOR):
برای حذف کردن (toggle) یک فلگ اگه وجود داشته باشه.
// ترکیب کردن دو فلگ
BorderSides leftRight = BorderSides.Left | BorderSides.Right;
// چک کردن وجود یک فلگ
if ((leftRight & BorderSides.Left) != 0)
{
Console.WriteLine("Includes Left"); // اجرا میشه
}
// حذف یک فلگ
leftRight ^= BorderSides.Left; // حالا فقط Right باقی میمونه
// جادوی ToString() با اتریبیوت [Flags]
Console.WriteLine(leftRight.ToString()); // خروجی: Right
اگه اتریبیوت
[Flags] رو نذارید، ToString() به جای اسم اعضا، مقدار عددی اونها رو برمیگردونه.🤔 حرف حساب و تجربه شما
الگوی [Flags] یکی از اون ترفندهای کلاسیک و قدرتمند #C هست که به شما اجازه میده با حجم کمی از داده، اطلاعات زیادی رو به صورت بهینه مدیریت کنید.
🔖 هشتگها:
#CSharp #DotNet #Enum #Flags #Bitwise
📖 سری آموزشی کتاب C# 12 in a Nutshell
تا حالا شده یه کلاسی بنویسید که اونقدر به یه کلاس دیگه وابسته باشه که انگار باید جزئی از اون باشه؟ یا بخواید یه کلاس کمکی بسازید که فقط و فقط توسط یک کلاس دیگه استفاده میشه؟
سیشارپ برای این سناریوها یه راه حل خیلی تمیز و قدرتمند داره: Nested Types (تایپهای تودرتو).
یه Nested Type، یک تایپ (class, struct, enum, ...) است که داخل یک کلاس یا struct دیگه تعریف میشه. مثل یه جعبهی کوچیک، داخل یه جعبهی بزرگتر.
برای دسترسی به یک Nested Type از بیرون، باید اون رو با اسم کلاس بیرونی مشخص کنید:
چرا باید از یه Nested Type استفاده کنیم؟ چون چند تا قدرت ویژه دارن که کلاسهای معمولی ندارن:
مهمترین قدرتشون اینه که میتونن به اعضای private و protected کلاسی که داخلش هستن، دسترسی داشته باشن! این یعنی یه سطح کپسولهسازی فوقالعاده.
میتونن هر Access Modifierی داشته باشن (public, private, protected و...). این در حالیه که کلاسهای معمولی فقط میتونن public یا internal باشن. پیشفرض دسترسی برای Nested Typeها، private هست.
این مهمترین بخش ماجراست.
❌ دلیل اشتباه: استفاده از Nested Type برای جلوگیری از شلوغ شدن namespace. برای این کار از namespace تودرتو استفاده کنید.
✅ دلایل درست: فقط زمانی از Nested Type استفاده کنید که:
• کلاس داخلی شما به شدت به کلاس بیرونی وابسته است و به اعضای private اون نیاز مستقیم داره.
• میخواید با استفاده از Access Modifierها (مثل private یا protected)، اون کلاس رو از دنیای بیرون کاملاً مخفی کنید و به عنوان یه جزئیات پیادهسازی داخلی نگهش دارید.
Nested Types
یه ابزار قدرتمند برای کپسولهسازی پیشرفته هستن، به شرطی که به جا و درست ازشون استفاده بشه.
📦 کلاس در کلاس: راهنمای کامل Nested Types در #C
تا حالا شده یه کلاسی بنویسید که اونقدر به یه کلاس دیگه وابسته باشه که انگار باید جزئی از اون باشه؟ یا بخواید یه کلاس کمکی بسازید که فقط و فقط توسط یک کلاس دیگه استفاده میشه؟
سیشارپ برای این سناریوها یه راه حل خیلی تمیز و قدرتمند داره: Nested Types (تایپهای تودرتو).
1️⃣ Nested Type چیست؟
یه Nested Type، یک تایپ (class, struct, enum, ...) است که داخل یک کلاس یا struct دیگه تعریف میشه. مثل یه جعبهی کوچیک، داخل یه جعبهی بزرگتر.
public class TopLevel
{
public class Nested { } // کلاس تودرتو
public enum Color { Red, Blue, Tan } // enum تودرتو
}
برای دسترسی به یک Nested Type از بیرون، باید اون رو با اسم کلاس بیرونی مشخص کنید:
TopLevel.Nested n = new TopLevel.Nested();
TopLevel.Color color = TopLevel.Color.Red;
2️⃣ ابرقدرتهای Nested Types 🦸♂️
چرا باید از یه Nested Type استفاده کنیم؟ چون چند تا قدرت ویژه دارن که کلاسهای معمولی ندارن:
دسترسی به اعضای private: 🔐
مهمترین قدرتشون اینه که میتونن به اعضای private و protected کلاسی که داخلش هستن، دسترسی داشته باشن! این یعنی یه سطح کپسولهسازی فوقالعاده.
public class TopLevel
{
private static int x = 10;
class Nested
{
// Nested به فیلد private کلاس TopLevel دسترسی داره!
static void Foo() => Console.WriteLine(TopLevel.x);
}
}
سطوح دسترسی بیشتر: 🛡
میتونن هر Access Modifierی داشته باشن (public, private, protected و...). این در حالیه که کلاسهای معمولی فقط میتونن public یا internal باشن. پیشفرض دسترسی برای Nested Typeها، private هست.
3️⃣ قانون طلایی: کی از Nested Type استفاده کنیم؟ 🎯
این مهمترین بخش ماجراست.
❌ دلیل اشتباه: استفاده از Nested Type برای جلوگیری از شلوغ شدن namespace. برای این کار از namespace تودرتو استفاده کنید.
✅ دلایل درست: فقط زمانی از Nested Type استفاده کنید که:
• کلاس داخلی شما به شدت به کلاس بیرونی وابسته است و به اعضای private اون نیاز مستقیم داره.
• میخواید با استفاده از Access Modifierها (مثل private یا protected)، اون کلاس رو از دنیای بیرون کاملاً مخفی کنید و به عنوان یه جزئیات پیادهسازی داخلی نگهش دارید.
🤔 حرف حساب و تجربه شما
Nested Types
یه ابزار قدرتمند برای کپسولهسازی پیشرفته هستن، به شرطی که به جا و درست ازشون استفاده بشه.
🔖 هشتگها:
#CSharp #DotNet #OOP #Encapsulation
📖 سری آموزشی کتاب C# 12 in a Nutshell
سیشارپ دو تا مکانیزم اصلی برای نوشتن کدهای قابل استفاده مجدد (reusable) داره: وراثت و جنریکها. وراثت این کار رو با یک تایپ پایه انجام میده، ولی جنریکها با یک "قالب" که شامل "نوعهای جایگزین" (placeholder) هست، این کار رو میکنن.
امروز میخوایم ببینیم جنریکها چی هستن و چرا به وجود اومدن.
فرض کنید به یه ساختار داده مثل پشته (Stack) نیاز داریم. بدون جنریکها، دو راه بد پیش روی ما بود:
باید برای هر نوع دادهای که میخواستیم، یه کلاس جدا مینوشتیم (IntStack, StringStack, UserStack و...). این یعنی حجم وحشتناکی از کد تکراری.
میتونستیم یه ObjectStack بسازیم که هر نوعی رو قبول کنه (چون همه تایپها از object ارث میبرن). اما این روش دو تا مشکل بزرگ داشت که تو پست قبلی دیدیم:
• عدم ایمنی نوع (Type Safety): شما میتونستید به اشتباه یه string رو تو پشتهای که برای int ساخته بودید، Push کنید و کامپایلر هیچ خطایی نمیگرفت!
• هزینه پرفورمنس Boxing/Unboxing : برای کار با Value Typeها (مثل int)، باید مدام هزینه کپی کردن داده روی Heap (Boxing) و برگردوندنش (Unboxing) رو پرداخت میکردیم.
جنریکها دقیقاً برای حل این مشکل به وجود اومدن. به ما اجازه میدن یه "قالب" کد بنویسیم که هم قابل استفاده مجدد باشه (مثل ObjectStack) و هم ایمنی نوع و پرفورمنس بالا داشته باشه (مثل IntStack).
شما یک نوع جنریک با یک پارامتر T میسازید. بعداً موقع استفاده، به جای T، نوع داده واقعی خودتون رو قرار میدید.
حالا ببینیم این نسخه جنریک چطور مشکلات رو حل میکنه:
جنریکها یکی از قدرتمندترین قابلیتهای #C هستن و اساس تمام کالکشنهای مدرن داتنت (List<T>, Dictionary<TKey, TValue>) رو تشکیل میدن. تسلط بر اونها برای نوشتن کدهای قابل استفاده مجدد، امن و بهینه، ضروریه.
🧬 جادوی Generics در #C: نوشتن یک کد برای همه تایپها!
سیشارپ دو تا مکانیزم اصلی برای نوشتن کدهای قابل استفاده مجدد (reusable) داره: وراثت و جنریکها. وراثت این کار رو با یک تایپ پایه انجام میده، ولی جنریکها با یک "قالب" که شامل "نوعهای جایگزین" (placeholder) هست، این کار رو میکنن.
امروز میخوایم ببینیم جنریکها چی هستن و چرا به وجود اومدن.
1️⃣ مشکل چه بود؟ (چرا به جنریکها نیاز داریم؟)
فرض کنید به یه ساختار داده مثل پشته (Stack) نیاز داریم. بدون جنریکها، دو راه بد پیش روی ما بود:
راه حل اول: تکرار کد 👎
باید برای هر نوع دادهای که میخواستیم، یه کلاس جدا مینوشتیم (IntStack, StringStack, UserStack و...). این یعنی حجم وحشتناکی از کد تکراری.
راه حل دوم: استفاده از object 👎
میتونستیم یه ObjectStack بسازیم که هر نوعی رو قبول کنه (چون همه تایپها از object ارث میبرن). اما این روش دو تا مشکل بزرگ داشت که تو پست قبلی دیدیم:
• عدم ایمنی نوع (Type Safety): شما میتونستید به اشتباه یه string رو تو پشتهای که برای int ساخته بودید، Push کنید و کامپایلر هیچ خطایی نمیگرفت!
• هزینه پرفورمنس Boxing/Unboxing : برای کار با Value Typeها (مثل int)، باید مدام هزینه کپی کردن داده روی Heap (Boxing) و برگردوندنش (Unboxing) رو پرداخت میکردیم.
2️⃣ جنریکها (Generics): بهترینهای هر دو دنیا ✨
جنریکها دقیقاً برای حل این مشکل به وجود اومدن. به ما اجازه میدن یه "قالب" کد بنویسیم که هم قابل استفاده مجدد باشه (مثل ObjectStack) و هم ایمنی نوع و پرفورمنس بالا داشته باشه (مثل IntStack).
شما یک نوع جنریک با یک پارامتر T میسازید. بعداً موقع استفاده، به جای T، نوع داده واقعی خودتون رو قرار میدید.
public class Stack<T> // T یک نوع جایگزین است
{
int position;
T[] data = new T[100]; // آرایه ما حالا از نوع T است
public void Push(T obj) => data[position++] = obj;
public T Pop() => data[--position];
}
3️⃣ مزایای بزرگ در عمل
حالا ببینیم این نسخه جنریک چطور مشکلات رو حل میکنه:
var stack = new Stack<int>();
stack.Push(5);
stack.Push(10);
// stack.Push("hello"); // ❌ خطای زمان کامپایل! کامپایلر به شما ایمنی نوع کامل میده.
int x = stack.Pop(); // ✅ بدون نیاز به کست. بدون هزینه Unboxing. (x is 10)
int y = stack.Pop(); // ✅ (y is 5)
🤔 حرف حساب و تجربه شما
جنریکها یکی از قدرتمندترین قابلیتهای #C هستن و اساس تمام کالکشنهای مدرن داتنت (List<T>, Dictionary<TKey, TValue>) رو تشکیل میدن. تسلط بر اونها برای نوشتن کدهای قابل استفاده مجدد، امن و بهینه، ضروریه.
🔖 هشتگها:
#CSharp #DotNet #OOP #Generics
معماری برش عمودی (Vertical Slice Architecture) 🔪
معماریهای لایهای، پایه و اساس بسیاری از سیستمهای نرمافزاری هستند. با این حال، معماریهای لایهای سیستم را حول لایههای فنی سازماندهی میکنند. و انسجام (cohesion) بین لایهها پایین است.
چه میشد اگر به جای آن، میخواستید سیستم را حول ویژگیها (features) سازماندهی کنید؟ 🤔
کوپلینگ (coupling) بین ویژگیهای نامرتبط را به حداقل برسانید و کوپلینگ را در یک ویژگی واحد به حداکثر برسانید.
امروز میخواهم در مورد معماری برش عمودی صحبت کنم، که دقیقاً همین کار را انجام میدهد.
مشکل معماریهای لایهای 👎
معماریهای لایهای، سیستم نرمافزاری را به لایهها یا طبقات (tiers) سازماندهی میکنند. هر یک از لایهها معمولاً یک پروژه در سولوشن شماست. برخی از پیادهسازیهای محبوب، معماری N-tier یا معماری تمیز (Clean architecture) هستند.
معماریهای لایهای بر روی تفکیک دغدغههای (separating the concerns) کامپوننتهای مختلف تمرکز میکنند. این کار، درک و نگهداری سیستم نرمافزاری را آسانتر میکند. و مزایای زیادی برای طراحی نرمافزار ساختاریافته وجود دارد ✅، مانند قابلیت نگهداری، انعطافپذیری و اتصال سست (loose coupling).