توزیعکننده (Dispatcher) (Strongly Typed) 🧠
حالا به چیزی نیاز داریم که فراخوانی handlerها را هماهنگ کند.
public interface IDomainEventsDispatcher
{
Task DispatchAsync(IEnumerable<IDomainEvent> domainEvents, CancellationToken cancellationToken = default);
}
internal sealed class DomainEventsDispatcher(IServiceProvider serviceProvider)
: IDomainEventsDispatcher
{
// ... (پیادهسازی پیچیده با Wrapper برای جلوگیری از reflection در زمان اجرا)
}
این توزیعکننده از یک wrapper برای حذف reflection در حین اجرای handler استفاده میکند در حالی که ایمنی نوع را حفظ میکند. این به ما مزایای عملکردی اجتناب از reflection در مسیر اصلی (اجرای handler) را میدهد. فراموش نکنید که توزیعکننده را با DI ثبت کنید.
مثال استفاده 🎯
در اینجا نحوه استفاده از توزیعکننده رویدادهای دامنه در اپلیکیشن شما آمده است:
public class UserController(...) : ControllerBase
{
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterUserRequest request)
{
// کاربر را ایجاد کن
var user = await userService.CreateUserAsync(request.Email, request.Password);
// رویداد دامنه را منتشر کن
var userRegisteredEvent = new UserRegisteredDomainEvent(user.Id, user.Email);
await domainEventsDispatcher.DispatchAsync([userRegisteredEvent]);
return Ok(...);
}
}
محدودیتها و مزایا و معایب ⚠️
این پیادهسازی کاملاً درون-فرآیندی (in-process) اجرا میشود، که پیامدهای مهمی دارد:
• بازخورد فوری: اگر هر handler شکست بخورد، استثنا بلافاصله به فراخواننده برمیگردد.
• کنترل فراخواننده: کدی که رویدادها را توزیع میکند، تصمیم میگیرد چگونه با شکستها برخورد کند.
• نگرانیهای قابلیت اطمینان: اگر فرآیند پس از موفقیت برخی handlerها اما قبل از تکمیل دیگران کرش کند، بازیابی خودکار وجود ندارد.
برای عوارض جانبی حیاتی که نباید از دست بروند، الگوی Outbox 📬 را در نظر بگیرید.
جمعبندی 📝
Domain events
یک الگوی قدرتمند برای جداسازی منطق بیزینس هستند و شما برای استفاده مؤثر از آنها به یک فریمورک سنگین نیاز ندارید. پیادهسازیای که ما در اینجا ساختیم، یک پایه محکم فراهم میکند.
زیبایی ساختن راهحل خودتان این است که شما هر قطعه را درک میکنید، که دیباگ و سفارشیسازی را ساده میکند. این الگو به طور عالی در سیستمهای Domain-Driven Design و Clean Architecture که در آنها جداسازی منطق بیزینس حیاتی است، جای میگیرد.
بینش کلیدی، درک مزایا و معایب خود از پیش است، نه کشف آنها در پروداکشن. ساده شروع کنید، آنچه مهم است را اندازهگیری کنید و بر اساس نیازمندیهای واقعی تکامل پیدا کنید.
🔖 هشتگها:
#CSharp #SoftwareArchitecture #DomainDrivenDesign #DDD #DomainEvents #CleanArchitecture #CQRS
📖 سری آموزشی کتاب C# 12 in a Nutshell
تا حالا شده یه فایل کد اونقدر بزرگ بشه که پیدا کردن یه متد توش عذابآور باشه؟ یا بخواید کدی که خودتون نوشتید رو از کدی که به صورت خودکار توسط یه ابزار (مثل دیزاینر) تولید شده، جدا کنید؟
سیشارپ برای این کار یه راه حل عالی و تمیز داره: کلمه کلیدی partial.
یک کلاس در چند فایل کلمه کلیدی partial به شما اجازه میده تعریف یک کلاس، struct یا اینترفیس رو در چند فایل مختلف تقسیم کنید. کامپایلر موقع کامپایل، تمام این تیکهها رو به هم میچسبونه و به عنوان یک کلاس واحد در نظر میگیره.
مهمترین کاربرد: جداسازی کدهای دستنویس شما از کدهای اتوماتیک.
فایل ۱ (اتوماتیک): PaymentForm.g.cs
فایل ۲ (دستنویس): PaymentForm.cs
این قابلیت به شما اجازه میده در یک بخش از کلاس (معمولاً کد اتوماتیک)، یک "قلاب" یا تعریف متد بدون بدنه بذارید. بعداً در بخش دیگه کلاس (کد دستنویس)، میتونید اون متد رو پیادهسازی کنید.
💡نکته جادویی: اگه شما هیچ پیادهسازیای براش ارائه ندید، کامپایلر هم تعریف و هم تمام فراخوانیهای اون متد رو از کد نهایی حذف میکنه! این یعنی هیچ هزینه پرفورمنسی نداره.
فایل ۱ (اتوماتیک):
فایل ۲ (دستنویس):
این نسخه جدیدتر، برای کار با Source Generatorها طراحی شده. در این حالت، شما تعریف متد رو مینویسید و انتظار دارید که Source Generator بدنه اون رو تولید کنه. این متدها دیگه اختیاری نیستن و باید پیادهسازی بشن و میتونن خروجی و پارامتر out هم داشته باشن.
🏗 تقسیم کدهای بزرگ با partial در #C: کلاسها و متدهای چندتکه
تا حالا شده یه فایل کد اونقدر بزرگ بشه که پیدا کردن یه متد توش عذابآور باشه؟ یا بخواید کدی که خودتون نوشتید رو از کدی که به صورت خودکار توسط یه ابزار (مثل دیزاینر) تولید شده، جدا کنید؟
سیشارپ برای این کار یه راه حل عالی و تمیز داره: کلمه کلیدی partial.
1️⃣ کلاسهای Partial:
یک کلاس در چند فایل کلمه کلیدی partial به شما اجازه میده تعریف یک کلاس، struct یا اینترفیس رو در چند فایل مختلف تقسیم کنید. کامپایلر موقع کامپایل، تمام این تیکهها رو به هم میچسبونه و به عنوان یک کلاس واحد در نظر میگیره.
مهمترین کاربرد: جداسازی کدهای دستنویس شما از کدهای اتوماتیک.
فایل ۱ (اتوماتیک): PaymentForm.g.cs
// این بخش توسط یک ابزار ساخته شده
public partial class PaymentForm
{
// ... کنترلهای دیزاینر و کدهای اتوماتیک
}
فایل ۲ (دستنویس): PaymentForm.cs
// این بخش رو شما مینویسید
public partial class PaymentForm
{
// ... منطق و ایونتهای مربوط به فرم
}
2️⃣ متدهای Partial: قلابهای جادویی که ناپدید میشن! 🎩
این قابلیت به شما اجازه میده در یک بخش از کلاس (معمولاً کد اتوماتیک)، یک "قلاب" یا تعریف متد بدون بدنه بذارید. بعداً در بخش دیگه کلاس (کد دستنویس)، میتونید اون متد رو پیادهسازی کنید.
💡نکته جادویی: اگه شما هیچ پیادهسازیای براش ارائه ندید، کامپایلر هم تعریف و هم تمام فراخوانیهای اون متد رو از کد نهایی حذف میکنه! این یعنی هیچ هزینه پرفورمنسی نداره.
فایل ۱ (اتوماتیک):
public partial class PaymentForm
{
public void Submit()
{
// یه قلاب برای اعتبارسنجی قبل از ارسال
ValidatePayment(this.Amount);
// ...
}
// تعریف متد partial (بدون بدنه)
partial void ValidatePayment(decimal amount);
}
فایل ۲ (دستنویس):
public partial class PaymentForm
{
// پیادهسازی متد partial
partial void ValidatePayment(decimal amount)
{
if (amount > 1000)
{
// ... منطق اعتبارسنجی ...
}
}
}
3️⃣ متدهای Partial توسعهیافته (از 9 #C) 🚀
این نسخه جدیدتر، برای کار با Source Generatorها طراحی شده. در این حالت، شما تعریف متد رو مینویسید و انتظار دارید که Source Generator بدنه اون رو تولید کنه. این متدها دیگه اختیاری نیستن و باید پیادهسازی بشن و میتونن خروجی و پارامتر out هم داشته باشن.
🔖 هشتگها:
#CSharp #Programming #DotNet #CleanCode #Partial
5 شیوه برتر (Best Practice) برای لاگینگ ساختاریافته بهتر با Serilog 📝
سریلاگ یک کتابخانه لاگینگ ساختاریافته (structured logging) برای NET. است.Serilog از مقصدهای لاگینگ زیادی به نام Sinks پشتیبانی میکند. مقصدهای لاگ از سینکهای کنسول و فایل گرفته تا سرویسهای مدیریت شده لاگینگ مانند Application Insights را شامل میشود.
امروز، میخواهم ۵ نکته عملی برای لاگینگ ساختاریافته بهتر با Serilog را به اشتراک بگذارم.
1️⃣ از سیستم پیکربندی (Configuration System) استفاده کنید ⚙️
شما میتوانید Serilog را در ASP.NET Core به دو روش پیکربندی کنید:
سیستم پیکربندی _ Fluent API
Fluent API
به شما امکان میدهد با کد، Serilog را به راحتی پیکربندی کنید. عیب آن این است که شما پیکربندی خود را هاردکد میکنید. هرگونه تغییر در پیکربندی نیازمند دیپلوی یک نسخه جدید است.
من ترجیح میدهم از سیستم پیکربندی ASP.NET برای راهاندازی Serilog استفاده کنم. مزیت آن این است که میتوانید پیکربندی لاگینگ را بدون دیپلوی مجدد اپلیکیشن خود تغییر دهید.
شما باید کتابخانه Serilog.Settings.Configuration را نصب کنید.
این به شما امکان میدهد Serilog را با استفاده از سیستم پیکربندی، کانفیگ کنید:
builder.Host.UseSerilog((context, loggerConfig) =>
loggerConfig.ReadFrom.Configuration(context.Configuration));
در اینجا یک پیکربندی Serilog با سینکهای Console و Seq آمده است. ما همچنین چند Serilog enricher را برای غنیسازی لاگهای اپلیکیشن با اطلاعات اضافی پیکربندی میکنیم.
{
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.Seq"],
"MinimumLevel": {
"Default": "Information",
"Override": { "Microsoft": "Information" }
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "Seq",
"Args": { "serverUrl": "http://localhost:5341" }
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
}
}2️⃣ از لاگینگ درخواست Serilog استفاده کنید 🌐
شما میتوانید کتابخانه Serilog.AspNetCore را نصب کنید تا لاگینگ Serilog را برای پایپلاین درخواست ASP.NET Core اضافه کنید. این قابلیت، عملیات داخلی ASP.NET را به همان سینکهای Serilog که رویدادهای اپلیکیشن شما ثبت میشوند، اضافه میکند.
تنها کاری که باید انجام دهید، فراخوانی متد UseSerilogRequestLogging است:
app.UseSerilogRequestLogging();
3️⃣ لاگهای خود را با CorrelationId غنیسازی کنید 🔗
چگونه میتوانید تمام لاگهای مربوط به یک درخواست واحد را ردیابی کنید؟
شما میتوانید یک پراپرتی CorrelationId به لاگهای ساختاریافته خود اضافه کنید.
این همچنین در چندین اپلیکیشن کار میکند. شما باید CorrelationId را با استفاده از یک هدر HTTP پاس دهید. برای مثال، میتوانید از یک هدر سفارشی X-Correlation-Id استفاده کنید.
در RequestContextLoggingMiddleware، من CorrelationId را به LogContext Serilog اضافه میکنم. این کار آن را برای تمام لاگهای ایجاد شده در طول این درخواست اپلیکیشن در دسترس قرار میدهد.
public class RequestContextLoggingMiddleware
{
private const string CorrelationIdHeaderName = "X-Correlation-Id";
// ...
public Task Invoke(HttpContext context)
{
string correlationId = GetCorrelationId(context);
using (LogContext.PushProperty("CorrelationId", correlationId))
{
return _next.Invoke(context);
}
}
// ...
}
4️⃣ رویدادهای مهم اپلیکیشن را لاگ کنید 📌
به طور کلی، من سعی میکنم رویدادهای مهم را در اپلیکیشن خود لاگ کنم. این شامل اطلاعات درخواست فعلی، خطاها، شکستها، مقادیر غیرمنتظره، نقاط انشعاب و غیره است.
اگر از الگوی CQRS با MediatR استفاده میکنید، میتوانید به راحتی لاگینگ را برای تمام درخواستهای اپلیکیشن اضافه کنید.
در RequestLoggingPipelineBehavior من پراپرتی Error را به LogContext پوش میکنم.
internal sealed class RequestLoggingPipelineBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : class
where TResponse : Result
{
// ...
public async Task<TResponse> Handle(...)
{
// ...
TResponse result = await next();
if (result.IsSuccess) { /* ... */ }
else
{
using (LogContext.PushProperty("Error", result.Error, true))
{
_logger.LogError(
"Completed request {RequestName} with error", requestName);
}
}
return result;
}
}
5️⃣ از Seq برای توسعه محلی استفاده کنید 🔍
Seq
یک سرور جستجو، تحلیل و هشدار self-hosted است که برای دادههای لاگ ساختاریافته ساخته شده است. استفاده از آن برای توسعه محلی رایگان است. این ابزار قابلیتهای جستجو و فیلترینگ پیشرفتهای را روی دادههای لاگ ساختاریافته ارائه میدهد.
میتوانید یک نمونه Seq را در یک کانتینر Docker بالا بیاورید:
version: '3.4'
services:
seq:
image: datalust/seq:latest
container_name: seq
environment:
- ACCEPT_EULA=Y
ports:
- 5341:5341
- 8081:80
`
خلاصه ✅
لاگهای ساختاریافته از یک ساختار یکسان پیروی میکنند و چون قابل خواندن توسط ماشین هستند، میتوانید آنها را برای اطلاعات خاص جستجو کنید. آنها زمینه و جزئیات بیشتری در مورد خطاهای اپلیکیشن ارائه میدهند و شناسایی و رفع مشکلات را آسانتر میکنند.
شما میتوانید از LogContext قدرتمند Serilog برای غنیسازی لاگهای خود با یک CorrelationId استفاده کنید.
🔖 هشتگها:
#CSharp #DotNet #Logging #StructuredLogging #Observability #Serilog
📖 سری آموزشی کتاب C# 12 in a Nutshell
تا حالا شده اسم یه پراپرتی رو تو یه رشته بنویسید (مثلاً برای Exceptionها یا INotifyPropertyChanged) و بعداً که اسم اون پراپرتی رو عوض میکنید، یادتون بره اون رشته رو هم آپدیت کنید و کل برنامه به باگ بخوره؟
به این رشتهها میگن "رشتههای جادویی" (Magic Strings) و یکی از منابع اصلی باگهای پنهان هستن. #C برای حل این مشکل، یه اپراتور خیلی ساده و قدرتمند داره.
اپراتور nameof اسم هرچیزی (متغیر، متد، کلاس، پراپرتی و...) رو در زمان کامپایل به یه رشته تبدیل میکنه.
مزیت اصلیش چیه؟ چک شدن در زمان کامپایل! ✅
اگه شما اسم اون متغیر یا پراپرتی رو عوض کنید (Refactor)، ویژوال استودیو به صورت خودکار تمام nameofهای مربوط به اون رو هم آپدیت میکنه و دیگه هیچوقت کد شما به خاطر یه رشته قدیمی، به باگ نمیخوره.
2️⃣ نحوه استفاده
برای متغیرهای محلی:
برای اعضای یک تایپ:
🪄 خداحافظی با رشتههای جادویی: قدرت nameof در #C
تا حالا شده اسم یه پراپرتی رو تو یه رشته بنویسید (مثلاً برای Exceptionها یا INotifyPropertyChanged) و بعداً که اسم اون پراپرتی رو عوض میکنید، یادتون بره اون رشته رو هم آپدیت کنید و کل برنامه به باگ بخوره؟
به این رشتهها میگن "رشتههای جادویی" (Magic Strings) و یکی از منابع اصلی باگهای پنهان هستن. #C برای حل این مشکل، یه اپراتور خیلی ساده و قدرتمند داره.
1️⃣ راه حل: اپراتور nameof
اپراتور nameof اسم هرچیزی (متغیر، متد، کلاس، پراپرتی و...) رو در زمان کامپایل به یه رشته تبدیل میکنه.
مزیت اصلیش چیه؟ چک شدن در زمان کامپایل! ✅
اگه شما اسم اون متغیر یا پراپرتی رو عوض کنید (Refactor)، ویژوال استودیو به صورت خودکار تمام nameofهای مربوط به اون رو هم آپدیت میکنه و دیگه هیچوقت کد شما به خاطر یه رشته قدیمی، به باگ نمیخوره.
2️⃣ نحوه استفاده
برای متغیرهای محلی:
int userCount = 123;
string variableName = nameof(userCount); // مقدار: "userCount"
برای اعضای یک تایپ:
// برای اعضای استاتیک و غیراستاتیک کار میکنه
string lengthPropName = nameof(StringBuilder.Length); // مقدار: "Length"
// برای گرفتن اسم کامل
string fullName = nameof(StringBuilder) + "." + nameof(StringBuilder.Length);
// "StringBuilder.Length"
🔖 هشتگها:
#CleanCode #Refactoring
📖 سری آموزشی کتاب C# 12 in a Nutshell
آمادهاید که به قلب برنامهنویسی شیءگرا (OOP) سفر کنیم؟ امروز میخوایم سه تا از مهمترین و به هم پیوستهترین مفاهیم #C رو کالبدشکافی کنیم: وراثت، چندریختی و کستینگ.
تسلط بر این سه مفهوم، پایه و اساس ساختن سیستمهای انعطافپذیر و قدرتمنده.
1️⃣ وراثت (Inheritance): "یک نوع از..." 👨👩👧
وراثت به یک کلاس اجازه میده که ویژگیها و رفتارهای یک کلاس دیگه رو به ارث ببره. به کلاس اصلی میگیم کلاس پایه (Base Class) و به کلاس جدید میگیم کلاس مشتقشده (Derived Class). این یعنی "کلاس مشتقشده، یک نوع از کلاس پایه است".
حالا Stock و House هم پراپرتی Name رو دارن و هم پراپرتیهای مخصوص خودشون.
چندریختی یعنی شما میتونید یه متغیر از نوع کلاس پایه داشته باشید، ولی در زمان اجرا، به اون یک نمونه از هر کدوم از کلاسهای فرزندش رو اختصاص بدید. این قابلیت به شما اجازه میده کدهای خیلی انعطافپذیری بنویسید.
مثال: این متد یک Asset به عنوان ورودی میگیره، ولی شما میتونید هم Stock و هم House رو بهش پاس بدید!
کستینگ یعنی تغییر نوع یک رفرنس برای دسترسی به اعضای مختلف. دو نوع اصلی داریم:
این کار همیشه امنه و به صورت ضمنی (implicit) انجام میشه. شما یک نمونه از کلاس فرزند رو در یک متغیر از نوع پدر قرار میدید. بعد از این کار، شما به آبجکت از یک زاویه دید "محدودتر" نگاه میکنید و فقط به اعضایی که در کلاس پدر تعریف شدن، دسترسی دارید.
این کار میتونه خطرناک باشه و باید به صورت صریح (explicit) با (type) انجام بشه. شما به کامپایلر میگید: "من مطمئنم این آبجکت که از نوع پدره، در واقع یک نمونه از نوع فرزند هست". اگه اشتباه کرده باشید، در زمان اجرا با خطای InvalidCastException مواجه میشید.
درک درست Upcasting و Downcasting، کلید استفاده امن و قدرتمند از چندریختی در کدهای شماست. (در آینده با روشهای امنتر Downcasting مثل as و is آشنا میشیم).
بهترین مثالی که از قدرت چندریختی تو پروژههاتون دیدید چی بوده؟ آیا تا حالا به خطای InvalidCastException به خاطر یه Downcast اشتباه برخورد کردید؟
🏛 ستونهای OOP در #C: وراثت، چندریختی و کستینگ
آمادهاید که به قلب برنامهنویسی شیءگرا (OOP) سفر کنیم؟ امروز میخوایم سه تا از مهمترین و به هم پیوستهترین مفاهیم #C رو کالبدشکافی کنیم: وراثت، چندریختی و کستینگ.
تسلط بر این سه مفهوم، پایه و اساس ساختن سیستمهای انعطافپذیر و قدرتمنده.
1️⃣ وراثت (Inheritance): "یک نوع از..." 👨👩👧
وراثت به یک کلاس اجازه میده که ویژگیها و رفتارهای یک کلاس دیگه رو به ارث ببره. به کلاس اصلی میگیم کلاس پایه (Base Class) و به کلاس جدید میگیم کلاس مشتقشده (Derived Class). این یعنی "کلاس مشتقشده، یک نوع از کلاس پایه است".
// این کلاس پایه ماست
public class Asset
{
public string Name;
}
// این دو کلاس، از Asset ارثبری میکنند
public class Stock : Asset
{
public long SharesOwned;
}
public class House : Asset
{
public decimal Mortgage;
}
حالا Stock و House هم پراپرتی Name رو دارن و هم پراپرتیهای مخصوص خودشون.
🎭 جادوی چندریختی (Polymorphism) در
C# : Upcasting و Downcasting
2️⃣ چندریختی (Polymorphism): یک متغیر، چند چهره
چندریختی یعنی شما میتونید یه متغیر از نوع کلاس پایه داشته باشید، ولی در زمان اجرا، به اون یک نمونه از هر کدوم از کلاسهای فرزندش رو اختصاص بدید. این قابلیت به شما اجازه میده کدهای خیلی انعطافپذیری بنویسید.
مثال: این متد یک Asset به عنوان ورودی میگیره، ولی شما میتونید هم Stock و هم House رو بهش پاس بدید!
// کلاسهای پست قبلی
public class Asset { public string Name; }
public class Stock : Asset { public long SharesOwned; }
public class House : Asset { public decimal Mortgage; }
// این متد به لطف چندریختی، با هر نوع Asset کار میکنه
public static void Display(Asset asset)
{
Console.WriteLine(asset.Name);
}
// --- نحوه استفاده ---
Stock msft = new Stock { Name = "MSFT" };
House mansion = new House { Name = "Mansion" };
Display(msft); // خروجی: MSFT
Display(mansion); // خروجی: Mansion
3️⃣ کستینگ (Casting): تغییر زاویه دید به آبجکت 🔄
کستینگ یعنی تغییر نوع یک رفرنس برای دسترسی به اعضای مختلف. دو نوع اصلی داریم:
Upcasting (تبدیل فرزند به پدر) ⬆️
این کار همیشه امنه و به صورت ضمنی (implicit) انجام میشه. شما یک نمونه از کلاس فرزند رو در یک متغیر از نوع پدر قرار میدید. بعد از این کار، شما به آبجکت از یک زاویه دید "محدودتر" نگاه میکنید و فقط به اعضایی که در کلاس پدر تعریف شدن، دسترسی دارید.
Stock msft = new Stock { Name = "MSFT", SharesOwned = 1000 };
Asset a = msft; // Upcast (ضمنی و همیشه امن)
Console.WriteLine(a.Name); // ✅ درسته، چون Name در Asset هست
// Console.WriteLine(a.SharesOwned); // ❌ خطای زمان کامپایل! a از نوع Asset است و SharesOwned را نمیشناسد.Downcasting (تبدیل پدر به فرزند) ⬇️
این کار میتونه خطرناک باشه و باید به صورت صریح (explicit) با (type) انجام بشه. شما به کامپایلر میگید: "من مطمئنم این آبجکت که از نوع پدره، در واقع یک نمونه از نوع فرزند هست". اگه اشتباه کرده باشید، در زمان اجرا با خطای InvalidCastException مواجه میشید.
Stock msft = new Stock();
Asset a = msft; // Upcast
// Downcast موفقیتآمیز
Stock s = (Stock)a;
Console.WriteLine(s.SharesOwned); // <بدون خطا>
// --- مثال خطا ---
House h = new House();
Asset a2 = h; // Upcast
// ❌ Downcast ناموفق! a2 یک Stock نیست.
// Stock s2 = (Stock)a2; // در زمان اجرا خطای InvalidCastException میدهد
🤔 حرف حساب و تجربه شما
درک درست Upcasting و Downcasting، کلید استفاده امن و قدرتمند از چندریختی در کدهای شماست. (در آینده با روشهای امنتر Downcasting مثل as و is آشنا میشیم).
بهترین مثالی که از قدرت چندریختی تو پروژههاتون دیدید چی بوده؟ آیا تا حالا به خطای InvalidCastException به خاطر یه Downcast اشتباه برخورد کردید؟
🔖 هشتگها:
#OOP #Polymorphism #Casting
معماری برش عمودی (Vertical Slice): ساختاربندی برشهای عمودی 🔪
از سازماندهی پروژه خود در لایههای مختلف خسته شدهاید؟ 😫
معماری برش عمودی (VSA) یک جایگزین قانعکننده برای معماریهای لایهای سنتی است. VSA فیلمنامه را در مورد نحوه ساختاردهی کد ما برعکس میکند.
به جای لایههای افقی (Presentation, Application, Domain)، VSA کد را بر اساس ویژگی (feature) سازماندهی میکند. هر ویژگی همه چیز مورد نیاز خود را، از endpointهای API گرفته تا دسترسی به داده، در بر میگیرد.
در این مقاله، ما بررسی خواهیم کرد که چگونه میتوانید برشهای عمودی را در VSA ساختاربندی کنید.
درک برشهای عمودی 🧩
در هسته خود، یک برش عمودی یک واحد مستقل از عملکرد را نشان میدهد. این یک برش از طریق کل پشته اپلیکیشن است. این برش، تمام کدها و کامپوننتهای لازم برای تحقق یک ویژگی خاص را کپسوله میکند.
در معماریهای لایهای سنتی، کد به صورت افقی در لایههای مختلف سازماندهی میشود. پیادهسازی یک ویژگی میتواند در چندین لایه پراکنده شود. تغییر یک ویژگی نیازمند اصلاح کد در چندین لایه است.
VSA
با گروهبندی تمام کدها برای یک ویژگی در یک برش واحد، به این مشکل رسیدگی میکند.
📌این تغییر دیدگاه چندین مزیت به همراه دارد:
✅ انسجام بهبود یافته: کدهای مربوط به یک ویژگی خاص در کنار هم قرار میگیرند، که درک، اصلاح و تست آن را آسانتر میکند.
✅ پیچیدگی کاهش یافته: VSA با اجتناب از نیاز به پیمایش چندین لایه، مدل ذهنی اپلیکیشن شما را ساده میکند.
✅ تمرکز بر منطق بیزینس: ساختار به طور طبیعی بر روی مورد استفاده بیزینس تأکید میکند تا جزئیات پیادهسازی فنی.
✅ نگهداری آسانتر: تغییرات در یک ویژگی، در داخل برش آن محلیسازی میشوند و ریسک عوارض جانبی ناخواسته را کاهش میدهند.
پیادهسازی معماری برش عمودی 👨💻
در اینجا یک مثال از برش عمودی که ویژگی CreateProduct را نشان میدهد، آمده است. ما از یک کلاس استاتیک برای نمایش ویژگی و گروهبندی انواع مرتبط استفاده میکنیم.
public static class CreateProduct
{
public record Request(string Name, decimal Price);
public record Response(int Id, string Name, decimal Price);
public class Endpoint : IEndpoint
{
public void MapEndpoint(IEndpointRouteBuilder app)
app.MapPost("products", Handler).WithTags("Products");
}
public static IResult Handler(Request request, AppDbContext context)
{
var product = new Product
{
Name = request.Name,
Price = request.Price
};
context.Products.Add(product);
context.SaveChanges();
return Results.Ok(
new Response(product.Id, product.Name, product.Price));
}
}
}
کد برای کل ویژگی CreateProduct به طور فشرده در یک فایل واحد گروهبندی شده است. این کار مکانیابی، درک و اصلاح همه چیز مربوط به این عملکرد را بسیار آسان میکند. ما نیازی به پیمایش چندین لایه (مانند کنترلرها، سرویسها، ریپازیتوریها و غیره) نداریم.
معرفی اعتبارسنجی (Validation) در برشهای عمودی 🛡
برشهای عمودی معمولاً نیاز به حل برخی دغدغههای مشترک (cross-cutting concerns) دارند که یکی از آنها اعتبارسنجی است. ما میتوانیم به راحتی اعتبارسنجی را با کتابخانه FluentValidation پیادهسازی کنیم.
public static class CreateProduct
{
public record Request(string Name, decimal Price);
public record Response(int Id, string Name, decimal Price);
public class Validator : AbstractValidator<Request> { /* ... */ }
public class Endpoint : IEndpoint
{
public void MapEndpoint(IEndpointRouteBuilder app)
{
app.MapPost("products", Handler).WithTags("Products");
}
public static async Task<IResult> Handler(
Request request,
IValidator<Request> validator,
AppDbContext context)
{
var validationResult = await validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
return Results.BadRequest(validationResult.Errors);
}
// ... (ایجاد محصول و بازگرداندن پاسخ)
}
}
}
مدیریت ویژگیهای پیچیده و منطق مشترک 🧠
VSA
در مدیریت ویژگیهای مستقل برتری دارد. با این حال، اپلیکیشنهای دنیای واقعی اغلب شامل تعاملات پیچیده و منطق مشترک هستند.
در اینجا چند استراتژی وجود دارد که میتوانید در نظر بگیرید:
🔹️ تجزیه (Decomposition): ویژگیهای پیچیده را به برشهای عمودی کوچکتر و قابل مدیریتتر تقسیم کنید.
🔹️ بازآرایی (Refactoring): وقتی یک برش عمودی نگهداریاش دشوار میشود، از تکنیکهای بازآرایی مانند Extract method و Extract class استفاده کنید.
🔹️ استخراج منطق مشترک: منطق مشترکی که در چندین ویژگی استفاده میشود را شناسایی کنید. یک کلاس جداگانه (یا متد توسعه) برای ارجاع به آن از برشهای عمودی خود ایجاد کنید.
🔹️ انتقال منطق به پایین: برشهای عمودی را با استفاده از کد رویهای، مانند یک Transaction Script بنویسید. سپس، میتوانید بخشهایی از منطق بیزینس را که به طور طبیعی به انتیتیهای دامین تعلق دارند، شناسایی کنید.
خلاصه 📝
معماری برش عمودی بیش از یک راه برای ساختاردهی کد شماست. با تمرکز بر ویژگیها، VSA به شما اجازه میدهد اپلیکیشنهای منسجم و قابل نگهداری ایجاد کنید. برشهای عمودی مستقل هستند و تست واحد و یکپارچهسازی را سادهتر میکنند.
تغییرات محلیسازی میشوند، ریسک رگرسیونها را کاهش داده و امکان تکرارهای سریعتر را فراهم میکنند.
معماری برش عمودی را در پروژه بعدی خود در نظر بگیرید. این یک تغییر ذهنی بزرگ از معماری تمیز است. با این حال، هر دو جایگاه خود را دارند و حتی ایدههای مشابهی را به اشتراک میگذارند.
🔖 هشتگها:
#SoftwareArchitecture #VerticalSliceArchitecture #CQRS
📌5 books that made me a better software engineer:
1️⃣ Clean Architecture, Robert Martin
2️⃣ Domain-Driven Design, Eric Evans
3️⃣ Building Microservices, Sam Newman
4️⃣ Designing Data-Intensive Applications, Martin Kleppmann
5️⃣ Patterns of Enterprise Application Architecture, Martin Fowler
Which book made you a better software engineer?🤔
1️⃣ Clean Architecture, Robert Martin
2️⃣ Domain-Driven Design, Eric Evans
3️⃣ Building Microservices, Sam Newman
4️⃣ Designing Data-Intensive Applications, Martin Kleppmann
5️⃣ Patterns of Enterprise Application Architecture, Martin Fowler
Which book made you a better software engineer?🤔
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی دیدیم که Downcasting (تبدیل پدر به فرزند) با (Stock)a میتونه خطرناک باشه و اگه اشتباه کنیم، با خطای InvalidCastException برنامه کرش میکنه.
اما #C دو تا ابزار خیلی بهتر و امنتر برای این کار داره که جلوی این کرشها رو میگیره: اپراتورهای as و is.
اپراتور as سعی میکنه عمل downcast رو انجام بده.
✅ اگه موفق بشه، نتیجه رو برمیگردونه.
❌ اگه شکست بخوره، به جای پرتاب Exception، خیلی آروم و بیصدا مقدار null رو برمیگردونه.
این اپراتور برای مواقعی عالیه که شما میخواید نتیجه رو برای null چک کنید.
💡نکته مهم:
اپراتور is فقط یه سوال true/false میپرسه: "آیا این آبجکت از این نوع هست (یا قابل تبدیل به این نوعه)؟" این روش کلاسیک برای جلوگیری از خطای کستینگه.
این روش مدرن و پیشنهادی امروزه. شما میتونید همزمان با چک کردن نوع، اگه شرط درست بود، نتیجه کست رو مستقیماً داخل یه متغیر جدید بریزید! این کار کد رو فوقالعاده تمیز و کوتاه میکنه.
ترفند حرفهای: شما حتی میتونید از این متغیر جدید، در ادامهی همون شرط استفاده کنید!
استفاده از as و به خصوص is با pattern variable، کد شما رو در کار با سلسلهمراتب وراثت، خیلی امنتر و خواناتر میکنه.
🛡 کستینگ امن در #C: راهنمای کامل اپراتورهای as و is
تو پست قبلی دیدیم که Downcasting (تبدیل پدر به فرزند) با (Stock)a میتونه خطرناک باشه و اگه اشتباه کنیم، با خطای InvalidCastException برنامه کرش میکنه.
اما #C دو تا ابزار خیلی بهتر و امنتر برای این کار داره که جلوی این کرشها رو میگیره: اپراتورهای as و is.
1️⃣ اپراتور as: کستینگ بدون استثنا (Exception)
اپراتور as سعی میکنه عمل downcast رو انجام بده.
✅ اگه موفق بشه، نتیجه رو برمیگردونه.
❌ اگه شکست بخوره، به جای پرتاب Exception، خیلی آروم و بیصدا مقدار null رو برمیگردونه.
این اپراتور برای مواقعی عالیه که شما میخواید نتیجه رو برای null چک کنید.
Asset a = new House(); // a یک House است، نه Stock
Stock s = a as Stock; // خطا رخ نمیدهد، s برابر با null میشود
if (s != null)
{
// این کد اجرا نخواهد شد
Console.WriteLine(s.SharesOwned);
}
💡نکته مهم:
as فقط زمانی خوبه که شما قصد دارید نتیجه رو برای null چک کنید. اگه چک نکنید و روی نتیجهش کاری انجام بدید، ممکنه با خطای مبهم NullReferenceException مواجه بشید.2️⃣ اپراتور is: سوال پرسیدن قبل از عمل
اپراتور is فقط یه سوال true/false میپرسه: "آیا این آبجکت از این نوع هست (یا قابل تبدیل به این نوعه)؟" این روش کلاسیک برای جلوگیری از خطای کستینگه.
// روش قدیمی ولی امن
if (a is Stock)
{
Stock s = (Stock)a; // حالا که مطمئنیم، با خیال راحت کست میکنیم
// ...
}
3️⃣ جادوی مدرن: is با Pattern Variable ✨
این روش مدرن و پیشنهادی امروزه. شما میتونید همزمان با چک کردن نوع، اگه شرط درست بود، نتیجه کست رو مستقیماً داخل یه متغیر جدید بریزید! این کار کد رو فوقالعاده تمیز و کوتاه میکنه.
Asset a = new Stock { SharesOwned = 500 };
if (a is Stock s)
{
// دیگه نیازی به کست جداگانه نیست!
// s همینجا از نوع Stock و قابل استفاده است
Console.WriteLine(s.SharesOwned); // 500
}ترفند حرفهای: شما حتی میتونید از این متغیر جدید، در ادامهی همون شرط استفاده کنید!
if (a is Stock s && s.SharesOwned > 100)
{
Console.WriteLine("This is a significant stock holding!");
}
🤔 حرف حساب و تجربه شما
استفاده از as و به خصوص is با pattern variable، کد شما رو در کار با سلسلهمراتب وراثت، خیلی امنتر و خواناتر میکنه.
🔖 هشتگها:
#Casting #is #as
معماری فریادزن (Screaming Architecture) 📢
اگر به ساختار پوشههای سیستم خود نگاهی بیندازید، آیا میتوانید بگویید که سیستم در مورد چیست؟ و یک سوال جالبتر. آیا یک توسعهدهنده جدید در تیم شما میتواند به راحتی بر اساس ساختار پوشهها بفهمد که سیستم چه کاری انجام میدهد؟
معماری شما باید بیانگر مشکلاتی باشد که حل میکند. سازماندهی سیستم شما حول موارد استفاده (use cases)، به ساختاری منجر میشود که با دامنه کسبوکار (business domain) هماهنگ است. این رویکرد معماری فریادزن نامیده میشود.
معماری فریادزن اصطلاحی است که توسط رابرت مارتین (عمو باب) ابداع شده است. او استدلال میکند که ساختار یک سیستم نرمافزاری باید بیانگر این باشد که سیستم در مورد چیست. او تشابهی بین نگاه کردن به نقشه یک ساختمان ترسیم میکند، که در آن شما میتوانید هدف ساختمان را بر اساس نقشه تشخیص دهید. 🗺
در این مقاله، میخواهم چند مثال عملی نشان دهم و در مورد مزایای معماری فریادزن بحث کنم.
یک رویکرد مبتنی بر مورد استفاده (Use Case Driven) 🎯
یک مورد استفاده، یک تعامل یا وظیفه خاص را نشان میدهد که کاربر میخواهد در سیستم شما به آن دست یابد. این، منطق بیزینس مورد نیاز برای انجام آن وظیفه را کپسوله میکند. یک مورد استفاده، توصیف سطح بالایی از هدف کاربر است. برای مثال، "رزرو یک آپارتمان" یا "خرید یک بلیت". این رویکرد بر روی چه چیزی رفتار سیستم تمرکز دارد، نه چگونه.
وقتی به ساختار پوشهها و فایلهای سورس کد سیستم خود نگاه میکنید:
آیا آنها فریاد میزنند: سیستم رزرو آپارتمان یا سیستم بلیتفروشی؟
یا فریاد میزنند ASP.NET Core؟
در اینجا یک مثال از ساختار پوشهای که حول دغدغههای فنی سازماندهی شده، آمده است: 👎
📁 Api/
| 📁 Controllers
| 📁 Entities
| 📁 Repositories
| 📁 Services
| #️⃣ ApartmentService.cs
| #️⃣ BookingService.cs
| 📁 Models
شما متوجه خواهید شد که انسجام (cohesion) با این ساختار پوشه پایین است.
معماری فریادزن چگونه کمک میکند؟
یک رویکرد مبتنی بر مورد استفاده، موارد استفاده سیستم را به عنوان مفهوم سطح بالا قرار میدهد. در داخل یک پوشه مورد استفاده، ممکن است مفاهیم فنی مورد نیاز برای پیادهسازی آن را پیدا کنیم. معماری برش عمودی نیز از دیدگاه مشابهی به این موضوع نزدیک میشود. 👍
📁 Api/
| 📁 Apartments
| 📁 ReserveApartment
| 📁 Bookings
| 📁 CancelBooking
| 📁 Payments
| 📁 Reviews
مزایای معماری فریادزن ✅
• انسجام بهبود یافته چون موارد استفاده مرتبط به هم نزدیک هستند.
• اتصال بالا (High coupling) برای یک مورد استفاده واحد و موارد استفاده مرتبط با آن.
• اتصال سست (Low coupling) بین موارد استفاده نامرتبط.
• ناوبری آسانتر در سراسر سولوشن.
Bounded Contextها و برشهای عمودی 🧩
ما تکنیکهای زیادی برای کشف ماژولهای سطح بالا در سیستم خود داریم. برای مثال، میتوانیم از event storming برای کاوش موارد استفاده سیستم استفاده کنیم.
ایده کلی در اینجا، فکر کردن در مورد انسجام حول عملکردهاست. Bounded contextها، برشهای عمودی، و معماری فریادزن مفاهیم مکمل یکدیگر هستند.
در اینجا یک مثال از معماری فریادزن برای این سیستم آمده است. بیایید بگوییم ماژول Ticketing به صورت داخلی از معماری تمیز استفاده میکند. اما ما هنوز هم میتوانیم سیستم را حول پوشههای ویژگی و موارد استفاده سازماندهی کنیم.
📁 Modules/
| 📁 Ticketing
| 📁 Application
| 📁 Carts
| 📁 AddItemToCart
| 📁 ClearCart
| 📁 Orders
| 📁 SubmitOrder
| 📁 Domain
| 📁 Customers
| 📁 Orders
| 📁 infrastructure
| 📁 Authentication
| 📁 Customers
نکات پایانی 📝
معماری فریادزن فقط یک عبارت جذاب نیست، بلکه رویکردی است که میتواند عمیقاً بر نحوه ساخت نرمافزار شما تأثیر بگذارد. با سازماندهی سیستم خود حول موارد استفاده، شما پایگاه کد خود را با دامنه اصلی کسبوکار هماهنگ میکنید.
به یاد داشته باشید، هدف ایجاد سیستمی است که هدف خود را از طریق ساختارش مخابره کند. یک رویکرد مبتنی بر مورد استفاده را در پیش بگیرید، دامنههای پیچیده را به bounded contextها تجزیه کنید. سیستمی بسازید که واقعاً در مورد مشکلاتی که حل میکند "فریاد" بزند.
🔖 هشتگها:
#SoftwareArchitecture #ScreamingArchitecture #DomainDrivenDesign #VerticalSliceArchitecture
خروجی کد زیر چیست؟🤔
int a = 5, b = 10;
Console.WriteLine( a++ + ++b );
int a = 5, b = 10;
Console.WriteLine( a++ + ++b );
Anonymous Quiz
10%
15
65%
16
26%
17
0%
18
کجا نباید از داداشمون RabbitMQ استفاده کنیم؟🤔
1️⃣ وقتى نياز به real-time response داريم.
چرا؟ چون RabbitMQ صف هست و ارسال/دريافت پيام ممكنه با تاخير انجام شود.
پيشنهادم WebSocket, gRPC يا Redis Pub/Sub است.
2️⃣ وقتى به message replay يا history نياز داريم.
چرا؟ چون RabbitMQ پيامها رو بعد از مصرف حذف میكند. البته ميشه كانفيگ كرد كه نگه داره.
3️⃣ زمانى كه پيامها حجمشون خيلى زياد است.
چرا؟ چون در حجم بالا RabbitMQ دچار افت performance میشود. Kafka رو پيشنهاد میكنم براى اینجا.
4️⃣ وقتى ترتيب دقيق پردازش پيامها خيلى مهم است. چرا؟ چون RabbitMQ تضمين دقيقى براى ترتيب پيامها ندارد اينجا هم پیشنهادم Kafka است
بقيهاش رو شما بگید.👇🏻
🌱 توسعهدهنده جونیور از EF Core مستقیم در کنترلر استفاده میکنه.
🌳 توسعهدهنده میدلول از الگوی Controller-Service-Repository استفاده میکنه.
🌲 توسعهدهنده سینیور از معماری تمیز (Clean Architecture) استفاده میکنه.
🤯 معمار دوباره برمیگرده به استفاده از EF Core در کنترلر!
چرا این اتفاق میفته؟ 🤔
• چون پروژههای واقعی از راه حلهای ساده سود میبرن.
• فقط زمانی لایه اضافه کن که واقعاً بهش نیاز داری.
• یه اپلیکیشن کوچیک میتونه با EF Core در کنترلرها یا Minimal API endpoints زنده بمونه.
• یه پروژه متوسط ممکنه از یه لایه اپلیکیشن (مثل سرویسها) سود ببره.
• یه سیستم بزرگ ممکنه به برشهای عمودی (Vertical Slices) و معماری تمیز یا حتی DDD نیاز داشته باشه.
📌درس اصلی اینه: از اول کار، بیش از حد مهندسی (over-engineer) نکن.
قدم به قدم بساز.
اگه به پیچیدگی بیشتری نیاز پیدا کردی، اضافهش کن.
اگه نه، ساده نگهش دار و برو جلو.
شما همیشه میتونید بعداً کد رو بازآرایی (refactor) یا گسترش بدید.
روی تحویل ارزش تمرکز کن، نه روی ساختن لایهها. 🎯
💡: خودِ EF Core از قبل الگوهای Repository و Unit Of Work رو پیادهسازی کرده، پس در اکثر مواقع شما نیازی به ساختن کلاسهای ریپازیتوری خودتون ندارید.
🌳 توسعهدهنده میدلول از الگوی Controller-Service-Repository استفاده میکنه.
🌲 توسعهدهنده سینیور از معماری تمیز (Clean Architecture) استفاده میکنه.
🤯 معمار دوباره برمیگرده به استفاده از EF Core در کنترلر!
چرا این اتفاق میفته؟ 🤔
• چون پروژههای واقعی از راه حلهای ساده سود میبرن.
• فقط زمانی لایه اضافه کن که واقعاً بهش نیاز داری.
• یه اپلیکیشن کوچیک میتونه با EF Core در کنترلرها یا Minimal API endpoints زنده بمونه.
• یه پروژه متوسط ممکنه از یه لایه اپلیکیشن (مثل سرویسها) سود ببره.
• یه سیستم بزرگ ممکنه به برشهای عمودی (Vertical Slices) و معماری تمیز یا حتی DDD نیاز داشته باشه.
📌درس اصلی اینه: از اول کار، بیش از حد مهندسی (over-engineer) نکن.
قدم به قدم بساز.
اگه به پیچیدگی بیشتری نیاز پیدا کردی، اضافهش کن.
اگه نه، ساده نگهش دار و برو جلو.
شما همیشه میتونید بعداً کد رو بازآرایی (refactor) یا گسترش بدید.
روی تحویل ارزش تمرکز کن، نه روی ساختن لایهها. 🎯
💡: خودِ EF Core از قبل الگوهای Repository و Unit Of Work رو پیادهسازی کرده، پس در اکثر مواقع شما نیازی به ساختن کلاسهای ریپازیتوری خودتون ندارید.
📖 سری آموزشی کتاب C# 12 in a Nutshell
آمادهاید برای یک شیرجه عمیق به قلب تپنده برنامهنویسی شیءگرا؟ امروز میخوایم تمام ابزارهای #C برای کنترل دقیق وراثت و چندریختی رو کالبدشکافی کنیم.
برای اینکه به کلاسهای فرزند اجازه بدیم رفتار کلاس پدر رو تخصصیسازی کنن، از این دو کلمه کلیدی استفاده میکنیم:
با virtual، کلاس پایه یک پیادهسازی پیشفرض ارائه میده ولی به فرزندان اجازه override کردن (بازنویسی) اون رفتار رو میده.
با abstract، کلاس پایه هیچ پیادهسازیای ارائه نمیده و فرزندان رو مجبور به override کردن و پیادهسازی اون عضو میکنه. کلاسی که عضو abstract داره، خودش هم باید abstract باشه و نمیشه ازش نمونه (new) ساخت.
گاهی وقتا یه عضو در کلاس فرزند، هماسم عضوی در کلاس پدر میشه. به این کار میگن مخفی کردن (Hiding). کلمه کلیدی new فقط برای اینه که به کامپایلر بگیم "من میدونم دارم چیکار میکنم، این کارم عمدیه!" و جلوی Warning رو بگیریم.
این مهمترین بخش ماجراست. تفاوت این دو، در رفتار چندریختی (Polymorphic) مشخص میشه:
• override:
رفتار واقعی چندریختی رو پیاده میکنه. مهم نیست متغیر شما از چه نوعی باشه (پدر یا فرزند)، همیشه متدِ نوعِ واقعی آبجکت صدا زده میشه.
• new:
چندریختی رو میشکنه! متدی که صدا زده میشه، بستگی به نوع متغیر در زمان کامپایل داره، نه نوع واقعی آبجکت.
این مثال رو ببینید تا کامل متوجه بشید:
حالا اگه بخواید جلوی override شدن بیشتر رو در زنجیره وراثت بگیرید، از sealed استفاده میکنید.
• sealed روی متد:
میتونید یه متد override شده رو sealed کنید تا کلاسهای فرزند بعدی دیگه نتونن اون رو تغییر بدن.
• sealed روی کلاس:
یا میتونید کل یک کلاس رو sealed کنید تا دیگه هیچ کلاسی نتونه ازش ارثبری کنه.
تسلط بر این کلمات کلیدی، شما رو از یه کاربر ساده وراثت، به یک معمار واقعی کلاسها تبدیل میکنه.
🏛 راهنمای کامل وراثت پیشرفته در #C: از virtual تا sealed
آمادهاید برای یک شیرجه عمیق به قلب تپنده برنامهنویسی شیءگرا؟ امروز میخوایم تمام ابزارهای #C برای کنترل دقیق وراثت و چندریختی رو کالبدشکافی کنیم.
1️⃣ تعریف قرارداد برای فرزندان: virtual و abstract
برای اینکه به کلاسهای فرزند اجازه بدیم رفتار کلاس پدر رو تخصصیسازی کنن، از این دو کلمه کلیدی استفاده میکنیم:
virtual (قرارداد اختیاری): ✍️
با virtual، کلاس پایه یک پیادهسازی پیشفرض ارائه میده ولی به فرزندان اجازه override کردن (بازنویسی) اون رفتار رو میده.
public class Asset
{
public string Name;
public virtual decimal Liability => 0;
}
public class House : Asset
{
public decimal Mortgage;
public override decimal Liability => Mortgage;
}
abstract (قرارداد اجباری): 🏗
با abstract، کلاس پایه هیچ پیادهسازیای ارائه نمیده و فرزندان رو مجبور به override کردن و پیادهسازی اون عضو میکنه. کلاسی که عضو abstract داره، خودش هم باید abstract باشه و نمیشه ازش نمونه (new) ساخت.
public abstract class Asset
{
public abstract decimal NetValue { get; }
}
public class Stock : Asset
{
public long SharesOwned;
public decimal CurrentPrice;
public override decimal NetValue => CurrentPrice * SharesOwned;
}
2️⃣ مخفی کردن عضو با new (Hiding)
گاهی وقتا یه عضو در کلاس فرزند، هماسم عضوی در کلاس پدر میشه. به این کار میگن مخفی کردن (Hiding). کلمه کلیدی new فقط برای اینه که به کامپایلر بگیم "من میدونم دارم چیکار میکنم، این کارم عمدیه!" و جلوی Warning رو بگیریم.
3️⃣ دوئل بزرگ: override در برابر new ⚔️
این مهمترین بخش ماجراست. تفاوت این دو، در رفتار چندریختی (Polymorphic) مشخص میشه:
• override:
رفتار واقعی چندریختی رو پیاده میکنه. مهم نیست متغیر شما از چه نوعی باشه (پدر یا فرزند)، همیشه متدِ نوعِ واقعی آبجکت صدا زده میشه.
• new:
چندریختی رو میشکنه! متدی که صدا زده میشه، بستگی به نوع متغیر در زمان کامپایل داره، نه نوع واقعی آبجکت.
این مثال رو ببینید تا کامل متوجه بشید:
public class BaseClass
{
public virtual void Foo() { Console.WriteLine("BaseClass.Foo"); }
}
public class Overrider : BaseClass
{
public override void Foo() { Console.WriteLine("Overrider.Foo"); }
}
public class Hider : BaseClass
{
public new void Foo() { Console.WriteLine("Hider.Foo"); }
}
// --- نتایج ---
Overrider over = new Overrider();
BaseClass b1 = over;
over.Foo(); // خروجی: Overrider.Foo
b1.Foo(); // خروجی: Overrider.Foo <- (چون override شده، رفتار واقعی حفظ میشه)
Hider h = new Hider();
BaseClass b2 = h;
h.Foo(); // خروجی: Hider.Foo
b2.Foo(); // خروجی: BaseClass.Foo <- (چون new شده، متد کلاس پایه اجرا میشه!)
4️⃣ قفل کردن وراثت با sealed 🔒
حالا اگه بخواید جلوی override شدن بیشتر رو در زنجیره وراثت بگیرید، از sealed استفاده میکنید.
• sealed روی متد:
میتونید یه متد override شده رو sealed کنید تا کلاسهای فرزند بعدی دیگه نتونن اون رو تغییر بدن.
• sealed روی کلاس:
یا میتونید کل یک کلاس رو sealed کنید تا دیگه هیچ کلاسی نتونه ازش ارثبری کنه.
🤔 حرف حساب
تسلط بر این کلمات کلیدی، شما رو از یه کاربر ساده وراثت، به یک معمار واقعی کلاسها تبدیل میکنه.
🔖 هشتگها:
#OOP #Inheritance #Override #Sealed #Polymorphism
پیادهسازی الگوی Outbox 📬
در سیستمهای توزیعشده، ما اغلب با چالش همگامسازی دیتابیس و سیستمهای خارجی مواجه هستیم. تصور کنید سفارشی را در دیتابیس ذخیره کرده و سپس پیامی را به یک message broker منتشر میکنید. اگر هر یک از این عملیاتها شکست بخورد، سیستم شما در وضعیتی ناسازگار (inconsistent) قرار میگیرد.
الگوی Outbox این مشکل را با در نظر گرفتن انتشار پیام به عنوان بخشی از تراکنش دیتابیس شما حل میکند. به جای انتشار مستقیم پیامها، ما آنها را در یک جدول Outbox در دیتابیس خود ذخیره میکنیم و از عملیات اتمیک اطمینان حاصل میکنیم. سپس یک فرآیند جداگانه این پیامها را به طور قابل اعتماد منتشر میکند.
در این مقاله، ما به پیادهسازی این الگو در NET.، از راهاندازی تا مقیاسپذیری، خواهیم پرداخت.
چرا به الگوی Outbox نیاز داریم؟ 🤔
الگوی transactional Outbox یک مشکل رایج را در سیستمهای توزیعشده حل میکند. این مشکل زمانی رخ میدهد که شما باید دو کار را همزمان انجام دهید: ذخیره داده و ارتباط با یک کامپوننت خارجی.
برای مثال، یک میکروسرویس را تصور کنید که باید:
🔹️ یک سفارش جدید را در دیتابیس خود ذخیره کند.
🔹️ در مورد این سفارش جدید به سیستمهای دیگر اطلاع دهد.
🔹️ اگر یکی از این مراحل شکست بخورد، سیستم شما میتواند در وضعیتی ناسازگار قرار گیرد.
// کد با مشکل سازگاری بالقوه
public async Task<OrderDto> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
// ...
await unitOfWork.CommitAsync(cancellationToken);
// تراکنش دیتابیس در این نقطه کامل شده است
// 💥 اگر برنامه اینجا کرش کند یا event bus در دسترس نباشد چه؟
await eventBus.Send(new OrderCreatedIntegrationEvent(order.Id));
return new OrderDto { Id = order.Id, Total = order.Total };
}
الگوی transactional Outbox به حل این مشکل کمک میکند. 📈 ما هم سفارش و هم پیام Outbox را در یک تراکنش دیتابیس واحد ذخیره میکنیم. این یک عملیات "همه یا هیچ" است.
سپس یک پردازشگر Outbox جداگانه، ارسال واقعی پیام را مدیریت میکند. ⚠️ نکته مهمی که باید در اینجا متوجه شوید این است که الگوی Outbox به ما تحویل حداقل-یکباره (at-least-once delivery) را میدهد. این یعنی ما باید مصرفکنندگان پیام خود را idempotent بسازیم.
پیادهسازی الگوی Outbox 💾
ابتدا، جدول Outbox خود را ایجاد میکنیم:
CREATE TABLE outbox_messages (
id UUID PRIMARY KEY,
type VARCHAR(255) NOT NULL,
content JSONB NOT NULL,
occurred_on_utc TIMESTAMP WITH TIME ZONE NOT NULL,
processed_on_utc TIMESTAMP WITH TIME ZONE NULL,
error TEXT NULL
);
و کلاس #C برای نمایش ورودی Outbox:
public sealed class OutboxMessage
{
public Guid Id { get; init; }
public string Type { get; init; }
public string Content { get; init; }
public DateTime OccurredOnUtc { get; init; }
public DateTime? ProcessedOnUtc { get; init; }
public string? Error { get; init; }
}
✨️یک رویکرد زیبا برای پیادهسازی این، استفاده از domain events برای نمایش نوتیفیکیشنها است. قبل از تکمیل تراکنش، میتوانید تمام رویدادها را برداشته و آنها را به عنوان پیامهای Outbox ذخیره کنید.