Forwarded from tech-afternoon (Amin Mesbahi)
🪞روز مهندس، و داستان جناب هانسکریستیناندرسون که هنوز تمام نشده!
قرن ۱۹ میلادی، نویسنده سرشناس دانمارکی، در کنار داستانهای به ظاهر سادهای مثل دختر کبریتفروش یا جوجهاردک زشت، داستان لباس جدید پادشاه رو نوشت که احتمالا اکثر ما یا کتابش رو در کودکی خوندیم، یا کارتونش رو تماشا کردیم.
فکر میکنم بد نباشه جامعه مهندسی نرمافزار، گاهی جلو آیینه بایسته و ببینه که چقدر لُخت است! توی قصه «پادشاه لخت»، مشکل فقط یک پادشاه سادهلوح نبود. مسئله، یک اکوسیستم کامل بود:
- خیاطهای دروغین
- درباریان تأییدکننده
- جمعیتی که جرئت پرسیدن نداشت
و فقط یک کودک بود که گفت: چیزی تنش نیست!
اگر بخوایم صادق باشیم، ما هم در اکوسیستم نرمافزار، کم از اون دربار نداریم. و چقدر برای مناسب ظاهر شدن، نیاز به یادگیری و تلاش مضاعف داریم. خیاطهای دروغین امروز همیشه شیادهای بیرونی نیستن.
گاهی در لباس لیدر فنی ظاهر میشن که بدون threat model و بدون طراحی امنیتی، سیستم رو «production-ready» اعلام میکنن.
گاهی در قامت تیم محصول که زمان تحویل را بالاتر از امنیت و کیفیت مینشونن.
گاهی در نقش مدیر که بودجه آموزش، معماری، یا امنیت رو هزینه اضافی میدونن.
و گاهی هم در قامت مهندس باتجربهای که سالهاست چیز جدیدی یاد نگرفته ولی همچنان با اعتمادبهنفس حرف میزنه.
و درباریان؟
ما وقتی بدون خوندن دقیق PR رو تأیید میکنیم.
وقتی postmortem واقعی نمینویسیم.
وقتی ضعف امنیتی رو میدونیم ولی میگیم “بعداً درستش میکنیم”.
وقتی outage رو «اتفاق طبیعی» جا میزنیم.
مسئله این نیست که هک شدیم یا نشدیم. کند یا ناپایدار هستیم یا نه!
مسئله اینه که آیا اصول مهندسی رو جدی گرفتهایم یا نه.
اما سؤال جدی اینه:
چند تیم واقعاً اینها رو در عمل اجرا میکنند، نه فقط در رزومه؟
اگر بخواهیم جلوی آینه بایستیم، شاید بهتر باشد به جای شعار، این چکلیست رو از خودمون بپرسیم:
آیا ما اینها رو داریم؟
- اصول Security by Design، نه Security after Incident
- مفاهیم Threat Modeling مستند
- ساختار Secure SDLC واقعی، نه اسلاید پاورپوینت
- مکانیزمهای observability جدی
- ساختار Data Governance مشخص
- طبقهبندی داده و سیاست retention
- مدیریت دسترسی مبتنی بر اصل Least Privilege یا زیروتراست
- اصول Software Composition Analysis و مدیریت dependency
- ساختار Incident response plan تمرینشده
و...
اگر اینها نیست، ما فقط امیدواریم، مهندسی نمیکنیم.
روز مهندس شاید بیشتر از اونکه تبریک بخواد، احتیاج به صداقت داره.
صداقت با خودمون.
شاید وقتش باشه به جای تبریکهای شاعرانه، هرکدوممون یک ضعف مهندسی رو در تیممون جدی اصلاح کنیم. (جلو آینه بایستیم و لخت بودنمون رو ببینیم!)
لباس، با آرزو دوخته نمیشه.
با استاندارد، تمرین و مسئولیتپذیری دوخته میشه.
قرن ۱۹ میلادی، نویسنده سرشناس دانمارکی، در کنار داستانهای به ظاهر سادهای مثل دختر کبریتفروش یا جوجهاردک زشت، داستان لباس جدید پادشاه رو نوشت که احتمالا اکثر ما یا کتابش رو در کودکی خوندیم، یا کارتونش رو تماشا کردیم.
فکر میکنم بد نباشه جامعه مهندسی نرمافزار، گاهی جلو آیینه بایسته و ببینه که چقدر لُخت است! توی قصه «پادشاه لخت»، مشکل فقط یک پادشاه سادهلوح نبود. مسئله، یک اکوسیستم کامل بود:
- خیاطهای دروغین
- درباریان تأییدکننده
- جمعیتی که جرئت پرسیدن نداشت
و فقط یک کودک بود که گفت: چیزی تنش نیست!
اگر بخوایم صادق باشیم، ما هم در اکوسیستم نرمافزار، کم از اون دربار نداریم. و چقدر برای مناسب ظاهر شدن، نیاز به یادگیری و تلاش مضاعف داریم. خیاطهای دروغین امروز همیشه شیادهای بیرونی نیستن.
گاهی در لباس لیدر فنی ظاهر میشن که بدون threat model و بدون طراحی امنیتی، سیستم رو «production-ready» اعلام میکنن.
گاهی در قامت تیم محصول که زمان تحویل را بالاتر از امنیت و کیفیت مینشونن.
گاهی در نقش مدیر که بودجه آموزش، معماری، یا امنیت رو هزینه اضافی میدونن.
و گاهی هم در قامت مهندس باتجربهای که سالهاست چیز جدیدی یاد نگرفته ولی همچنان با اعتمادبهنفس حرف میزنه.
و درباریان؟
ما وقتی بدون خوندن دقیق PR رو تأیید میکنیم.
وقتی postmortem واقعی نمینویسیم.
وقتی ضعف امنیتی رو میدونیم ولی میگیم “بعداً درستش میکنیم”.
وقتی outage رو «اتفاق طبیعی» جا میزنیم.
مسئله این نیست که هک شدیم یا نشدیم. کند یا ناپایدار هستیم یا نه!
مسئله اینه که آیا اصول مهندسی رو جدی گرفتهایم یا نه.
من قبلاً دو اپیزود درباره تولید امن نرمافزار ساختم. درباره: SSDLC, STRIDE, Shift-left, SAST, DAST, IAST, RASP, SCA
در مورد بدهی فنی هم یه ویدیو کوتاه
اما سؤال جدی اینه:
چند تیم واقعاً اینها رو در عمل اجرا میکنند، نه فقط در رزومه؟
اگر بخواهیم جلوی آینه بایستیم، شاید بهتر باشد به جای شعار، این چکلیست رو از خودمون بپرسیم:
آیا ما اینها رو داریم؟
- اصول Security by Design، نه Security after Incident
- مفاهیم Threat Modeling مستند
- ساختار Secure SDLC واقعی، نه اسلاید پاورپوینت
- مکانیزمهای observability جدی
- ساختار Data Governance مشخص
- طبقهبندی داده و سیاست retention
- مدیریت دسترسی مبتنی بر اصل Least Privilege یا زیروتراست
- اصول Software Composition Analysis و مدیریت dependency
- ساختار Incident response plan تمرینشده
و...
اگر اینها نیست، ما فقط امیدواریم، مهندسی نمیکنیم.
روز مهندس شاید بیشتر از اونکه تبریک بخواد، احتیاج به صداقت داره.
صداقت با خودمون.
شاید وقتش باشه به جای تبریکهای شاعرانه، هرکدوممون یک ضعف مهندسی رو در تیممون جدی اصلاح کنیم. (جلو آینه بایستیم و لخت بودنمون رو ببینیم!)
لباس، با آرزو دوخته نمیشه.
با استاندارد، تمرین و مسئولیتپذیری دوخته میشه.
منظورم از اکوسیستم، فقط گروه خاصی نیست که ربطش بدیم به وقایع سیاسی و اجتماعی کنونی و بگیم اونا که از ما نیستن یا رفتنیاند و بعدش درستش میشه و از خودمون صلب مسئولیت کنیم.
از استارتاپهای متعدد خصوصی که هک شدن، تا بانک و سازمانی که با هک شدن دادههاش نابود شد و از روی پیامکها موجودی مردم رو بازسازی! کردن! تا نرمافزار دانشگاه تا... همه و همه گواهی بر لُختی جامعه نرم افزاریه!
ءVertical Slices دقیقاً کجای Modular Monolith قرار میگیرند؟ 🤔
بیشتر تیمها معماری در سطح کلان (macro architecture) را بهدرستی پیادهسازی میکنند. آنها یک Modular Monolith با مرزهای مشخص بین ماژولها، APIهای عمومی و ایزولاسیون مناسب دادهها میسازند.
اما بعد از آن، دیگر به معماری فکر نمیکنند! ❌
هر ماژول دقیقاً همان ساختار داخلی را دریافت میکند. معمولاً نوعی از Layered Architecture.
موضوع اینجاست که Clean Architecture و Vertical Slice Architecture آنقدرها هم که فکر میکنید از هم دور نیستند. هر دو روی Use Caseها تمرکز دارند و هدفشان حداکثرسازی Cohesion است.
ءClean Architecture فقط یک قانون اضافه میکند:
جهت وابستگیها (Direction of Dependencies) باید کنترل شود. که این معمولاً منجر به Abstractionها و Ceremonyهای بیشتری میشود.
رویکرد Pragmatic Clean Architecture یک حد وسط را انتخاب میکند و از نظر ماهیت، شباهت زیادی به VSA دارد.
سوال اصلی این نیست که کدامیک "بهتر" است.
سوال این است که هر کدام در کجای Modular Monolith شما میدرخشند ✨
و بخش جذاب ماجرا اینجاست:
میتوانید آنها را با هم Mix & Match کنید.
🧠 دو سطح از معماری
وقتی یک Modular Monolith میسازید، باید دو تصمیم معماری مهم بگیرید:
1️⃣ Macro Architecture
سیستم را چطور به ماژولها تقسیم میکنید؟
این شامل موارد زیر است:
🔹️مرزهای ماژولها
🔹️الگوهای ارتباطی بین ماژولها
🔹️ایزولاسیون دادهها
🔹️طراحی Public API
🔹️نحوه Deploy شدن ماژولها
2️⃣ Micro Architecture
کد داخل هر ماژول چطور سازماندهی میشود؟
این شامل موارد زیر است:
🔸️ساختار پوشهها
🔸️جهت وابستگیها
🔸️نحوه پیادهسازی Use Caseها
🔸️محل قرارگیری Validation
ُ🔸️نحوه دسترسی به دیتابیس
بیشتر مقالات درباره Modular Monolith کاملاً روی سطح Macro تمرکز میکنند.
و دلیل خوبی هم دارد — اشتباه گرفتن مرزهای ماژولها، هزینهی اصلاح بسیار بالایی دارد 💸
اما سطح Micro هم به همان اندازه مهم است.
چرا؟ چون تعیین میکند:
🔹️اضافه کردن Feature جدید چقدر آسان است
🔹️ءNavigation در کد چقدر ساده است
🔹️ءOnboard کردن توسعهدهنده جدید چقدر زمان میبرد
این همان معماریای است که تیم شما هر روز با آن تعامل دارد.
🔑 نکته کلیدی:
ءMacro Architecture مشخص میکند که ماژولها چگونه با یکدیگر تعامل داشته باشند.
اما Micro Architecture یک تصمیم محلی است که هر ماژول میتواند بهصورت مستقل بگیرد.
ماژول Ticketing شما مجبور نیست همان ساختار داخلی ماژول Notifications را دنبال کند.
مرز ماژولار این آزادی را به شما میدهد 🚀
ءVertical Sliceها ماژول نیستند ❗️
من دیدهام که بعضیها Vertical Sliceها را با ماژولها یکی میدانند.
در سطح Macro، یک ماژول میتواند شبیه یک "vertical slice" از دامنهی کسبوکار به نظر برسد.
اما این تشبیه در سطح Application از بین میرود.
یک ماژول یک Bounded Context است.
مالک دادههای خودش است، یک Public API ارائه میدهد، و یک قابلیت کسبوکاری را کپسوله میکند.
یک Vertical Slice یک الگوی پیادهسازی Feature است.
درخواست (Request)، Handler، Validation، و Data Access را برای یک Use Case در کنار هم گروهبندی میکند.
ماژولها و Vertical Sliceها در سطوح متفاوتی عمل میکنند.ماژولها مرزهای سیستم را تعریف میکنند.Vertical Sliceها کد داخل این مرزها را سازماندهی میکنند.
📦 ءVertical Sliceها داخل یک ماژول
ءVertical Slice Architecture کد را بر اساس Feature سازماندهی میکند، نه بر اساس لایههای فنی.
هر Feature یک واحد Self-contained است:
🔹️ءRequest، Handler، Validation، Data Access — همه در یک مکان.
🔸️در داخل یک ماژول از Modular Monolith، این یک انتخاب طبیعی است. مرز ماژول از قبل جداسازی از بقیه سیستم را اعمال میکند. شما به Layerها برای محافظت نیازی ندارید.Public API ماژول این کار را انجام میدهد.
🧭 انتخاب معماری داخلی
یک سوءبرداشت رایج این است که VSA فقط برای ماژولهای ساده است و Clean Architecture برای ماژولهای پیچیده. اینطور نیست.
ءVertical Sliceها با Domain Modelهای غنی هم عالی کار میکنند. میتوانید Domain Entityها، Value Objectها، و Domain Eventها را داخل یک Vertical Slice داشته باشید.
ءSlice نقطه ورود و Orchestration را سازماندهی میکند.Domain Model قوانین کسبوکار را مدیریت میکند. با رشد پیچیدگی Slice، منطق را به Domain منتقل میکنید دقیقاً همانطور که در Clean Architecture انجام میدهید.
بهطور مشابه، Pragmatic Clean Architecture برای ماژولهای ساده هم به خوبی کار میکند. پس تصمیمگیری درباره پیچیدگی نیست. بلکه درباره راحتی تیم شما و میزان شفافیتی است که به دست میآورید.
📌 چند موردی که باید در نظر بگیرید:
✨️آشنایی تیم
اگر تیم شما با Layer و Direction of Dependencies فکر میکند، Clean Architecture طبیعیتر خواهد بود. اگر ترجیح میدهند حول Featureها سازماندهی شوند، VSA اصطکاک را کاهش میدهد.
✨️منطق Domain مشترک
وقتی Use Caseهای زیادی از Domain Entityهای مشابه استفاده میکنند، داشتن یک Domain Layer اختصاصی میتواند این اشتراک را واضحتر کند. در VSA میتوانید منطق مشترک را به یک پوشه جداگانه استخراج کنید.
✨️استقلال Featureها
وقتی Featureها مستقل هستند و به ندرت رفتار مشترک دارند، Vertical Sliceها همه چیز را ساده نگه میدارند.
مرز ماژول، صرفنظر از انتخاب شما در داخل آن،از بقیه سیستم محافظت میکند.
پس چیزی را انتخاب کنید که تیم شما را بیشترین بهرهور میکند و آماده تغییر باشید وقتی ماژول تکامل پیدا میکند.
🎯 جمعبندی
ءModular Monolith به سوال Macro پاسخ میدهد:
چگونه سیستم را به ماژولهایی با مرزهای مشخص تقسیم کنیم؟
ءVertical Slice Architecture به سوال Micro پاسخ میدهد:چگونه کد را بر اساس Feature داخل این ماژولها سازماندهی کنیم؟
آنها در سطوح متفاوتی عمل میکنند.
ءModular Monolith Cohesion بالا در سطح ماژول فراهم میکند و Coupling بین ماژولها را مدیریت میکند.
ءVertical Slice Architecture Cohesion بالا در سطح Feature داخل ماژول فراهم میکند.
شما مجبور نیستید یک معماری داخلی را برای کل سیستم انتخاب کنید. هر ماژول میتواند گزینه مناسب با Context خودش را انتخاب کند.برخی ماژولها از Pragmatic Clean Architecture استفاده خواهند کرد.
برخی دیگر از Vertical Sliceها.
مرز ماژول که بهخوبی تعریف شده باشد،
این انتخاب را ایمن میکند.
🔖هشتگها:
#DotNet #SoftwareArchitecture #ModularMonolith #VerticalSliceArchitecture #CleanArchitecture
📌Domain Driven Design : Value Object
مفهوم Value Object یکی از مهمترین Building Blockهایی است که توسط Eric Evans در کتاب معروفش Domain-Driven Design: Tackling Complexity in the Heart of Software معرفی شد؛ مفهومی که اگر درست درک شود، میتواند ساختار Domain Model شما را بهشدت سادهتر، Maintainableتر و Expressiveتر کند.
💡ءValue Object چیست؟
در DDD، همه چیز در Domain یا Identity دارد یا فقط Value است.
یک Value Object:
🔹 هویتی (Identity) ندارد
🔹 فقط با مقدارش (Value) تعریف میشود
🔹 ءImmutable است
🔹 قابل جایگزینی است بدون اینکه معنی Domain تغییر کند
به بیان ساده:
اگر دو Object از نظر مقدار برابر باشند، در Domain یکی محسوب میشوند.
مثال:
📍 Address
💰 Money
📏 Distance
در Domain، مهم نیست کدام Address instance را داریم؛ اگر مقدارشان برابر باشد، از نظر Business یکی هستند.
🆚 Entity vs Value Object
📌Entity :
• دارای Identity
• ءMutable
• ءLifecycle دارد
• مقایسه با Id
• قابل Track شدن
📌 Value Object :
• بدون Identity
• ءImmutable
• ءLifecycle ندارد
• مقایسه با Value
• ءReplaceable
مثال:
Customer → Entity
CustomerEmail → Value Object
در دامین، Email هویت ندارد؛ فقط مقدار آن مهم است.
🔒 Immutability در Value Object
طبق تعریف Evans:
"Value Object should be immutable"
چرا؟چون اگر مقدار تغییر کند، دیگر همان مفهوم قبلی نیست.
مثال:
public sealed class Email
{
public string Value { get; }
public Email(string value)
{
if(string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Email is required");
Value = value;
}
}
اگر Email تغییر کند:
یعنی یک Email جدید ساختهایم،
نه اینکه Email قبلی تغییر کرده باشد.
⚖️ Equality بر اساس Value
ءValue Objectها باید بر اساس مقدارشان مقایسه شوند نه Reference.
public override bool Equals(object? obj)
{
if (obj is not Email other)
return false;
return Value == other.Value;
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
در Domain:
new Email("a@gmail.com") == new Email("a@gmail.com")باید True باشد حتی اگر دو Instance متفاوت باشند.
🎯 چه زمانی از Value Object استفاده کنیم؟
وقتی:
✅ ءObject مفهوم Domain دارد
✅ ءIdentity ندارد
✅ فقط با مقدار تعریف میشود
✅ قابل تغییر نیست
✅ ءLifecycle مستقل ندارد
🚫 ءAnti-Pattern رایج
بسیاری از پروژهها این اشتباه را انجام میدهند:
public string Email { get; set; }در حالی که:
ءEmail یک Primitive نیست
بلکه یک Concept در Domain است.
این همان Primitive Obsession است که DDD سعی دارد از آن جلوگیری کند.
🧠 مزایای استفاده از Value Object
✔️ جلوگیری از Invalid State
✔️ افزایش Expressiveness در Domain Model
✔️ کاهش Coupling
✔️ ءEncapsulation بهتر Business Ruleها
✔️ جلوگیری از Primitive Obsession
✔️ تستپذیری بیشتر
✅ جمعبندی
ءValue Objectها کمک میکنند:
🔹️ءDomain شما Richتر شود
🔸️ءBusiness Ruleها نزدیک به Data تعریف شوند
🔹️ءModel شما از Primitive Typeها جدا شود
🔸️و مهمتر از همه، زبان Ubiquitous Language در کد منعکس شود
اگر در Domain چیزی Identity ندارد،
احتمالاً باید یک Value Object باشد.
🔖هشتگها:
#DDD #ValueObject #DomainModel
📌نکته های مفیدی که در رابطه با رزومه فرستادن وجود داره و بهمون کمک میکنه که بهتر دیده بشیم رو از زبان مهندس کرمپور بشنویم.
https://t.me/thisisnabi_dev/1521
https://t.me/thisisnabi_dev/1521
Forwarded from iCodeNext
Audio
آیا داریم کد نوشتن رو فراموش میکنیم ؟
03:00 دنیال چند طبقه ی قبلی
10:00 تحلیل رفتن ذهن
14:00 یادگیری واقعی
16:00 توهم قطعیت
21:00 بعد از فراموشی کد چطوری سیستم رو نجات بدم.
🧶 تمام صحبت ها نظرات شخصی خودم هست، و هیچ دلیلی بر درست بودنش نیست.
مثل همیشه مشتی هستید. ❤️
امیدوارم که بزودی با محتوی تصویری هم این مسیر رو جلو ببریم.
03:00 دنیال چند طبقه ی قبلی
10:00 تحلیل رفتن ذهن
14:00 یادگیری واقعی
16:00 توهم قطعیت
21:00 بعد از فراموشی کد چطوری سیستم رو نجات بدم.
🧶 تمام صحبت ها نظرات شخصی خودم هست، و هیچ دلیلی بر درست بودنش نیست.
مثل همیشه مشتی هستید. ❤️
امیدوارم که بزودی با محتوی تصویری هم این مسیر رو جلو ببریم.
🚀 چرا برای Dependency Injection در #C به Primary Constructor مهاجرت کردم؟صادقانه بگویم، مدتها در برابر Primary Constructorها مقاومت میکردم.
زمانی که در C# 12 این قابلیت از Recordها به Classها و Structهای معمولی گسترش پیدا کرد، اولین واکنش من تردید بود.
استفاده از یک mutable capture ضمنی بهجای فیلدهای صریح
readonly، در نگاه اول شبیه این بود که امنیت و صراحت کد را فدای راحتی کنیم.اما بعد از استفاده از آن در چندین پروژه مختلف، نظرم کاملاً تغییر کرد.
مقدار Boilerplateی که در کلاسهای مبتنی بر Dependency Injection حذف میشود قابل توجه است و مشکلی که در ابتدا بابت آن نگران بودم، در عمل کاملاً قابل مدیریت است؛ البته به شرطی که از آن آگاه باشید.
در این مطلب توضیح میدهم چه چیزی باعث شد به Primary Constructorها مهاجرت کنم و مهمترین نکتهای که باید هنگام استفاده از آنها بدانید چیست.
💡 چه چیزی نظرم را تغییر داد؟
کلاسهای سرویس من قبلاً معمولاً به این شکل بودند:
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
_orderRepository = orderRepository;
_logger = logger;
}
public async Task<Order?> GetOrderAsync(Guid id)
{
_logger.LogInformation("Fetching order {OrderId}", id);
return await _orderRepository.GetByIdAsync(id);
}
}
و حالا همان کلاس به این شکل نوشته میشود:
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);
}
}
تعریف فیلدها، بدنه Constructor و Assignmentها همگی حذف شدهاند.
پارامترهای Constructor بهصورت خودکار Capture میشوند و در تمام بدنه کلاس قابل استفاده هستند.
این دقیقاً رایجترین سناریوی استفاده از Primary Constructorها است:Dependency Injection در کلاسهای سرویس.
وابستگیها را تعریف میکنید و مستقیماً از آنها استفاده میکنید.
🎯 جایی که بیشترین استفاده را از آن دارم: کلاسهای سرویس مبتنی بر DI
بیشترین جایی که Primary Constructorها مرا متقاعد کردند، کلاسهای سرویس در ASP.NET Core بود.
بخش عمده زمان من صرف توسعه همین نوع کلاسها میشود و حذف Boilerplate در اینجا بهسرعت خودش را نشان میدهد.
یک نمونه واقعیتر از فرآیند Checkout:
public class CheckoutService(
IPaymentProcessor paymentProcessor,
IOrderRepository orderRepository,
ILogger<CheckoutService> logger,
IOptions<CheckoutOptions> options)
{
public async Task<CheckoutResult> ProcessAsync(
Cart cart,
CancellationToken ct = default)
{
var settings = options.Value;
if (cart.Total < settings.MinimumOrderAmount)
{
logger.LogWarning("Order below minimum: {Total}", cart.Total);
return CheckoutResult.BelowMinimum;
}
var order = Order.Create(cart);
await paymentProcessor.ChargeAsync(order, ct);
await orderRepository.SaveAsync(order, ct);
logger.LogInformation("Checkout complete for order {OrderId}", order.Id);
return CheckoutResult.Success;
}
}
چهار Dependency، بدون حتی یک خط Boilerplate.
کلاس از ابتدا تا انتها فقط منطق کسبوکار را نمایش میدهد و هیچ کد اضافهای بین آن دیده نمیشود.
این الگو بسیار خوب عمل میکند زیرا کلاسهای سرویس معمولاً نیازی به اعتبارسنجی یا تبدیل Dependencyها ندارند.Container آنها را ایجاد میکند و شما صرفاً از آنها استفاده میکنید.
به همین دلیل Primary Constructorها انتخابی بسیار مناسب برای این سناریو هستند.
🏗 استفاده در 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