🏗 استفاده در Entityها و Value Objectها (با یک نکته مهم)
کمکم استفاده از Primary Constructorها را در Entityها و Value Objectها نیز آغاز کردم؛ مخصوصاً در جاهایی که میخواهید وجود برخی پارامترها را هنگام ساخت شیء اجباری کنید.
public class Order(Guid customerId, Money total)
{
public Guid Id { get; } = Guid.NewGuid();
public Guid CustomerId { get; } = customerId;
public Money Total { get; } = total;
public OrderStatus Status { get; private set; } = OrderStatus.Pending;
public DateTime CreatedAt { get; } = DateTime.UtcNow;
public void Confirm()
{
if (Status != OrderStatus.Pending)
{
throw new InvalidOperationException(
$"Cannot confirm order in {Status} status.");
}
Status = OrderStatus.Confirmed;
}
}
در این طراحی، هیچ راهی برای ایجاد یک Order بدون
customerId یا total وجود ندارد.ءPrimary Constructor این محدودیت را مستقیماً در سطح تعریف Type نمایش میدهد.
اما یک تفاوت مهم با مثال سرویسها وجود دارد:
در اینجا پارامترهای Primary Constructor را به Propertyها اختصاص دادهایم:
public Guid CustomerId { get; } = customerId;این نکته اهمیت زیادی دارد و مستقیماً به بزرگترین چالش Primary Constructorها منتهی میشود.
⚠️ مشکلی که نزدیک بود باعث شود هرگز از آن استفاده نکنم
دلیل اصلی مقاومت اولیه من همین موضوع بود.
پارامترهای Primary Constructor در واقع فیلدهای readonly نیستند.
زمانی که مستقیماً از پارامترهای Primary Constructor در بدنه کلاس استفاده میکنید، کامپایلر آنها را بهعنوان یک متغیر قابل تغییر (mutable) Capture میکند.
هیچ فیلد readonly مخفیای در پشت صحنه ایجاد نمیشود.
به همین دلیل میتوانید بهاشتباه مقدار آنها را تغییر دهید:
public class OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
public async Task<Order?> GetOrderAsync(Guid id)
{
logger.LogInformation("Fetching order {OrderId}", id);
return await orderRepository.GetByIdAsync(id);
}
public void SomeOtherMethod()
{
// This compiles. No warning. No error.
orderRepository = null!;
logger = null!;
}
}
این کد بدون هیچ Warning یا Error کامپایل میشود.
در صورتی که اگر از Constructor سنتی و فیلدهای
private readonly استفاده میکردید، کامپایلر بلافاصله جلوی این کار را میگرفت.اما در Primary Constructorها سکوت میکند.
🔒 اگر به Immutability نیاز دارید
در صورتی که تضمین Immutable بودن برای شما اهمیت دارد، میتوانید پارامترها را به فیلدهای readonly اختصاص دهید:
public class OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ILogger<OrderService> _logger = logger;
public async Task<Order?> GetOrderAsync(Guid id)
{
_logger.LogInformation("Fetching order {OrderId}", id);
return await _orderRepository.GetByIdAsync(id);
}
}
اما در این حالت بخش زیادی از مزیت Primary Constructor از بین میرود.
دوباره به تعریف فیلدها و Assignmentها بازمیگردید؛ فقط با سینتکسی متفاوت.
در عمل، تاکنون هرگز با این مشکل در یک کلاس سرویس مبتنی بر DI مواجه نشدهام.
احتمال اینکه در میانه اجرای یک متد، بهاشتباه Logger یا Repository را Reassign کنید بسیار کم است.
اما در Entityها و Value Objectها که Immutability اهمیت بیشتری دارد، این موضوع میتواند دردسرساز شود.
به همین دلیل هنوز در این بخش با احتیاط بیشتری عمل میکنم.
❌ چه زمانی هنوز از Constructorهای سنتی استفاده میکنم؟
با وجود تمام مزایا، هنوز همه چیز را به Primary Constructor تبدیل نکردهام.
برخی سناریوها همچنان برای Constructorهای کلاسیک مناسبتر هستند.
1️⃣ اعتبارسنجی پیچیده
اگر لازم باشد قبل از مقداردهی، پارامترها اعتبارسنجی شوند، به بدنه Constructor نیاز خواهید داشت.
public class EmailAddress
{
private readonly string _value;
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
{
throw new ArgumentException(
"Invalid email address.", nameof(value));
}
_value = value;
}
}
ءPrimary Constructorها محلی برای قرار دادن Validation قبل از اجرای بدنه کلاس در اختیار شما قرار نمیدهند.
ءPrimary Constructor تنها یک Signature را پشتیبانی میکند.
اگر نیاز به چندین Constructor مختلف داشته باشید، باید از Constructorهای ثانویه و زنجیره کردن آنها با
این رویکرد خیلی سریع پیچیده و ناخوانا میشود.
وقتی تعداد Dependencyها از ۵ یا بیشتر عبور میکند، تعریف Primary Constructor شروع به شلوغ شدن میکند.
در چنین شرایطی معمولاً مشکل اصلی Constructor نیست.
مشکل این است که کلاس بیش از حد مسئولیت دارد.
در این سناریو Refactor کردن کلاس راهحل بهتری نسبت به استفاده از ترفندهای ظاهری برای قالببندی کد است.
بعد از استفاده از Primary Constructorها در چندین پروژه، به این نتیجه رسیدهام:
برای تمام کلاسهای سرویس مبتنی بر Dependency Injection از Primary Constructor استفاده میکنم.
حذف Boilerplate در این سناریو کاملاً ارزشمند است.
برای ساخت Entityها و Value Objectها نیز میتوانند مفید باشند؛ مخصوصاً زمانی که میخواهید پارامترهای ضروری را در سطح Type اجبار کنید.
پارامترهای Primary Constructor فیلد readonly نیستند؛ بلکه بهصورت mutable capture ذخیره میشوند و این مهمترین نکتهای است که باید بدانید.
در کلاسهای سرویس بابت این موضوع نگرانی خاصی ندارم، زیرا احتمال بروز خطا بسیار پایین است.
برای Typeهایی که Validation سنگین دارند، Constructorهای متعدد دارند یا وابستگیهای زیادی دریافت میکنند، همچنان از Constructorهای سنتی استفاده میکنم.
در مجموع، این مهاجرت برای من ارزشمند بود.
کلاسهای سرویس کوتاهتر شدهاند، خوانایی بیشتری دارند و با شناخت درست از محدودیتها، میتوان با اطمینان از Primary Constructorها استفاده کرد.
تا مطلب بعدی، موفق باشید 🚀
2️⃣ ءConstructor Overloadهای متعدد
ءPrimary Constructor تنها یک Signature را پشتیبانی میکند.
اگر نیاز به چندین Constructor مختلف داشته باشید، باید از Constructorهای ثانویه و زنجیره کردن آنها با
this(...) استفاده کنید.این رویکرد خیلی سریع پیچیده و ناخوانا میشود.
3️⃣ تعداد زیاد Dependencyها
وقتی تعداد Dependencyها از ۵ یا بیشتر عبور میکند، تعریف Primary Constructor شروع به شلوغ شدن میکند.
در چنین شرایطی معمولاً مشکل اصلی Constructor نیست.
مشکل این است که کلاس بیش از حد مسئولیت دارد.
در این سناریو Refactor کردن کلاس راهحل بهتری نسبت به استفاده از ترفندهای ظاهری برای قالببندی کد است.
✅ جمعبندی
بعد از استفاده از Primary Constructorها در چندین پروژه، به این نتیجه رسیدهام:
برای تمام کلاسهای سرویس مبتنی بر Dependency Injection از Primary Constructor استفاده میکنم.
حذف Boilerplate در این سناریو کاملاً ارزشمند است.
برای ساخت Entityها و Value Objectها نیز میتوانند مفید باشند؛ مخصوصاً زمانی که میخواهید پارامترهای ضروری را در سطح Type اجبار کنید.
پارامترهای Primary Constructor فیلد readonly نیستند؛ بلکه بهصورت mutable capture ذخیره میشوند و این مهمترین نکتهای است که باید بدانید.
در کلاسهای سرویس بابت این موضوع نگرانی خاصی ندارم، زیرا احتمال بروز خطا بسیار پایین است.
برای Typeهایی که Validation سنگین دارند، Constructorهای متعدد دارند یا وابستگیهای زیادی دریافت میکنند، همچنان از Constructorهای سنتی استفاده میکنم.
در مجموع، این مهاجرت برای من ارزشمند بود.
کلاسهای سرویس کوتاهتر شدهاند، خوانایی بیشتری دارند و با شناخت درست از محدودیتها، میتوان با اطمینان از Primary Constructorها استفاده کرد.
تا مطلب بعدی، موفق باشید 🚀
🔖هشتگها:
#primaryconstructor
بسیاری از توسعهدهندگان فکر میکنند پیچیدگی زمانی ایجاد میشود که سیستم بزرگ شود.
اما معمولاً پیچیدگی خیلی زودتر از آن به وجود میآید.
از اولین جایی که میگوییم:
«شاید بعداً به درد بخورد.»
یک Interface اضافی،
یک لایه اضافی،
یک Generic پیچیده،
یا یک Abstraction که هنوز هیچ مسئلهای را حل نمیکند.
همه چیز با نیت خوب شروع میشود.
میخواهیم سیستم آماده آینده باشد.
اما آیندهای که هنوز نیامده، گاهی بیشترین هزینه را به امروز تحمیل میکند.
جالب اینجاست که اکثر پروژهها از کمبود معماری آسیب نمیبینند.
از معماریای آسیب میبینند که زودتر از نیاز واقعی وارد سیستم شده است.
سادگی به معنی نداشتن دانش نیست.
سادگی یعنی توانایی مقاومت در برابر اضافه کردن چیزهایی که هنوز به آنها نیاز نداریم.
در مهندسی نرمافزار، حذف کردن اغلب سختتر از اضافه کردن است.
اما معمولاً پیچیدگی خیلی زودتر از آن به وجود میآید.
از اولین جایی که میگوییم:
«شاید بعداً به درد بخورد.»
یک Interface اضافی،
یک لایه اضافی،
یک Generic پیچیده،
یا یک Abstraction که هنوز هیچ مسئلهای را حل نمیکند.
همه چیز با نیت خوب شروع میشود.
میخواهیم سیستم آماده آینده باشد.
اما آیندهای که هنوز نیامده، گاهی بیشترین هزینه را به امروز تحمیل میکند.
جالب اینجاست که اکثر پروژهها از کمبود معماری آسیب نمیبینند.
از معماریای آسیب میبینند که زودتر از نیاز واقعی وارد سیستم شده است.
سادگی به معنی نداشتن دانش نیست.
سادگی یعنی توانایی مقاومت در برابر اضافه کردن چیزهایی که هنوز به آنها نیاز نداریم.
در مهندسی نرمافزار، حذف کردن اغلب سختتر از اضافه کردن است.
🎯 ءInvariant چیست و چرا بهترین مکان برای اعمال آن Domain Model است؟
بخش زیادی از کدهای بهظاهر DDD که در پروژههای NET. میبینم، قوانین کسبوکار را بین Handlerها، Validatorها و Controllerها پخش کردهاند، اما خود Domain Model تقریباً هیچ نقشی در محافظت از این قوانین ندارد.نتیجه چیست؟
همان قانون در چندین نقطه مختلف تکرار میشود، هر نسخه به مرور کمی از نسخههای دیگر فاصله میگیرد و در نهایت معتبر بودن یک شیء به این بستگی پیدا میکند که از چه مسیری وارد سیستم شده است.
البته میتوان با این رویکرد سیستمهای کاملاً عملیاتی ساخت. همه ما پروژههایی را دیدهایم که با تعداد زیادی
if و Validation کار میکنند و سالها بدون مشکل در حال اجرا هستند.اما راه بهتری هم وجود دارد؛ راهی که از یک مفهوم بسیار مهم شروع میشود: Invariant
💡 ءInvariant چیست؟
ءInvariant قانونی است که باید در تمام طول عمر یک شیء برقرار بماند.
نه فقط هنگام ذخیره شدن در پایگاه داده.
نه فقط زمانی که یک Validator اجرا میشود.
بلکه هر زمان که به آن شیء دسترسی پیدا میکنید، باید آن قانون برقرار باشد؛ فارغ از اینکه شیء چگونه ساخته شده یا از کجا بارگذاری شده است.
چند مثال:
✅ یک Course همیشه باید عنوان داشته باشد.
✅ مجموع Order همیشه باید برابر مجموع آیتمهای آن باشد.
✅ یک Subscription در هر لحظه فقط در یکی از وضعیتهای Trial، Active، PastDue یا Canceled قرار دارد.
✅ یک Course منتشرشده باید حداقل یک Lesson داشته باشد.
نکته مهم اینجاست که هیچکدام از این قوانین درباره HTTP، Validation یا Database صحبت نمیکنند.
اینها قوانین دامنه هستند و باید مستقل از زیرساخت همیشه برقرار باشند.
❌ مشکل رویکردهای Procedural
فرض کنید یک Course به شکل زیر تعریف شده باشد:
public class Course
{
public string Title { get; set; }
public CourseStatus Status { get; set; }
public DateTime? PublishedOn { get; set; }
public decimal Price { get; set; }
}
در این طراحی: Constructor وجود ندارد.
تمام Setterها عمومی هستند.
هر کسی میتواند هر مقداری را در هر زمانی تغییر دهد.
در نتیجه قوانین کسبوکار در بخشهای مختلف سیستم پخش میشوند:
ءCreateCourseValidator بررسی میکند Title خالی نباشد.
ءPublishCourseHandler بررسی میکند Course قبلاً منتشر نشده باشد.
ءChangePriceHandler بررسی میکند Course آرشیو نشده باشد.
ءEndpoint جدیدی اضافه میشود و توسعهدهنده یکی از Handlerهای قبلی را Copy میکند اما یکی از Validationها را فراموش میکند.
اینجاست که مدل بهتدریج وارد وضعیتهای نامعتبر میشود.
مشکل اصلی Anemic Model همین است.
نه اینکه رفتار ندارد.
بلکه هیچ تضمینی درباره وضعیت خودش ارائه نمیکند.
در نتیجه تمام مصرفکنندگان آن مجبورند دائماً قوانین را تکرار کنند.
✅ Always Valid Model
ایده اصلی بسیار ساده است:
ءDomain Model نباید هیچگاه وضعیت نامعتبر را بپذیرد.
اگر Reference یک Course را در اختیار دارید، باید بتوانید به آن اعتماد کنید.
نباید در لایههای بالاتر دائماً بنویسید:
if(course.Title is null)
نباید چندین Validator موازی داشته باشید.
نباید امیدوار باشید که Handler مربوطه Validationها را فراموش نکرده باشد.
برای رسیدن به این هدف معمولاً سه اصل مهم وجود دارد.
1️⃣ جلوگیری از ساخت Object نامعتبر
اگر Course بدون Title معنایی ندارد، پس نباید امکان ساخت آن وجود داشته باشد.
public class Course
{
private Course(CourseId id, string title, Money price)
{
Id = id;
Title = title;
Price = price;
Status = CourseStatus.Draft;
}
public static Result<Course> Create(string title, Money price)
{
if (string.IsNullOrWhiteSpace(title))
{
return CourseErrors.TitleRequired;
}
return new Course(CourseId.New(), title, price);
}
}
در اینجا: Constructor خصوصی است.
تمام ساخت Object از طریق Factory انجام میشود.Validation تنها در یک نقطه انجام میشود.
از این لحظه به بعد هر Course موجود در سیستم دارای Title معتبر است.
2️⃣ محافظت از تغییر وضعیتها
حتی اگر ساخت Object کنترل شود، همچنان تغییر وضعیت میتواند قوانین را نقض کند.
به همین دلیل Setterهای عمومی نباید وجود داشته باشند.
تمام تغییرات باید از طریق متدهایی انجام شوند که قوانین را میشناسند.
public Result Publish(IDateTimeProvider clock)
{
if (Status != CourseStatus.Draft)
{
return CourseErrors.AlreadyPublished;
}
if (_lessons.Count == 0)
{
return CourseErrors.CannotPublishWithoutLessons;
}
Status = CourseStatus.Published;
PublishedOn = clock.UtcNow;
return Result.Success();
}
در این طراحی: Handler دیگر مسئول دانستن قوانین نیست.
فقط متد Publish را فراخوانی میکند.
خود Entity تصمیم میگیرد آیا عملیات مجاز است یا خیر.
3️⃣ محافظت از Aggregate
برخی قوانین تنها به یک Entity محدود نیستند و چندین Entity درون یک Aggregate را درگیر میکنند.
در چنین شرایطی Aggregate Root مسئول حفظ Invariantها است.
مثال: Course منتشرشده باید حداقل یک Lesson داشته باشد.
پس از انتشار Course، حذف Lessonها مجاز نیست.
پیادهسازی صحیح:
public sealed class Course
{
private readonly List<Lesson> _lessons = [];
public IReadOnlyCollection<Lesson> Lessons =>
_lessons.AsReadOnly();
public Result RemoveLesson(LessonId id)
{
if (Status == CourseStatus.Published)
{
return CourseErrors
.CannotModifyPublishedLessons;
}
var lesson =
_lessons.FirstOrDefault(l => l.Id == id);
if (lesson is null)
{
return CourseErrors.LessonNotFound;
}
_lessons.Remove(lesson);
return Result.Success();
}
}
در این مدل:
هیچ مصرفکنندهای نمیتواند مستقیماً Collection را تغییر دهد.
تمام تغییرات باید از طریق Aggregate Root انجام شوند.
در نتیجه Invariantها همیشه حفظ خواهند شد.
🎯 اگر قانون بین چند Aggregate باشد چه؟
اینجا دیگر مسئولیت یک Aggregate تمام میشود.
اگر قانونی بین دو Aggregate مستقل وجود داشته باشد، معمولاً باید از Domain Event استفاده شود.
نه اینکه یک Aggregate مستقیماً وارد Aggregate دیگر شود.
این دقیقاً یکی از دلایل اصلی وجود Domain Eventها در DDD است.
🚀 نتیجه واقعی این رویکرد چیست؟
شاید همان سیستم را بتوان بهصورت Procedural نیز پیادهسازی کرد.
اما چیزی که به دست میآورید «اعتماد» است.
در یک سیستم Procedural:
تمام مصرفکنندگان مسئول حفظ قوانین هستند.
اما در یک Always Valid Model:
مسئولیت حفظ قوانین فقط بر عهده Domain Model است.
این تفاوت در طول زمان تأثیر بزرگی ایجاد میکند:
✅ ءValidationها از هم فاصله نمیگیرند.
✅ قوانین تکرار نمیشوند.
✅ ءEndpointهای جدید نمیتوانند بهاشتباه قوانین را دور بزنند.
✅ تستها سادهتر میشوند.
✅ ءDomain Model به مرکز واقعی قوانین کسبوکار تبدیل میشود.
📌 جمعبندی
ءInvariant قانونی است که باید در تمام طول عمر یک Object برقرار باشد.
بهترین محل برای اعمال این قوانین خود Domain Model است.
ءInvariantهای مربوط به ساخت Object در Factory و Constructor خصوصی قرار میگیرند.
ءInvariantهای مربوط به تغییر وضعیت در متدهای Domain پیادهسازی میشوند.
ءInvariantهای سطح Aggregate توسط Aggregate Root محافظت میشوند.
در مقابل، ممکن است مقداری از سادگی ظاهری کدهای Procedural را از دست بدهید.
اما در عوض مدلی خواهید داشت که همیشه معتبر است و میتوانید به آن اعتماد کنید.
و این دقیقاً یکی از مهمترین اهداف Domain-Driven Design است.
در سالهای اول فعالیت، فکر میکردم ارزش یک توسعهدهنده به تعداد پاسخهایی است که دارد.
امروز بیشتر فکر میکنم ارزش او به کیفیت سؤالهایی است که میپرسد.
افراد کمتجربه معمولاً سریع به دنبال جواب میگردند.افراد باتجربهتر ابتدا تلاش میکنند مسئله را بهتر بفهمند.
چون بسیاری از مشکلاتی که هفتهها برای حل آنها زمان صرف میشود، از ابتدا اشتباه تعریف شدهاند.
و هیچ راهحلی برای یک مسئله اشتباه، راهحل خوبی نخواهد بود.
گاهی یک سؤال درست، ارزشمندتر از ساعتها پیادهسازی است.
مهندسی نرمافزار فقط هنر پاسخ دادن نیست.هنر پرسیدن سؤالهای بهتر هم هست.
امروز بیشتر فکر میکنم ارزش او به کیفیت سؤالهایی است که میپرسد.
افراد کمتجربه معمولاً سریع به دنبال جواب میگردند.افراد باتجربهتر ابتدا تلاش میکنند مسئله را بهتر بفهمند.
چون بسیاری از مشکلاتی که هفتهها برای حل آنها زمان صرف میشود، از ابتدا اشتباه تعریف شدهاند.
و هیچ راهحلی برای یک مسئله اشتباه، راهحل خوبی نخواهد بود.
گاهی یک سؤال درست، ارزشمندتر از ساعتها پیادهسازی است.
مهندسی نرمافزار فقط هنر پاسخ دادن نیست.هنر پرسیدن سؤالهای بهتر هم هست.
🚀 ءUnion Types بالاخره به #C میآیند
هر توسعهدهنده Backend دیر یا زود با یک چالش تکراری مواجه میشود: متدی که بتواند یکی از چندین مقدار مختلف را برگرداند.
یک عملیات Parse که یا یک عدد معتبر تولید میکند یا یک خطا. یک Lookup که یا یک مقدار را برمیگرداند یا نتیجه «یافت نشد» را. یک عملیات که یا موفق میشود یا شکست میخورد.
در #C تاکنون هیچ راهکار تمیز و استانداردی برای مدلسازی مفهوم «این یا آن» (A یا B) نداشتیم. بنابراین مجبور بودیم آن را شبیهسازی کنیم؛ با استفاده از Marker Interfaceها، Abstract Base Classها، Tupleها، Nullable Returnها، Exceptionها یا کتابخانه بسیار خوب OneOf.
اکنون C# 15 (که همراه با NET 11. منتشر خواهد شد) بالاخره Union Typeها را به زبان اضافه میکند.
سالها منتظر چنین قابلیتی بودیم، بنابراین اجازه دهید یک معرفی سریع از آن داشته باشیم.
بیایید شروع کنیم.
🎯 مشکل
فرض کنید متدی داریم که میتواند یک User را برگرداند یا به دلیل وجود نداشتن کاربر با شکست مواجه شود.
امروزه معمولاً چنین چیزی مینویسیم:
// Throw for the "failure" case - control flow via exceptions
public User GetUser(int id) =>
_users.TryGetValue(id, out var user)
? user
: throw new UserNotFoundException(id);
امضای متد (Signature) میگوید که یک User برمیگرداند، اما این حقیقت کامل نیست؛ زیرا ممکن است Exception نیز پرتاب کند.
فراخواننده متد هیچ راهی برای دانستن این موضوع ندارد، مگر اینکه بدنه متد را مطالعه کند.
سایر راهکارهای رایج نیز (مانند استفاده از
bool TryGet به همراه out parameter، یک کلاس Result سفارشی با فیلدهای Nullable یا OneOf<User>, NotFound) همگی پیچیدگی بیشتری را برای بیان یک مفهوم ساده تحمیل میکنند.آنچه واقعاً نیاز دارید، یک مجموعه بسته (Closed Set) از Typeها است.
و دقیقاً همین چیزی است که Union ارائه میکند.
🏗 تعریف یک Union
ءSyntax آن به طرز دلپذیری ساده است.
کافی است نام Union و Typeهای ممکن را مشخص کنید:
public union Result<T>(T, Exception);
همین.
از این لحظه <Result<T فقط میتواند یکی از دو حالت زیر باشد:
TExceptionو هیچ چیز دیگر.
حتی لازم نیست این Typeها با یکدیگر ارتباطی داشته باشند و دقیقاً همین موضوع هدف اصلی Union است.
مثال ملموستر:
public record CreditCard(string Last4, string Brand);
public record PayPal(string Email);
public record BankTransfer(string Iban);
public union PaymentMethod(CreditCard, PayPal, BankTransfer);
⚡️ ساختن مقدار
برای هر Case Type یک تبدیل ضمنی (Implicit Conversion) وجود دارد، بنابراین کافی است مقدار را مستقیماً Assign کنید:
PaymentMethod method = new CreditCard("4242", "Visa");اگر سعی کنید Typeای را Assign کنید که جزو مجموعه تعریفشده نیست، با خطای کامپایل مواجه خواهید شد.
زیرا این مجموعه بسته است.
🔍 استفاده از Union
اینجاست که قدرت واقعی آن مشخص میشود. Pattern Matching بهصورت مستقیم کار میکند و کامپایلر نیز نوع داخلی مقدار را برای شما بررسی میکند:
string Describe(PaymentMethod method) => method switch
{
CreditCard card => $"{card.Brand} ending {card.Last4}",
PayPal paypal => $"PayPal ({paypal.Email})",
BankTransfer ach => $"Bank transfer to {ach.Iban}",
};
توجه کنید که هیچ:
_
یا
default
وجود ندارد.
چرا؟
زیرا Union یک مجموعه بسته است و کامپایلر میداند که هر سه حالت پوشش داده شدهاند.
اگر یکی از آنها را فراموش کنید، هنگام کامپایل هشدار دریافت خواهید کرد:
warning CS8509: The switch expression does not handle all possible values
of its input type (it is not exhaustive). For example, the pattern 'BankTransfer'
is not covered.
قابلیت Exhaustiveness Checking همان ویژگیای است که بیش از همه درباره آن هیجانزدهام.
اگر بعداً یک Case جدید به Union اضافه کنید، کامپایلر تمام Switchهایی را که نیاز به بروزرسانی دارند به شما نشان خواهد داد.
🔄 بازگشت به مشکل اولیه
متد
GetUser که قبلاً درباره آن صحبت کردیم را به خاطر دارید؟بیایید آن را با استفاده از Union بازنویسی کنیم.
ابتدا مشخص میکنیم که این متد واقعاً چه چیزهایی میتواند برگرداند:
public record NotFound(int Id);
public union UserResult(User, NotFound);
اکنون Signature حقیقت را بیان میکند و دیگر از Exception برای کنترل جریان برنامه استفاده نمیشود:
و فراخواننده مجبور است هر دو حالت را مدیریت کند؛ کامپایلر اجازه نخواهد داد که یکی از آنها را فراموش کنید:
تمام ایده همین است.
نوع بازگشتی متد حقیقت را بیان میکند:
اینها دقیقاً حالتهایی هستند که دریافت خواهید کرد و نمیتوانید بهصورت تصادفی یکی از آنها را نادیده بگیرید.
دیگر نیازی نیست برای فهمیدن اینکه متد چه Exceptionهایی ممکن است پرتاب کند، بدنه آن را بررسی کنید.
این قابلیت هنوز در مرحله Preview و Experimental قرار دارد.
چند نکته که باید در نظر داشته باشید:
🔹 این قابلیت C# 15 و NET 11. را هدف قرار داده است و ممکن است Syntax آن تا زمان انتشار نهایی تغییر کند. برای آزمایش میتوانید از NET 11 Preview 4. یا نسخههای جدیدتر استفاده کنید.
🔹 در پشت صحنه، Union به یک Struct تبدیل میشود که Caseهای Value Type را Boxing کرده و محتوا را به شکل یک ?
🔹 این قابلیت فعلاً یک Type Union (A یا B) است، نه یک Discriminated Union کامل با Caseهای نامگذاریشده. با این حال، بخش عمده سناریوهایی را که امروز برای آنها از OneOf استفاده میکنم پوشش میدهد.
ءUnion Typeها یکی از خلأهای قدیمی و مهم #C را برطرف میکنند.
شما میتوانید:
تعریف کنید.
مقادیر Caseها را مستقیماً Assign کنید و از Conversionهای ضمنی استفاده کنید.
ءPattern Matching را همراه با بررسی کامل Exhaustiveness توسط کامپایلر انجام دهید، بدون اینکه به
ءResultها، Optionها و خروجیهای «یکی از این چند حالت» را بدون نیاز به Marker Interfaceها، Base Classها یا کتابخانههای جانبی مدلسازی کنید.
این قابلیت از نظر Syntax کوچک است، اما تأثیر بسیار بزرگی دارد:
امضای متدها بالاخره حقیقت را درباره خروجی خود بیان میکنند و کامپایلر نیز اطمینان حاصل میکند که تمام حالتها بهدرستی مدیریت شدهاند.
🙏 از اینکه مطالعه کردید متشکرم.
public UserResult GetUser(int id) =>
_users.TryGetValue(id, out var user)
? user
: new NotFound(id);
و فراخواننده مجبور است هر دو حالت را مدیریت کند؛ کامپایلر اجازه نخواهد داد که یکی از آنها را فراموش کنید:
IResult response = GetUser(42) switch
{
User user => Results.Ok(user),
NotFound found => Results.NotFound($"No user with id {found.Id}"),
};
تمام ایده همین است.
نوع بازگشتی متد حقیقت را بیان میکند:
اینها دقیقاً حالتهایی هستند که دریافت خواهید کرد و نمیتوانید بهصورت تصادفی یکی از آنها را نادیده بگیرید.
دیگر نیازی نیست برای فهمیدن اینکه متد چه Exceptionهایی ممکن است پرتاب کند، بدنه آن را بررسی کنید.
⚠️ چند نکته مهم
این قابلیت هنوز در مرحله Preview و Experimental قرار دارد.
چند نکته که باید در نظر داشته باشید:
🔹 این قابلیت C# 15 و NET 11. را هدف قرار داده است و ممکن است Syntax آن تا زمان انتشار نهایی تغییر کند. برای آزمایش میتوانید از NET 11 Preview 4. یا نسخههای جدیدتر استفاده کنید.
🔹 در پشت صحنه، Union به یک Struct تبدیل میشود که Caseهای Value Type را Boxing کرده و محتوا را به شکل یک ?
object نگهداری میکند. برای سناریوهای حساس به Performance مسیر Non-Boxing نیز در نظر گرفته شده است، اما رفتار پیشفرض ساده نگه داشته شده است.🔹 این قابلیت فعلاً یک Type Union (A یا B) است، نه یک Discriminated Union کامل با Caseهای نامگذاریشده. با این حال، بخش عمده سناریوهایی را که امروز برای آنها از OneOf استفاده میکنم پوشش میدهد.
📌 جمعبندی
ءUnion Typeها یکی از خلأهای قدیمی و مهم #C را برطرف میکنند.
شما میتوانید:
public union Name(A, B, C);
تعریف کنید.
مقادیر Caseها را مستقیماً Assign کنید و از Conversionهای ضمنی استفاده کنید.
ءPattern Matching را همراه با بررسی کامل Exhaustiveness توسط کامپایلر انجام دهید، بدون اینکه به
default یا _ نیاز داشته باشید.ءResultها، Optionها و خروجیهای «یکی از این چند حالت» را بدون نیاز به Marker Interfaceها، Base Classها یا کتابخانههای جانبی مدلسازی کنید.
این قابلیت از نظر Syntax کوچک است، اما تأثیر بسیار بزرگی دارد:
امضای متدها بالاخره حقیقت را درباره خروجی خود بیان میکنند و کامپایلر نیز اطمینان حاصل میکند که تمام حالتها بهدرستی مدیریت شدهاند.
🙏 از اینکه مطالعه کردید متشکرم.
🔖هشتگها:
#csharp #dotnet11 #csharp15 #uniontypes
هرچه بیشتر در پروژههای مختلف کار میکنم، بیشتر به این نتیجه میرسم که اکثر مشکلات نرمافزار، مشکل تکنولوژی نیستند.تکنولوژی فقط جایی است که مشکل خودش را نشان میدهد.
سیستم کند شده است.
در نگاه اول مشکل از Database است.
اما کمی عمیقتر که نگاه میکنی، میبینی هیچوقت برای حجم فعلی کاربران طراحی نشده بود.
ءFeatureها دیر تحویل میشوند.
در ظاهر مشکل از توسعهدهندگان است.
اما شاید مسئله از تغییر مداوم اولویتها باشد.
تعداد Bugها زیاد شده است.
به نظر میرسد کیفیت کد پایین آمده.
اما شاید تیم ماههاست که فرصت رسیدگی به بدهیهای فنی را نداشته است.
به همین دلیل است که راهحل بسیاری از مشکلات، صرفاً اضافه کردن یک ابزار جدید نیست.گاهی قبل از پیدا کردن پاسخ، باید مطمئن شویم سؤال را درست فهمیدهایم.
چون اگر ریشه مسئله را اشتباه تشخیص دهیم،بهترین راهحل دنیا هم ما را به نتیجه درستی نمیرساند.
در مهندسی نرمافزار، پیدا کردن مسئله واقعی اغلب سختتر از حل کردن آن است.
سیستم کند شده است.
در نگاه اول مشکل از Database است.
اما کمی عمیقتر که نگاه میکنی، میبینی هیچوقت برای حجم فعلی کاربران طراحی نشده بود.
ءFeatureها دیر تحویل میشوند.
در ظاهر مشکل از توسعهدهندگان است.
اما شاید مسئله از تغییر مداوم اولویتها باشد.
تعداد Bugها زیاد شده است.
به نظر میرسد کیفیت کد پایین آمده.
اما شاید تیم ماههاست که فرصت رسیدگی به بدهیهای فنی را نداشته است.
به همین دلیل است که راهحل بسیاری از مشکلات، صرفاً اضافه کردن یک ابزار جدید نیست.گاهی قبل از پیدا کردن پاسخ، باید مطمئن شویم سؤال را درست فهمیدهایم.
چون اگر ریشه مسئله را اشتباه تشخیص دهیم،بهترین راهحل دنیا هم ما را به نتیجه درستی نمیرساند.
در مهندسی نرمافزار، پیدا کردن مسئله واقعی اغلب سختتر از حل کردن آن است.
🚀 قانون DRY؛ یکی از بدفهمیدهشدهترین قوانین در برنامهنویسی
هر برنامهنویسی خیلی زود با DRY آشنا میشود، و تقریباً همه آن را اشتباه یاد میگیرند.Don't Repeat Yourself.
دو قطعه کد مشابه دیدی؟ یک متد استخراج کن و کد تکراری را حذف کن.
من هم سالها همین کار را انجام میدادم و نتیجهاش بعضی از بدترین کدهایی بود که تاکنون مجبور به نگهداریشان شدهام:
🔹 یک Helper مشترک که هر اسپرینت یک پارامتر
bool جدید به آن اضافه میشد.🔹 یک Base Class که هیچکس جرئت تغییرش را نداشت، چون شش قابلیت کاملاً نامرتبط از آن ارثبری کرده بودند.
🔹 یک ماژول «مشترک» که دو بخش مستقل سیستم به آن وابسته بودند و در نتیجه هیچکدام نمیتوانستند بدون تأثیر روی دیگری تغییر کنند.
همه اینها با یک تلاش کاملاً بیضرر برای تکرار نکردن کد شروع شدند.
🎯 ءDRY واقعاً چه میگوید؟
این همان بخشی است که بیشتر افراد از آن عبور میکنند.
تعریف اصلی DRY که توسط Andy Hunt و Dave Thomas در کتاب The Pragmatic Programmer
ارائه شد، اصلاً درباره کد نیست:
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
«هر بخش از دانش باید در یک سیستم، تنها یک نمایش یکتا، شفاف و مرجع داشته باشد.»
موضوع اصلی دانش (Knowledge) است.
یک حقیقت درباره دامنه کسبوکار شما، مانند یک قانون مالیاتی یا فرمت شماره فاکتور، باید فقط در یک مکان وجود داشته باشد.
وقتی آن حقیقت تغییر کرد، باید فقط یک بار آن را تغییر دهید؛ نه اینکه به دنبال هفت نسخه مختلف آن در سیستم بگردید.
❌ اشتباه رایج: حذف تکرار کد، نه حذف تکرار دانش
دو قطعه کد میتوانند کاملاً مشابه باشند اما دانش کاملاً متفاوتی را نمایش دهند.
فرض کنید دو نوع آدرس را اعتبارسنجی میکنید:
🔹 آدرس ارسال مشتری
🔹 آدرس انبار
امروز قوانین هر دو یکسان هستند:
public bool IsValid(Address address) =>
!string.IsNullOrWhiteSpace(address.Street) &&
!string.IsNullOrWhiteSpace(address.City) &&
!string.IsNullOrWhiteSpace(address.PostalCode);
واکنش طبیعی DRY این است که یک Validator مشترک استخراج کنیم و از هر دو جا آن را فراخوانی کنیم.
اما اینها دو مفهوم متفاوت هستند که فقط همین هفته قوانین یکسانی دارند.
روزی که انبار به یک Loading Dock Code نیاز پیدا کند، دوباره به همان متد مشترک برمیگردید و یک Flag جدید اضافه میکنید تا مصرفکننده قبلی همچنان کار کند:
public bool IsValid(Address address, bool requireDockCode = false) =>
!string.IsNullOrWhiteSpace(address.Street) &&
!string.IsNullOrWhiteSpace(address.City) &&
!string.IsNullOrWhiteSpace(address.PostalCode) &&
(!requireDockCode || !string.IsNullOrWhiteSpace(address.DockCode));
همین پارامتر
bool نشانه خطر است.اولین باری که یک متد مشترک مجبور میشود Flag بگیرد تا برای یک مصرفکننده رفتار متفاوتی داشته باشد، یعنی شما با تکرار مواجه نبودید.
شما فقط دو چیز مشابه را به زور به هم چسباندهاید.
یک سال بعد، امضای متد سه Flag دیگر هم خواهد داشت؛ هر کدام نشانهای از اینکه این دو مفهوم هیچوقت واقعاً یکسان نبودهاند.
💸 هزینه Abstraction اشتباه از هزینه Duplication بیشتر است
تکرار کد معمولاً بسیار ارزانتر از یک Abstraction اشتباه است.
کپیپیست:
✅ قابل مشاهده است
✅ محلی است
✅ هر نسخه میتواند مستقل تکامل پیدا کند
اما Abstraction اشتباه:
❌ پنهان است
❌ سراسری است
❌ همه مصرفکنندهها را به یک شکل خاص وابسته میکند
ءFlagها اضافه میشوند، پیچیدگی افزایش پیدا میکند و در نهایت از تغییر متدی که دیگر درکش نمیکنید میترسید.
من زمان بسیار بیشتری را صرف حذف Abstractionهای بد کردهام تا زمانی که از ایجاد آنها صرفهجویی کرده باشم.
این همان هزینه پنهان Coupling است که قبلاً درباره آن صحبت کردهام و DRY افراطی یکی از رایجترین راههای ورود آن به سیستم است.
🏗 جایی که بیشترین آسیب را میزند: مرزهای سیستم
داخل یک کلاس، یک Helper بد فقط آزاردهنده است.
اما در مرز بین ماژولها، تبدیل به یک آسیب ساختاری میشود.
فرض کنید یک Modular Monolith داریم که شامل دو ماژول است:
🔹 Billing
🔹 Shipping
هر دو یک مفهوم Order دارند.
یک توسعهدهنده با نیت خیر متوجه میشود این دو کلاس فیلدهای مشابهی دارند و آنها را به یک Type مشترک منتقل میکند:
// Shared.Orders, referenced by both Billing and Shipping
public class Order
{
public Guid Id { get; set; }
public string CustomerName { get; set; }
public decimal Total { get; set; }
// ...whatever either module happens to need
}
حالا Billing و Shipping دیگر نمیتوانند مستقل تکامل پیدا کنند.
یک تغییر در Billing:
➡️ نیاز به Recompile
➡️ نیاز به Retest
➡️ نیاز به Redeploy
برای Shipping خواهد داشت.
شما دو Bounded Context را که قرار بود مستقل باشند، فقط برای صرفهجویی در چند Property به هم جوش دادهاید.
داشتن دو مدل Order مستقل دقیقاً همان دلیلی است که دادهها باید داخل مرزهای خودشان باقی بمانند.
شباهت داشتن آنها کاملاً طبیعی است.
آنها یک مفهوم واقعی را از دو زاویه متفاوت مدل میکنند؛ زاویههایی که به مرور زمان از هم فاصله خواهند گرفت.
من بار دوم چیزی را Deduplicate نمیکنم.
تا بار سوم صبر میکنم و فقط یک سؤال میپرسم:
اگر پاسخ بله باشد:
✅ با تکرار واقعی روبهرو هستید.
✅ همان حقیقت در چند نقطه نوشته شده است.
✅ آن را استخراج کنید.
اگر پاسخ خیر باشد:
✅ شباهت فقط تصادفی است.
✅ آن را به حال خودش رها کنید.
✅ ءCoupling آینده هزینه بیشتری خواهد داشت.
اجازه دهید کد تکرار شود تا زمانی که Abstraction مناسب خودش را نشان دهد.
ءAbstractionهای خوب از مثالهای واقعی کشف میشوند، نه اینکه از ابتدا حدس زده شوند.
بعضی افراد این رویکرد را AHA (Avoid Hasty Abstractions) مینامند.
وقتی بتوانید مفهوم را نامگذاری کنید، زمان استخراج فرا رسیده است.
نامهایی مثل:
✅
✅
✅
احتمالاً دانش دامنه هستند و ارزش تبدیل شدن به Value Object را دارند.
اما اگر بهترین نامی که پیدا میکنید اینها باشد:
❌
❌
❌
احتمالاً در حال Abstract کردن شکل کد هستید، نه دانش.
وقتی درست استفاده شود، DRY فوقالعاده ارزشمند است.
یک قانون کسبوکار باید فقط در یک مکان وجود داشته باشد.
فرض کنید قانون زیر را در سه بخش مختلف کپی کردهایم:
بالاخره روزی دو مورد را تغییر میدهید و سومی را فراموش میکنید.
همین نسخه سومِ منحرفشده منشأ باگ خواهد بود.
قانون را به Domain Model منتقل کنید تا تنها یک خانه داشته باشد:
این دقیقاً همان «نمایش یکتای مرجع» است که DRY درباره آن صحبت میکند.
🔹 ءDRY درباره دانش است، نه کدهایی که شبیه هم هستند.
🔹 هزینه یک Abstraction اشتباه از هزینه Duplication بیشتر است و حذف آن نیز سختتر خواهد بود.
🔹 تا بار سوم صبر کنید. فقط زمانی استخراج کنید که هر دو نسخه یک حقیقت مشترک را نمایش میدهند و باید با هم تغییر کنند.
🔹 دفعه بعد که خواستید یک کد تکراری را حذف کنید، از خودتان نپرسید:
«آیا این دو قطعه کد شبیه هم هستند؟»
بپرسید:
«آیا معنای یکسانی دارند؟»
همین یک سؤال، دردسرهای نگهداری بیشتری را از شما دور خواهد کرد تا هر مقدار تایپ نکردنی که DRY برایتان ذخیره میکند. 🚀
یک تغییر در Billing:
➡️ نیاز به Recompile
➡️ نیاز به Retest
➡️ نیاز به Redeploy
برای Shipping خواهد داشت.
شما دو Bounded Context را که قرار بود مستقل باشند، فقط برای صرفهجویی در چند Property به هم جوش دادهاید.
داشتن دو مدل Order مستقل دقیقاً همان دلیلی است که دادهها باید داخل مرزهای خودشان باقی بمانند.
شباهت داشتن آنها کاملاً طبیعی است.
آنها یک مفهوم واقعی را از دو زاویه متفاوت مدل میکنند؛ زاویههایی که به مرور زمان از هم فاصله خواهند گرفت.
📏 قانونی که من استفاده میکنم: تا بار سوم صبر کن
من بار دوم چیزی را Deduplicate نمیکنم.
تا بار سوم صبر میکنم و فقط یک سؤال میپرسم:
اگر این قانون تغییر کند، آیا هر دو نسخه باید با هم تغییر کنند؟
اگر پاسخ بله باشد:
✅ با تکرار واقعی روبهرو هستید.
✅ همان حقیقت در چند نقطه نوشته شده است.
✅ آن را استخراج کنید.
اگر پاسخ خیر باشد:
✅ شباهت فقط تصادفی است.
✅ آن را به حال خودش رها کنید.
✅ ءCoupling آینده هزینه بیشتری خواهد داشت.
اجازه دهید کد تکرار شود تا زمانی که Abstraction مناسب خودش را نشان دهد.
ءAbstractionهای خوب از مثالهای واقعی کشف میشوند، نه اینکه از ابتدا حدس زده شوند.
بعضی افراد این رویکرد را AHA (Avoid Hasty Abstractions) مینامند.
💡 یک نشانه عملی
وقتی بتوانید مفهوم را نامگذاری کنید، زمان استخراج فرا رسیده است.
نامهایی مثل:
✅
Money✅
TaxRate✅
InvoiceNumberاحتمالاً دانش دامنه هستند و ارزش تبدیل شدن به Value Object را دارند.
اما اگر بهترین نامی که پیدا میکنید اینها باشد:
❌
Helper❌
Utils❌
ProcessDataاحتمالاً در حال Abstract کردن شکل کد هستید، نه دانش.
✅ زمانی که DRY واقعاً درست است
وقتی درست استفاده شود، DRY فوقالعاده ارزشمند است.
یک قانون کسبوکار باید فقط در یک مکان وجود داشته باشد.
فرض کنید قانون زیر را در سه بخش مختلف کپی کردهایم:
«سفارشهای بالاتر از ۱۰۰۰ دلار نیاز به تأیید مدیر دارند.»
// OrderService
if (order.Total > 1000) { /* require approval */ }
// CheckoutService
if (order.Total > 1000m) { /* require approval */ }
// AdminController - someone bumped the limit here, and only here
if (order.Total > 5000) { /* require approval */ }
بالاخره روزی دو مورد را تغییر میدهید و سومی را فراموش میکنید.
همین نسخه سومِ منحرفشده منشأ باگ خواهد بود.
قانون را به Domain Model منتقل کنید تا تنها یک خانه داشته باشد:
public bool RequiresManagerApproval() => Total > 1000;
این دقیقاً همان «نمایش یکتای مرجع» است که DRY درباره آن صحبت میکند.
📝 جمعبندی
🔹 ءDRY درباره دانش است، نه کدهایی که شبیه هم هستند.
🔹 هزینه یک Abstraction اشتباه از هزینه Duplication بیشتر است و حذف آن نیز سختتر خواهد بود.
🔹 تا بار سوم صبر کنید. فقط زمانی استخراج کنید که هر دو نسخه یک حقیقت مشترک را نمایش میدهند و باید با هم تغییر کنند.
🔹 دفعه بعد که خواستید یک کد تکراری را حذف کنید، از خودتان نپرسید:
«آیا این دو قطعه کد شبیه هم هستند؟»
بپرسید:
«آیا معنای یکسانی دارند؟»
همین یک سؤال، دردسرهای نگهداری بیشتری را از شما دور خواهد کرد تا هر مقدار تایپ نکردنی که DRY برایتان ذخیره میکند. 🚀
🔖هشتگها:
#cleanarchitecture #dry #dotnetdeveloper
یکی از تغییرات جالبی که با افزایش تجربه اتفاق میافتد این است که دیگر از کدهای قدیمی تعجب نمیکنی.
اوایل مسیر، وقتی با یک متد ۵۰۰ خطی یا یک کلاس عجیب روبهرو میشوی، سریع قضاوت میکنی.
با خودت میگویی:«چه کسی چنین چیزی نوشته؟» اما بعد از چند سال کار روی سیستمهای واقعی، نگاهت تغییر میکند.
میفهمی بیشتر کدهای بد، توسط توسعهدهندگان بد نوشته نشدهاند.توسط توسعهدهندگانی نوشته شدهاند که تحت فشار بودهاند.
فشار زمان.
فشار تحویل.
فشار تغییر مداوم نیازمندیها.
و گاهی فشار زنده نگه داشتن سیستمی که هر روز باید کار کند.
بیشتر سیستمهای پیچیده، یک روز با نیت خوب شروع شدهاند.اما هر تصمیم کوچک،
هر میانبر موقت، و هر «فعلاً همین را Deploy کنیم» لایهای جدید به پیچیدگی آن اضافه کرده است.
به همین دلیل است که قضاوت کردن کدهای قدیمی آسان است.اما نگهداری و بهبود آنها مهارت واقعی را نشان میدهد.
هر توسعهدهندهای میتواند درباره ساختن یک سیستم ایدهآل صحبت کند.
اما مهندس واقعی کسی است که بتواند با واقعیتهای یک سیستم غیرایدهآل کار کند و آن را قدمبهقدم بهتر کند.
چون در دنیای واقعی، بیشتر ما نرمافزار را از صفر نمیسازیم.ما وارث تصمیمهای دیروز هستیم.
اوایل مسیر، وقتی با یک متد ۵۰۰ خطی یا یک کلاس عجیب روبهرو میشوی، سریع قضاوت میکنی.
با خودت میگویی:«چه کسی چنین چیزی نوشته؟» اما بعد از چند سال کار روی سیستمهای واقعی، نگاهت تغییر میکند.
میفهمی بیشتر کدهای بد، توسط توسعهدهندگان بد نوشته نشدهاند.توسط توسعهدهندگانی نوشته شدهاند که تحت فشار بودهاند.
فشار زمان.
فشار تحویل.
فشار تغییر مداوم نیازمندیها.
و گاهی فشار زنده نگه داشتن سیستمی که هر روز باید کار کند.
بیشتر سیستمهای پیچیده، یک روز با نیت خوب شروع شدهاند.اما هر تصمیم کوچک،
هر میانبر موقت، و هر «فعلاً همین را Deploy کنیم» لایهای جدید به پیچیدگی آن اضافه کرده است.
به همین دلیل است که قضاوت کردن کدهای قدیمی آسان است.اما نگهداری و بهبود آنها مهارت واقعی را نشان میدهد.
هر توسعهدهندهای میتواند درباره ساختن یک سیستم ایدهآل صحبت کند.
اما مهندس واقعی کسی است که بتواند با واقعیتهای یک سیستم غیرایدهآل کار کند و آن را قدمبهقدم بهتر کند.
چون در دنیای واقعی، بیشتر ما نرمافزار را از صفر نمیسازیم.ما وارث تصمیمهای دیروز هستیم.
یکی از خطرناکترین اتفاقاتی که میتواند برای یک تیم فنی بیفتد، این نیست که اشتباه کند.این است که دیگر اشتباهاتش را نبیند.
در روزهای اول یک پروژه، همه چیز زیر ذرهبین است.هر Bug بررسی میشود.هر تصمیم به چالش کشیده میشود.هر تغییر مورد بحث قرار میگیرد.
اما به مرور زمان، بعضی مشکلات آنقدر تکرار میشوند که به بخشی از واقعیت روزمره تبدیل میشوند.Buildها گاهی Fail میشوند.طبیعی است.
استقرارها کمی استرس دارند.طبیعی است.
برخی سرویسها هر از گاهی دچار مشکل میشوند.طبیعی است.
تعدادی Bug هم همیشه در Production وجود دارد.طبیعی است.
و دقیقاً از همین نقطه، خطر شروع میشود.
چون بسیاری از مشکلات بزرگ، زمانی ایجاد نمیشوند که یک اتفاق غیرعادی رخ دهد.زمانی ایجاد میشوند که یک اتفاق اشتباه، عادی شود.
تیمهای حرفهای فقط به دنبال حل مشکلات نیستند.آنها مراقب هستند که به مشکلات عادت نکنند.
چون هر بار که یک ضعف را به عنوان بخشی از طبیعت سیستم میپذیریم،استانداردهای خودمان را کمی پایینتر میآوریم.
و در مهندسی نرمافزار، سقوط معمولاً ناگهانی اتفاق نمیافتد.از مجموعهای از چیزهایی شروع میشود که کمکم برایمان عادی شدهاند.
در روزهای اول یک پروژه، همه چیز زیر ذرهبین است.هر Bug بررسی میشود.هر تصمیم به چالش کشیده میشود.هر تغییر مورد بحث قرار میگیرد.
اما به مرور زمان، بعضی مشکلات آنقدر تکرار میشوند که به بخشی از واقعیت روزمره تبدیل میشوند.Buildها گاهی Fail میشوند.طبیعی است.
استقرارها کمی استرس دارند.طبیعی است.
برخی سرویسها هر از گاهی دچار مشکل میشوند.طبیعی است.
تعدادی Bug هم همیشه در Production وجود دارد.طبیعی است.
و دقیقاً از همین نقطه، خطر شروع میشود.
چون بسیاری از مشکلات بزرگ، زمانی ایجاد نمیشوند که یک اتفاق غیرعادی رخ دهد.زمانی ایجاد میشوند که یک اتفاق اشتباه، عادی شود.
تیمهای حرفهای فقط به دنبال حل مشکلات نیستند.آنها مراقب هستند که به مشکلات عادت نکنند.
چون هر بار که یک ضعف را به عنوان بخشی از طبیعت سیستم میپذیریم،استانداردهای خودمان را کمی پایینتر میآوریم.
و در مهندسی نرمافزار، سقوط معمولاً ناگهانی اتفاق نمیافتد.از مجموعهای از چیزهایی شروع میشود که کمکم برایمان عادی شدهاند.
بعضی از تصمیمهای اشتباه در مهندسی نرمافزار، در لحظه کاملاً منطقی به نظر میرسند.همه اطلاعات موجود از آن تصمیم حمایت میکنند.
فشار زمان وجود دارد.
نیاز کسبوکار مشخص است.
راهحل هم جواب میدهد.
پس تصمیم گرفته میشود.
مشکل از جایی شروع میشود که چند ماه بعد، نتیجه آن تصمیم را میبینیم.اینجاست که معمولاً جملههای آشنایی شنیده میشود:
«از اول معلوم بود اشتباه است.»
«نباید این کار را انجام میدادیم.»
«همان موقع هم مشخص بود به مشکل میخوریم.»
اما واقعیت این است که بسیاری از تصمیمهای امروز، با دانشی که امروز داریم قضاوت میشوند.نه با دانشی که آن روز در اختیار داشتیم.
به همین دلیل است که تیمهای حرفهای کمتر به دنبال پیدا کردن مقصر هستند.و بیشتر تلاش میکنند فرآیند تصمیمگیری را بهبود دهند.
چون در پروژههای واقعی، همیشه نمیتوان بهترین تصمیم را گرفت.گاهی فقط میتوان بهترین تصمیم ممکن را با اطلاعات موجود گرفت.
بلوغ مهندسی زمانی اتفاق میافتد که به جای قضاوت گذشته،از آن برای ساختن تصمیمهای بهتر در آینده استفاده کنیم.
در نهایت، کیفیت یک تیم را تعداد اشتباهاتش مشخص نمیکند.توانایی یاد گرفتن از آنها مشخص میکند.
فشار زمان وجود دارد.
نیاز کسبوکار مشخص است.
راهحل هم جواب میدهد.
پس تصمیم گرفته میشود.
مشکل از جایی شروع میشود که چند ماه بعد، نتیجه آن تصمیم را میبینیم.اینجاست که معمولاً جملههای آشنایی شنیده میشود:
«از اول معلوم بود اشتباه است.»
«نباید این کار را انجام میدادیم.»
«همان موقع هم مشخص بود به مشکل میخوریم.»
اما واقعیت این است که بسیاری از تصمیمهای امروز، با دانشی که امروز داریم قضاوت میشوند.نه با دانشی که آن روز در اختیار داشتیم.
به همین دلیل است که تیمهای حرفهای کمتر به دنبال پیدا کردن مقصر هستند.و بیشتر تلاش میکنند فرآیند تصمیمگیری را بهبود دهند.
چون در پروژههای واقعی، همیشه نمیتوان بهترین تصمیم را گرفت.گاهی فقط میتوان بهترین تصمیم ممکن را با اطلاعات موجود گرفت.
بلوغ مهندسی زمانی اتفاق میافتد که به جای قضاوت گذشته،از آن برای ساختن تصمیمهای بهتر در آینده استفاده کنیم.
در نهایت، کیفیت یک تیم را تعداد اشتباهاتش مشخص نمیکند.توانایی یاد گرفتن از آنها مشخص میکند.
Forwarded from thisisnabi.dev [Farsi] (Nabi Karampour)
بچه ها بیاید با هم یک چالش بذاریم تا آخر سال بلکه یه نظمی در یادگیری پیدا کنیم.
من هر پنجشنبه ساعت 09:00 یک تاپیک رو معرفی میکنم با یکسری منبع، اگر منابع رو خوندید و موضوع رو یاد گرفتید پستش رو لایک کنید.
خواهشم اینه که اگر انجام ندادید لایک نکنید که آمارمون به هم نریزه.
@thisisnabi_dev
من هر پنجشنبه ساعت 09:00 یک تاپیک رو معرفی میکنم با یکسری منبع، اگر منابع رو خوندید و موضوع رو یاد گرفتید پستش رو لایک کنید.
خواهشم اینه که اگر انجام ندادید لایک نکنید که آمارمون به هم نریزه.
@thisisnabi_dev
چند سال پیش فکر میکردم بهترین برنامهنویس تیم کسی است که برای هر سؤال سریع جواب داشته باشد.
هر مشکلی پیش میآمد فوری راهحل میداد.
هر بحثی میشد نظر قطعی داشت.
هر تصمیمی مطرح میشد سریع انتخاب میکرد.
بعد از مدتی با آدمهای باتجربهتری آشنا شدم.
متوجه شدم اتفاقاً آنها کمتر جواب میدهند.
بیشتر سؤال میپرسند.
میپرسند:
اگر این فرض اشتباه باشد چه؟
اگر حجم کاربران ۱۰ برابر شود چه؟
اگر تیم دیگری بخواهد این کد را نگهداری کند چه؟
اگر نیازمندی تغییر کند چه؟
اول فکر میکردم مردد هستند.
بعد فهمیدم دارند هزینه تصمیم را میسنجند.
تازه آنجا بود که متوجه شدم.
خیلی وقتها اعتمادبهنفس نشانه دانستن نیست.
نشانه ندیدن پیچیدگیهای مسئله است.
هرچه تجربه بیشتر میشود، تعداد جوابهای قطعی کمتر میشود.
چون میفهمی پشت هر تصمیمی که امروز میگیری،
فهرستی از پیامدها ایستادهاند که هنوز خودشان را نشان ندادهاند.
شاید یکی از نشانههای رشد حرفهای این باشد که
به جای اینکه سریعتر جواب بدهی،
عمیقتر فکر کنی.
هر مشکلی پیش میآمد فوری راهحل میداد.
هر بحثی میشد نظر قطعی داشت.
هر تصمیمی مطرح میشد سریع انتخاب میکرد.
بعد از مدتی با آدمهای باتجربهتری آشنا شدم.
متوجه شدم اتفاقاً آنها کمتر جواب میدهند.
بیشتر سؤال میپرسند.
میپرسند:
اگر این فرض اشتباه باشد چه؟
اگر حجم کاربران ۱۰ برابر شود چه؟
اگر تیم دیگری بخواهد این کد را نگهداری کند چه؟
اگر نیازمندی تغییر کند چه؟
اول فکر میکردم مردد هستند.
بعد فهمیدم دارند هزینه تصمیم را میسنجند.
تازه آنجا بود که متوجه شدم.
خیلی وقتها اعتمادبهنفس نشانه دانستن نیست.
نشانه ندیدن پیچیدگیهای مسئله است.
هرچه تجربه بیشتر میشود، تعداد جوابهای قطعی کمتر میشود.
چون میفهمی پشت هر تصمیمی که امروز میگیری،
فهرستی از پیامدها ایستادهاند که هنوز خودشان را نشان ندادهاند.
شاید یکی از نشانههای رشد حرفهای این باشد که
به جای اینکه سریعتر جواب بدهی،
عمیقتر فکر کنی.
یه زمانی فکر میکردم مشکل پروژهها از کمبود آدمهای حرفهایه.
فکر میکردم اگه چند تا Senior Developer خوب بیاری، بیشتر مشکلات حل میشه.
بعد توی چند تا پروژه مختلف دیدم.
پروژههایی که آدمهای خیلی قوی داشتن.
و پروژههایی که تیمشون معمولیتر بود.
یه نکته جالب دیدم.
خیلی از پروژههای موفق، بهترین آدمها رو نداشتن.ولی بهترین تصمیمها رو داشتن.
و خیلی از پروژههای ناموفق، پر بودن از آدمهای باهوش.
اما هر کسی در حال اثبات خودش بود.
هر کسی میخواست ثابت کنه راهحلش بهتره.
هر کسی میخواست معماری خودش انتخاب بشه.
هر کسی میخواست حرف آخر رو بزنه.
کمکم فهمیدم که موفقیت یک تیم، فقط جمع توانایی اعضاش نیست.
حاصل جهتگیری اون تواناییهاست.
هشت نفر که در یک مسیر حرکت میکنن،
معمولاً نتیجه بهتری میگیرن از هشت نفر فوقالعاده که هر کدوم به یک سمت میکشن.
در دنیای نرمافزار، مشکل همیشه کمبود استعداد نیست.
خیلی وقتها، کمبود همراستاییه.
و ساختن همراستایی،معمولاً از نوشتن کد خیلی سختتره.
فکر میکردم اگه چند تا Senior Developer خوب بیاری، بیشتر مشکلات حل میشه.
بعد توی چند تا پروژه مختلف دیدم.
پروژههایی که آدمهای خیلی قوی داشتن.
و پروژههایی که تیمشون معمولیتر بود.
یه نکته جالب دیدم.
خیلی از پروژههای موفق، بهترین آدمها رو نداشتن.ولی بهترین تصمیمها رو داشتن.
و خیلی از پروژههای ناموفق، پر بودن از آدمهای باهوش.
اما هر کسی در حال اثبات خودش بود.
هر کسی میخواست ثابت کنه راهحلش بهتره.
هر کسی میخواست معماری خودش انتخاب بشه.
هر کسی میخواست حرف آخر رو بزنه.
کمکم فهمیدم که موفقیت یک تیم، فقط جمع توانایی اعضاش نیست.
حاصل جهتگیری اون تواناییهاست.
هشت نفر که در یک مسیر حرکت میکنن،
معمولاً نتیجه بهتری میگیرن از هشت نفر فوقالعاده که هر کدوم به یک سمت میکشن.
در دنیای نرمافزار، مشکل همیشه کمبود استعداد نیست.
خیلی وقتها، کمبود همراستاییه.
و ساختن همراستایی،معمولاً از نوشتن کد خیلی سختتره.