Level 3: Distributed Consistency with Sagas
🔄 الگویی برای ایجاد سازگاری توزیعشده با استفاده از Saga
الگوی Outbox برای side effectهایی مثل email که fire-and-forget هستند عالی است.
اما اگر عملیات بعدی اجباری باشد چه؟
📌 سناریو:
وقتی یک کاربر ثبتنام میکند، باید یک crypto-wallet برای او در WalletService ایجاد کنیم.
اگر ساخت wallet شکست بخورد (مثلاً به دلیل مسائل قانونی)، نباید اجازه دهیم کاربر در سیستم ما باقی بماند.
در چنین حالتی نمیتوانیم بگوییم "بعداً retry کن".
اگر WalletService پاسخ دهد که "Fraud Detected"، باید ایجاد کاربر را undo کنیم.
این یک Distributed Transaction است، و با Saga Pattern مدیریت میشود.
یک Saga، دنبالهای از مراحل را هماهنگ میکند. اگر یکی از مراحل شکست بخورد، Compensating Transactions اجرا میشوند تا مراحل قبلی undo شوند.
Flow: Choreography-based Saga
💥 در ادامه نحوهٔ مدیریت سناریوی Failure در یک Saga مبتنی بر Choreography را میبینید:
گامبهگام:
UserService: Creates User → Publishes UserCreated
WalletService: Listens to UserCreated → Tries to create wallet
•Failure: Wallet creation fails
•Action: Publishes WalletCreationFailed
UserService: Listens to WalletCreationFailed → Deletes/Deactivates the User
با این روش به Eventual Consistency میرسیم.
ممکن است سیستم برای چند ثانیه ناسازگار باشد (کاربر وجود دارد ولی wallet ندارد)، اما در نهایت به یک وضعیت معتبر میرسد (کاربر حذف میشود).
Summary: A Heuristic for Decision Making
🧠 شما لازم نیست برای همهچیز از Saga استفاده کنید.Over-engineering به همان اندازه بد است که Tight Coupling.
از این قاعدهٔ ساده استفاده کنید:
1️⃣ آیا این یک notification ساده است؟
(Email، Analytics، Cache Invalidation)
→ از Domain Events + Outbox استفاده کنید.
اشکالی ندارد ۵ ثانیه بعد پردازش شود.
2️⃣ آیا این یک وابستگی حیاتی در Business rule است؟
(Payments، Inventory، Account Status)
→ از Saga استفاده کنید.
اگر مرحلهٔ B شکست بخورد، مرحلهٔ A باید revert شود.
کاپلینگ فقط دربارهٔ ساختار کد نیست.
دربارهٔ درک مرزهای failure است.
اگر Analytics Service از کار بیفتد، نباید مانع ثبتنام کاربر شود.
سیستمها را برای survive کردن در برابر Unhappy Path بسازید.
امیدوارم مفید بوده باشد.✨
🔗Link
🔖هشتگها:
#softwarearchitecture #distributed_systems #saga_pattern #eventdriven #ddd #cleanarchitecture #outbox_pattern
الگوی Options در ASP.NET Core 🎛
الگوی Options pattern از کلاسها برای ارائهٔ دسترسی strongly typed به گروهی از تنظیمات مرتبط استفاده میکند.
وقتی تنظیمات پیکربندی (Configuration Settings) بر اساس سناریو در کلاسهای جداگانه ایزوله میشوند، برنامه به دو اصل مهم مهندسی نرمافزار پایبند میماند:
1. Encapsulation 🔐
کلاسهایی که به تنظیمات پیکربندی وابسته هستند، فقط به همان تنظیماتی وابستهاند که واقعاً استفاده میکنند.
2. Separation of Concerns 🧩
تنظیمات بخشهای مختلف برنامه به یکدیگر وابسته یا Coupled نیستند.
ءOptions همچنین یک مکانیزم برای اعتبارسنجی دادههای پیکربندی فراهم میکند.
ءBind کردن پیکربندی سلسلهمراتبی 🔗
روش پیشنهادی برای خواندن مقادیر پیکربندی مرتبط، استفاده از Options Pattern است.
برای مثال، فرض کنید مقادیر پیکربندی زیر را داریم:
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}ایجاد کلاس PositionOptions 📦
public class PositionOptions
{
public const string Position = "Position";
public string Title { get; set; } = String.Empty;
public string Name { get; set; } = String.Empty;
}
ویژگیهای یک Options class✨️
یک کلاس Options باید:
• غیر abstract باشد.
• شامل propertyهای public read-write برای مقادیری باشد که در config وجود دارند.
• ءpropertyهای read-write آن مطابق با ورودیهای configuration مقداردهی شوند.
• فیلدها (Fields) مقداردهی نمیشوند.
در مثال بالا، فیلد Position مقداردهی نمیشود و تنها برای جلوگیری از hard-code کردن رشتهٔ "Position" استفاده میشود.
ءBind کردن تنظیمات و نمایش مقدار 📥📤
کد زیر:
متد ConfigurationBinder.Bind را فراخوانی میکند تا کلاس PositionOptions را به سکشن Position bind کند.دادههای پیکربندی Position را نمایش میدهد.
public class Test22Model : PageModel
{
private readonly IConfiguration Configuration;
public Test22Model(IConfiguration configuration)
{
Configuration = configuration;
}
public ContentResult OnGet()
{
var positionOptions = new PositionOptions();
Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);
return Content($"Title: {positionOptions.Title} \n" +
$"Name: {positionOptions.Name}");
}
}
در کد فوق، بهصورت پیشفرض، تغییرات فایل JSON configuration بعد از اجرای برنامه نیز خوانده میشوند. 🔄
استفاده از <ConfigurationBinder.Get<T✨️
متد <ConfigurationBinder.Get<T نوع مشخصشده را Bind کرده و همان نوع را برمیگرداند.
در بسیاری از موارد، استفاده از <ConfigurationBinder.Get<T راحتتر از ConfigurationBinder.Bind است.
کد زیر نحوهٔ استفاده از <Get<T با کلاس PositionOptions را نشان میدهد:
public class Test21Model : PageModel
{
private readonly IConfiguration Configuration;
public PositionOptions? positionOptions { get; private set; }
public Test21Model(IConfiguration configuration)
{
Configuration = configuration;
}
public ContentResult OnGet()
{
positionOptions = Configuration.GetSection(PositionOptions.Position)
.Get<PositionOptions>();
return Content($"Title: {positionOptions.Title} \n" +
$"Name: {positionOptions.Name}");
}
}
در کد بالا، بهصورت پیشفرض، تغییرات فایل JSON configuration بعد از اجرای برنامه نیز خوانده میشوند. 🔄
ءBind و مقداردهی برای کلاس abstract🔧
ءBind اجازه میدهد که یک کلاس abstract مقداردهی شود.
کد زیر از کلاس abstract زیر استفاده میکند:
namespace ConfigSample.Options;
public abstract class SomethingWithAName
{
public abstract string? Name { get; set; }
}
public class NameTitleOptions(int age) : SomethingWithAName
{
public const string NameTitle = "NameTitle";
public override string? Name { get; set; }
public string Title { get; set; } = string.Empty;
public int Age { get; set; } = age;
}
کد زیر مقداردهی NameTitleOptions را نمایش میدهد:
public class Test33Model : PageModel
{
private readonly IConfiguration Configuration;
public Test33Model(IConfiguration configuration)
{
Configuration = configuration;
}
public ContentResult OnGet()
{
var nameTitleOptions = new NameTitleOptions(22);
Configuration.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);
return Content($"Title: {nameTitleOptions.Title} \n" +
$"Name: {nameTitleOptions.Name} \n" +
$"Age: {nameTitleOptions.Age}"
);
}
}
تفاوت Bind و Get ⚖️
Bind:
• امکان مقداردهی یک کلاس abstract را میدهد.
• نیازی به ساخت instance ندارد.
Get<>:
• باید خودش یک instance بسازد.
• بنابراین فقط روی انواع concrete کار میکند.
Options Pattern🔖
روش دیگر هنگام استفاده از Options Pattern این است که سکشن Position را Bind کرده و آن را به Service Container اضافه کنیم.
کد زیر، کلاس PositionOptions را با Configure به DI اضافه میکند:
using ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
var app = builder.Build();
اکنون میتوانیم از طریق DI تنظیمات را بخوانیم:
public class Test2Model : PageModel
{
private readonly PositionOptions _options;
public Test2Model(IOptions<PositionOptions> options)
{
_options = options.Value;
}
public ContentResult OnGet()
{
return Content($"Title: {_options.Title} \n" +
$"Name: {_options.Name}");
}
}
در این روش، تغییرات فایل JSON بعد از شروع برنامه خوانده نمیشوند.
برای پشتیبانی از تغییرات runtime باید از IOptionsSnapshot استفاده کنید. 🔄
Options Interfaces 🔍
1️⃣ IOptions<TOptions>
پشتیبانی نمیکند از:
• خواندن تغییرات configuration بعد از start
• ءNamed options
• ءSingleton است → در تمام طول برنامه ثابت است.
• میتواند در هر service lifetime inject شود.
2️⃣ IOptionsSnapshot<TOptions>
• در سناریوهایی مفید است که تنظیمات باید در هر درخواست مجدداً محاسبه شوند.
• ءScoped است → نمیتوان آن را در یک Singleton inject کرد.
• از Named options پشتیبانی میکند.
• برای خواندن تغییرات Runtime مناسب است. 🔁
3️⃣ IOptionsMonitor<TOptions>
• برای دریافت options و مدیریت Change Notification استفاده میشود.
• ءSingleton است.
پشتیبانی میکند از:
• Reloadable configuration
• Named options
• Selective invalidation (با IOptionsMonitorCache)
4️⃣ IOptionsFactory<TOptions>
مسئول ایجاد instanceهای جدید گزینههاست.
پیکربندیها را (IConfigureOptions و IPostConfigureOptions) اجرا میکند.
5️⃣ IOptionsMonitorCache<TOptions>
• کش داخلی IOptionsMonitor
• میتواند یک گزینه را حذف کند تا مقدار جدید دوباره محاسبه شود.
• امکان اضافهکردن دستی مقدار جدید با TryAdd
• متد Clear برای ریست تمام named options
استفاده از IOptionsSnapshot برای خواندن دادههای بهروزشده ⚙️📄
IOptionsSnapshot<TOptions>🧪:
🔹️تنظیمات (Options) در هر درخواست یک بار محاسبه میشوند و برای مدت زمان همان درخواست کش میشوند.
🔹️چون یک سرویس Scoped است و در هر درخواست دوباره محاسبه میشود، ممکن است باعث هزینهٔ کارایی شود.
🔹️زمانی تغییرات پیکربندی را پس از شروع برنامه میخواند که Provider مربوطه از بارگذاری مجدد پشتیبانی کند.
تفاوت IOptionsMonitor با IOptionsSnapshot🖇:
🔸️ءIOptionsMonitor یک Singleton است و همیشه مقدار لحظهای تنظیمات را ارائه میدهد؛ مناسب برای سرویسهای Singleton.
🔸️ءIOptionsSnapshot یک Scoped است و هنگام ایجاد شدن، یک Snapshot از تنظیمات میگیرد؛ مناسب برای سرویسهای Transient و Scoped.
نمونهٔ استفاده از <IOptionsSnapshot<TOptions🧪
public class TestSnapModel : PageModel
{
private readonly MyOptions _snapshotOptions;
public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
{
_snapshotOptions = snapshotOptionsAccessor.Value;
}
public ContentResult OnGet()
{
return Content($"Option1: {_snapshotOptions.Option1} \n" +
$"Option2: {_snapshotOptions.Option2}");
}
}
ثبت MyOptions در DI 🧩
using SampleApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
var app = builder.Build();
در این حالت، تغییرات فایل JSON پس از شروع برنامه خوانده میشوند.
IOptionsMonitor 🛰
ثبت MyOptions در سرویسها (مانند قبل)
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
نمونهٔ استفاده از <IOptionsMonitor<TOptions 📡
public class TestMonitorModel : PageModel
{
private readonly IOptionsMonitor<MyOptions> _optionsDelegate;
public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
{
_optionsDelegate = optionsDelegate;
}
public ContentResult OnGet()
{
return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
$"Option2: {_optionsDelegate.CurrentValue.Option2}");
}
}
در این حالت نیز، تغییرات JSON بعد از شروع برنامه خوانده میشوند.
استفاده از ConfigurationKeyName برای تعیین کلید سفارشی 🔑✨
بهطور پیشفرض، نام پراپرتی کلاس Options برابر با نام کلید در پیکربندی است.
اگر بین نام پراپرتی و نام کلید تفاوت وجود دارد، میتوان از ConfigurationKeyName استفاده کرد.
مواقع استفاده:
• زمانی که نام کلید در پیکربندی یک شناسهٔ معتبر #C نیست.
• یا زمانی که میخواهید نام متفاوتی در کد داشته باشید.
مثال 🎯
public class PositionOptionsWithConfigurationKeyName
{
public const string Position = "Position";
[ConfigurationKeyName("position-noscript")]
public string Title { get; set; } = string.Empty;
[ConfigurationKeyName("position-name")]
public string Name { get; set; } = string.Empty;
}
فایل appsettings.json
{
"Position": {
"position-noscript": "Editor",
"position-name": "Joe Smith"
}
}
در نتیجه،
Title ← مقدار position-noscript
Name ← مقدار position-name
پشتیبانی از Named Options با استفاده از IConfigureNamedOptions 🎯⚙️
🔹️ءNamed Options چه هستند؟
ءNamed Options زمانی مفید هستند که:
• چند بخش متفاوت از configuration نیاز دارند به یک کلاس مشترک Bind شوند.
• نامگذاریها Case-Sensitive هستند.
• بتوانیم چند نسخهٔ متفاوت از یک Options را با Names مختلف مدیریت کنیم.
مثال فایل appsettings.json 📄
{
"TopItem": {
"Month": {
"Name": "Green Widget",
"Model": "GW46"
},
"Year": {
"Name": "Orange Gadget",
"Model": "OG35"
}
}
}در این مثال، دو بخش داریم:
TopItem:Month
TopItem:Year
بهجای تعریف دو کلاس جداگانه، از یک کلاس مشترک استفاده میکنیم:
کلاس مشترک TopItemSettings 🧱
public class TopItemSettings
{
public const string Month = "Month";
public const string Year = "Year";
public string Name { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty;
}
پیکربندی Named Options در DI 🛠
اینجا دو Named Options میسازیم:
TopItemSettings.Month
TopItemSettings.Year
استفاده از Named Options با IOptionsSnapshot 🔍📦
در کد بالا:
• ءSnapshot مربوط به Month گرفته میشود.
• ءSnapshot مربوط به Year گرفته میشود.
🔸️تمام Options در واقع Named Instance هستند.
🔹️ء<IConfigureOptions<T> برای Default Name اعمال میشود، یعنی نام خالی ("").
🔸️ء<IConfigureNamedOptions<T علاوهبر آنکه IConfigureOptions را هم پیادهسازی میکند، برای Named Options مخصوص استفاده میشود.
🔹️در <IOptionsFactory<TOptions منطق انتخاب نام وجود دارد:
🔸️اگر نام Null باشد یعنی Configure روی همهٔ Named Options اعمال میشود.
🔹️متدهای ConfigureAll و PostConfigureAll از همین رفتار استفاده میکنند.
این باعث میشود بتوانید:
• یک پیکربندی خاص را برای یک نام خاص تنظیم کنید.
• و یک پیکربندی کلی را برای همه نامها اعمال کنید.
چرا OptionsBuilder؟
ء<OptionsBuilder<TOptions فرایند ساخت Named Options را سادهتر میکند چون:
تنها در یک جای اولیه نام option مشخص میشود:
اما در بقیهٔ متدهای Configure نیازی نیست نام را تکرار کنید.
ءValidation Options فقط با OptionsBuilder قابل انجام است.
متدهایی که Service Dependency دریافت میکنند نیز فقط با OptionsBuilder پشتیبانی میشوند.
نمونهٔ استفاده
در بخش Options Validation به کار میرود.
سرویسها را میتوان هنگام پیکربندی options به دو روش از dependency injection دریافت کرد:
ارسال یک configuration delegate به متد Configure روی <OptionsBuilder<TOptions.
ء<OptionsBuilder<TOptions چندین overload از Configure ارائه میدهد که اجازه میدهد تا حداکثر پنج سرویس برای پیکربندی options استفاده شوند:
ایجاد یک نوع (class) که <IConfigureOptions<TOptions یا <IConfigureNamedOptions<TOptions را پیادهسازی کند و ثبت آن بهعنوان سرویس.
توصیه میشود از روش ارسال configuration delegate به Configure استفاده کنید، زیرا ایجاد یک سرویس پیچیدهتر است. ایجاد نوع دقیقاً معادل کاری است که فریمورک هنگام فراخوانی Configure انجام میدهد.
فراخوانی Configure یک سرویس transient از نوع generic IConfigureNamedOptions<TOptions ثبت میکند که دارای سازندهای است که سرویسهای generic مشخصشده را میپذیرد.
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
اینجا دو Named Options میسازیم:
TopItemSettings.Month
TopItemSettings.Year
استفاده از Named Options با IOptionsSnapshot 🔍📦
public class TestNOModel : PageModel
{
private readonly TopItemSettings _monthTopItem;
private readonly TopItemSettings _yearTopItem;
public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
{
_monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
_yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
}
public ContentResult OnGet()
{
return Content($"Month:Name {_monthTopItem.Name} \n" +
$"Month:Model {_monthTopItem.Model} \n\n" +
$"Year:Name {_yearTopItem.Name} \n" +
$"Year:Model {_yearTopItem.Model} \n");
}
}
در کد بالا:
• ءSnapshot مربوط به Month گرفته میشود.
• ءSnapshot مربوط به Year گرفته میشود.
توضیح ساختار Options و NamedOptions 🧩
🔸️تمام Options در واقع Named Instance هستند.
🔹️ء<IConfigureOptions<T> برای Default Name اعمال میشود، یعنی نام خالی ("").
🔸️ء<IConfigureNamedOptions<T علاوهبر آنکه IConfigureOptions را هم پیادهسازی میکند، برای Named Options مخصوص استفاده میشود.
🔹️در <IOptionsFactory<TOptions منطق انتخاب نام وجود دارد:
🔸️اگر نام Null باشد یعنی Configure روی همهٔ Named Options اعمال میشود.
🔹️متدهای ConfigureAll و PostConfigureAll از همین رفتار استفاده میکنند.
این باعث میشود بتوانید:
• یک پیکربندی خاص را برای یک نام خاص تنظیم کنید.
• و یک پیکربندی کلی را برای همه نامها اعمال کنید.
OptionsBuilder API 🏗✨
چرا OptionsBuilder؟
ء<OptionsBuilder<TOptions فرایند ساخت Named Options را سادهتر میکند چون:
تنها در یک جای اولیه نام option مشخص میشود:
services.AddOptions<TOptions>("MyName")اما در بقیهٔ متدهای Configure نیازی نیست نام را تکرار کنید.
ءValidation Options فقط با OptionsBuilder قابل انجام است.
متدهایی که Service Dependency دریافت میکنند نیز فقط با OptionsBuilder پشتیبانی میشوند.
نمونهٔ استفاده
در بخش Options Validation به کار میرود.
استفاده از سرویسهای DI برای پیکربندی Options ⚙️📦
سرویسها را میتوان هنگام پیکربندی options به دو روش از dependency injection دریافت کرد:
ارسال یک configuration delegate به متد Configure روی <OptionsBuilder<TOptions.
ء<OptionsBuilder<TOptions چندین overload از Configure ارائه میدهد که اجازه میدهد تا حداکثر پنج سرویس برای پیکربندی options استفاده شوند:
builder.Services.AddOptions<MyOptions>("optionalName")
.Configure<Service1, Service2, Service3, Service4, Service5>(
(o, s, s2, s3, s4, s5) =>
o.Property = DoSomethingWith(s, s2, s3, s4, s5));ایجاد یک نوع (class) که <IConfigureOptions<TOptions یا <IConfigureNamedOptions<TOptions را پیادهسازی کند و ثبت آن بهعنوان سرویس.
توصیه میشود از روش ارسال configuration delegate به Configure استفاده کنید، زیرا ایجاد یک سرویس پیچیدهتر است. ایجاد نوع دقیقاً معادل کاری است که فریمورک هنگام فراخوانی Configure انجام میدهد.
فراخوانی Configure یک سرویس transient از نوع generic IConfigureNamedOptions<TOptions ثبت میکند که دارای سازندهای است که سرویسهای generic مشخصشده را میپذیرد.
اعتبارسنجی Options ✅🔍
ءOptions validation امکان اعتبارسنجی مقدارهای options را فراهم میکند.
فایل appsettings.json زیر را در نظر بگیرید:
{
"MyConfig": {
"Key1": "My Key One",
"Key2": 10,
"Key3": 32
}
}کلاس زیر برای Bind شدن به بخش "MyConfig" استفاده میشود و چند قانون DataAnnotations را اعمال میکند:
public class MyConfigOptions
{
public const string MyConfig = "MyConfig";
[RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
public string Key1 { get; set; }
[Range(0, 1000,
ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int Key2 { get; set; }
public int Key3 { get; set; }
}
کد زیر:
ءAddOptions را فراخوانی میکند تا <OptionsBuilder<MyConfigOptions را دریافت کند که به کلاس MyConfigOptions Bind میشود.
ءValidateDataAnnotations را فراخوانی میکند تا اعتبارسنجی با استفاده از DataAnnotations فعال شود.
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations();
متد ValidateDataAnnotations در پکیج Microsoft.Extensions.Options.DataAnnotations قرار دارد. برای web appهایی که از SDK نوع Microsoft.NET.Sdk.Web استفاده میکنند، این پکیج بهطور ضمنی از shared framework مرجع میشود.
کد زیر مقدارهای configuration یا خطاهای validation را نمایش میدهد:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IOptions<MyConfigOptions> _config;
public HomeController(IOptions<MyConfigOptions> config,
ILogger<HomeController> logger)
{
_config = config;
_logger = logger;
try
{
var configValue = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (var failure in ex.Failures)
{
_logger.LogError(failure);
}
}
}
public ContentResult Index()
{
string msg;
try
{
msg = $"Key1: {_config.Value.Key1} \n" +
$"Key2: {_config.Value.Key2} \n" +
$"Key3: {_config.Value.Key3}";
}
catch (OptionsValidationException optValEx)
{
return Content(optValEx.Message);
}
return Content(msg);
}
}
کد زیر یک قانون پیچیدهتر اعتبارسنجی را با استفاده از یک delegate اعمال میکند:
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Key2 != 0)
{
return config.Key3 > config.Key2;
}
return true;
}, "Key3 must be > than Key2."); // Failure message.
IValidateOptions<TOptions> و IValidatableObject ✨
کلاس زیر رابط <IValidateOptions<TOptions را پیادهسازی میکند:
public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
public MyConfigOptions _config { get; private set; }
public MyConfigValidation(IConfiguration config)
{
_config = config.GetSection(MyConfigOptions.MyConfig)
.Get<MyConfigOptions>();
}
public ValidateOptionsResult Validate(string name, MyConfigOptions options)
{
string? vor = null;
var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
var match = rx.Match(options.Key1!);
if (string.IsNullOrEmpty(match.Value))
{
vor = $"{options.Key1} doesn't match RegEx \n";
}
if ( options.Key2 < 0 || options.Key2 > 1000)
{
vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
}
if (_config.Key2 != default)
{
if(_config.Key3 <= _config.Key2)
{
vor += "Key3 must be > than Key2.";
}
}
if (vor != null)
{
return ValidateOptionsResult.Fail(vor);
}
return ValidateOptionsResult.Success;
}
}
ءIValidateOptions این امکان را فراهم میکند که کد اعتبارسنجی از فایل Program.cs خارج شده و در یک کلاس قرار گیرد. 🧩
با استفاده از کد بالا، اعتبارسنجی در فایل Program.cs با کد زیر فعال میشود:
using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
MyConfigOptions.MyConfig));
builder.Services.AddSingleton<IValidateOptions
<MyConfigOptions>, MyConfigValidation>();
var app = builder.Build();
اعتبارسنجی Options همچنین از IValidatableObject پشتیبانی میکند. برای انجام اعتبارسنجی سطح کلاس در داخل خود کلاس:
رابط IValidatableObject و متد Validate را در کلاس پیادهسازی کنید.
در Program.cs متد ValidateDataAnnotations را فراخوانی کنید. ✔️
ValidateOnStart 🚀
اعتبارسنجی Options اولین باری که یک نمونه از TOption ساخته میشود اجرا میگردد. این یعنی مثلاً زمانیکه اولین دسترسی به IOptionsSnapshot<TOptions>.Value در pipeline یک درخواست رخ میدهد یا زمانیکه IOptionsMonitor<TOptions>.Get(string) فراخوانی میشود. پس از reload شدن تنظیمات، اعتبارسنجی دوباره اجرا میشود.
زمان اجرا از OptionsCache<TOptions> برای کش کردن نمونه ساختهشده استفاده میکند.
برای اجرای اعتبارسنجی هنگام شروع اپلیکیشن (Eager Validation) باید در Program.cs متد زیر را فراخوانی کنید:
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.ValidateOnStart();
Options post-configuration 🛠
ءPost-configuration را میتوان با <IPostConfigureOptions<TOptions انجام داد. Post-configuration پس از اجرای تمام <IConfigureOptions<TOptions انجام میشود:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});
ءPostConfigure برای Named Options نیز قابل استفاده است:
برای post-config کردن تمام نمونههای تنظیمات از PostConfigureAll استفاده میشود:
برای دسترسی به <IOptions<TOptions یا <IOptionsMonitor<TOptions در Program.cs، باید از GetRequiredService روی WebApplication.Services استفاده کنید:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
myOptions.Name = "post_configured_name_value";
myOptions.Model = "post_configured_model_value";
});
var app = builder.Build();
برای post-config کردن تمام نمونههای تنظیمات از PostConfigureAll استفاده میشود:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});
Access options in Program.cs 🔍
برای دسترسی به <IOptions<TOptions یا <IOptionsMonitor<TOptions در Program.cs، باید از GetRequiredService روی WebApplication.Services استفاده کنید:
var app = builder.Build();
var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
.CurrentValue.Option1;
🔖هشتگها:
#aspnetcore #dotnet #csharp #optionspattern #configuration #softwarearchitecture #cleanarchitecture
Forwarded from tech-afternoon (Amin Mesbahi)
- ساختار و سازماندهی تولید چیه؟
ساختار و سازماندهی تولید نرمافزار رو من ذیل ۳ مولفه اصلی بیان میکنم. یعنی فرهنگ، فرایند، و ابزار:
۱: ابزار:
ابزار را شاید بشه سادهترین مولفه از بین ۳ عامل دونست (هرچند خیلیها همین رو هم به درستی ندارن). ابزار یعنی هر سرویس سختافزاری و نرمافزاری که به شما کمک میکنه تا چرخه عمر نرمافزار رو بهتر مدیریت و تجربه کنید. مثلا:
پلتفرم مناسب برای مستندسازی که جستجوی خوب، نسخهبندی، امکانات امنیتی، کامپلاینس، پشتیبانی از نمودار (بهصورت کد، نه صرفاً تصویر)، و کوئری پویا از سورسکد یا سیستم مدیریت پروژه رو فراهم کنه.
یا سرور/سرویس Version Control و CI/CD درست و حسابی (منظورم TFS 2012 اونم به صورت شلخته نیست)
یا ابزارهای مدرن نظارت (منظورم observability است نه monitoring)
یا سرویس Secret Manager (منظورم KeePass نیست طبیعتا)
یا...
۲: فرایند:
از ابتدای چرخه، چه توسط تیم بومی، چه توسط پیمانکار، و چه بهصورت خریداری محصول؛ باید فرایند داشت. فرایند برای استفاده از ابزارها، برای مستندسازی، برای نگهداشت اطلاعات محرمانه، برای نظارت و بازبینی روالها، حتی برای اینکه چجوری و با چه تناوبی تغییر ورژن کتابخونهها و ابزارها، آسیبپذیریها، تغییر لایسنسها و غیره رو مطلع شیم و در موردشون تصمیم بگیریم. یا اینکه طی چه فرایندی و توسط کی، تضمین کنیم که فرایندها در طول زمان، فراموش یا ناکارآمد نشن. اینها همه و همه فرایندهایی هستن که تدوین و جا انداختنشون هزینه و زمان نیاز داره.
همچنین دانش مورد نیاز برای تدوینشون بارها پیچیدهتر از یاد گرفتن سینتکس فلان زبانه و گاها گرونتر! چون باید متناسب با بضاعت و استعداد سازمان انجام بشه؛ چون گاها حرف و عمل مدیران و کارشناسان سازمانها در التزام به فرایندهای جدید، یکی نیست و یا مقاومتهای مرئی و نامرئی زیادی تجربه خواهیم کرد.
۳: فرهنگ:
ابزار و فرایند، قابل خریداری و استقرار هستن؛ اما موفقیت اونها منوط به پذیرش و التزام عملی تیم است. تجربه نشون میده که بدون فرهنگ مناسب، بهترین ابزارها هم در عمل نادیده گرفته میشن؛ و بهترین فرایندها به حاشیه رانده میرن.
فرهنگسازی، حتی از تدوین و استقرار فرایند هم سختتره! فرهنگ نیروی انسانی و فرهنگ سازمانی چیزی نیست که یک شبه به دست بیاد، محصول قابل خریداری و استقرار سریع نیست!
فرق سازمان با جامعه اینه که شما در جامعه وقت، زیاد داری! بین رنسانس تا انقلاب صنعتی چند قرن زمان برد، ولی یک سازمان چند قرن یا چند سال زمان نداره! حساب دو دو تا چهار تا است؛ دیر بجنبی زیانده و ورشکسته میشی. حتی اگر دولتی باشی، به خودت میای و میبینی ۲,۲۴۵,۶۲۳ نفر کارمند داری که راهی جز تعدیل و جایگزینی درصد بسیار بالایی از اونها نداری... و دلیل اصلیش: «فرهنگ نیروی انسانی» است
همونطور که در ادبیات Enterprise Architecture اومده "Culture Ensures Sustainability" فرهنگ، پایداری رو تضمین میکنه. حالا فرهنگ مهندسی در اینجا به معنی ترکیب سه عامل است:
۱. شایستگی فنی (Technical Competence):
تسلط بر معماری، الگوهای طراحی، و...
۲. تعهد به فرایند (Process Commitment):
پایبندی به code review، testing، و documentation
۳. مهارت در ابزار (Tool Proficiency):
استفاده مؤثر از CI/CD، و observability tools
بعضی گزارشهای McKinsey میگن حدود ۷۰٪ پروژههای تحول دیجیتال، به دلایل فرهنگی و سازمانی شکست میخورن. تجربه شخصی من در ایران این عدد رو حتی بالاتر، و حدود۹۰٪ نشون میده.
تغییر فرهنگ، کندترین و حیاتیترین بخش transformation است و نیازمند ۳-۵ سال زمان، تعهد مدیریت ارشد، و گاهی جایگزینی تدریجی ۲۰-۴۰٪ نیروی انسانی است.
به فرض، اگر فردا تحریم برداشته بشه، فلان شرکتِ استارتاپی دیروز که امروز نسبتا بالغ شده، شاید خیلی زود بتونه از سرویسهای متنوع کلادی بهره بگیره و پرسنلش کاراتر عمل کنن. ولی فلان سازمان دولتی تا سالها درگیر انواع مقاومتها و مباحثات زمانبر درباره تغییرات هرچند ساده است. پس ابزار، مولفهی سادهتر، فرایند مولفهی دشوار، و فرهنگ رو میتونیم مولفهی حیاتی و بسیار بسیار دشوار برشمریم!
چکیده:
- Tools Provide Acceleration, Not Discipline
- Processes Create Predictability
- Culture Ensures Sustainability
اگر از این مطلب استقبال بشه، در ادامه، موضوعات زیر رو هر کدوم ذیل ۳ مولفهای که در این مقدمه عرض کردم در یک نرمافزار انترپرایز بررسی میکنم:
بخش دوم: الزامات توسعه و نگهداری محصول
بخش سوم: الزامات زیرساخت
بخش چهارم: الزامات امنیت
بخش پنجم: الزامات طراحی و نگهداشت محصول
Please open Telegram to view this post
VIEW IN TELEGRAM
بهبود کیفیت کد در #C با Static Code Analysis 🔍✨
نوشتن کد خوب برای هر پروژه نرمافزاری مهم است. این موضوعی است که من نیز عمیقاً به آن اهمیت میدهم. با این حال، تشخیص مشکلات تنها با خواندن تمام کد میتواند دشوار باشد.
خوشبختانه ابزاری وجود دارد که میتواند کمک کند: static code analysis.
این ابزار مثل یک جفت چشم اضافی است که بهطور خودکار کد شما را بررسی میکند. Static code analysis کمک میکند کدی ایمن، قابل نگهداری و باکیفیت در #C بسازید. 👨💻⚙️
در این مقاله قرار است به موارد زیر بپردازیم:
• Static code analysis
• Static analysis در .NET
• یافتن ریسکهای امنیتی
بیایید ببینیم static code analysis چطور میتواند به بهبود کیفیت کد کمک کند. 🚀
Static Code Analysis چیست؟ 🧠
ءStatic code analysis روشی برای بررسی کد بدون اجرای آن است. این روش هرگونه مشکل مرتبط با امنیت، عملکرد، سبک کدنویسی یا بهترین شیوهها را گزارش میکند.
با static code analysis میتوانید "shift left" انجام دهید؛ یعنی مشکلات را در مراحل اولیه توسعه پیدا و برطرف کنید، زمانی که رفع آنها کمهزینهتر است. 🕓➡️🛠
با نوشتن کد باکیفیت، میتوانید سیستمهایی بسازید که قابلاعتمادتر، مقیاسپذیرتر و در طول زمان آسانتر برای نگهداری باشند. سرمایهگذاری روی کیفیت کد در مراحل بعدی پروژه نتیجه خواهد داد. 💡📈
میتوانید static code analysis را داخل CI pipeline یکپارچه کنید تا یک بازخورد سریع دریافت کنید. همچنین میتوان آن را با آزمونهای معماری (Architecture Testing) ترکیب کرد تا استانداردهای کدنویسی بیشتری اعمال شود. 🧪🏗
Static Code Analysis در .NET 🔍⚙️
ءNET. دارای Roslyn analyzerهای داخلی است که کد #C شما را برای مشکلات مربوط به سبک کدنویسی و کیفیت بررسی میکنند. اگر پروژه شما NET 5. یا بالاتر را هدف قرار دهد، code analysis بهصورت پیشفرض فعال است.
بهترین روشی که من برای پیکربندی static code analysis پیدا کردهام، استفاده از Directory.Build.props است. این یک فایل XML است که در آن میتوانید ویژگیهای مشترک پروژهها را پیکربندی کنید. میتوانید فایل Directory.Build.props را در پوشه ریشه قرار دهید تا برای تمام پروژهها اعمال شود. 📁✨
میتوانید TargetFramework، ImplicitUsings، Nullable (nullable reference types)، و غیره را پیکربندی کنید. اما آنچه برای ما مهم است، پیکربندی static code analysis است.
در ادامه برخی ویژگیهایی که میتوانیم پیکربندی کنیم آورده شده است:
• TreatWarningsAsErrors –
همه warningها را به error تبدیل میکند.
• CodeAnalysisTreatWarningsAsErrors–
هشدارهای کیفیت کد (CAxxxx) را به error تبدیل میکند.
• EnforceCodeStyleInBuild –
قوانین مربوط به تحلیل سبک کد ("IDExxxx") را فعال میکند.
• AnalysisLevel –
مشخص میکند کدام analyzerها فعال شوند. مقدار پیشفرض latest است.
• AnalysisMode –
پیکربندی تحلیل کد پیشفرض را تنظیم میکند.
همچنین میتوانیم بستههای NuGet اضافی را در پروژهها نصب کنیم. SonarAnalyzer.CSharp مجموعهای از analyzerهای اضافی دارد که به ما کمک میکند کدی تمیز، ایمن و قابلاعتماد بنویسیم. این کتابخانه توسط همان شرکتی توسعه یافته که SonarQube را ساخته است. 🧪🔒
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Configure code analysis. -->
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>All</AnalysisMode>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup Condition="'$(MSBuildProjectExtension)' != '.dcproj'">
<PackageReference Include="SonarAnalyzer.CSharp" Version="*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>
runtime; build; native; contentfiles; analyzers; buildtransitive
</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
ءAnalyzerهای داخلی NET. و analyzerهای موجود در SonarAnalyzer.CSharp میتوانند بسیار مفید باشند. اما گاهی اوقات ممکن است با تعداد زیادی هشدار در هنگام build باعث ایجاد سروصدا شوند. ⚠️📢
وقتی با قوانین تحلیل کدی مواجه میشوید که از نظر شما مفید نیستند، میتوانید آنها را غیرفعال کنید. میتوانید قوانین تحلیل کد را به صورت جداگانه در فایل editorconfig. پیکربندی کنید.
# S125: Sections of code should not be commented out
dotnet_diagnostic.S125.severity = none
# S1075: URIs should not be hardcoded
dotnet_diagnostic.S1075.severity = none
# S2094: Classes should not be empty
dotnet_diagnostic.S2094.severity = none
# S3267: Loops should be simplified with "LINQ" expressions
dotnet_diagnostic.S3267.severity = none
یافتن (و رفع کردن) ریسکهای امنیتی 🔍🛡
ءStatic code analysis میتواند به شما کمک کند آسیبپذیریهای امنیتی احتمالی را در کدتان شناسایی کنید.
در این مثال، یک PasswordHasher فقط از 10,000 iterations برای تولید یک password hash استفاده میکند.
قانون S5344 از SonarAnalyzer.CSharp این مشکل را شناسایی کرده و به شما هشدار میدهد. حداقل تعداد iterations پیشنهادی 100,000 است. ⚠️
میتوانید به توضیحات قانون S5344 مراجعه کنید تا بیشتر یاد بگیرید:
ذخیرهسازی رمز عبور با hashing ضعیف یک ریسک امنیتی جدی برای اپلیکیشنها است. 🔐❗️
وقتی TreatWarningsAsErrors فعال باشد، build شما تا زمانی که مشکل را حل نکنید fail خواهد شد.
این کار احتمال ورود ریسکهای امنیتی به محیط production را به شدت کاهش میدهد. 🚫🔓
نتیجهگیری 📌
ءStatic code analysis یک ابزار قدرتمند است که من همیشه در تمام پروژههای #C استفاده میکنم.
این ابزار کمک میکند مشکلات را زودتر پیدا کنم، کد قابلاعتمادتر و امنتری داشته باشم و در نهایت زمان و انرژی کمتری صرف کنم.
هرچند راهاندازی اولیه و تنظیم دقیق قوانین ممکن است کمی زمانبر باشد، اما مزایای بلندمدت آن غیرقابل انکار است. ⏳✔️
به خاطر داشته باشید که static code analysis یک ابزار کمکی است و جایگزین سایر فعالیتهای توسعه نمیشود.
وقتی static code analysis را با تکنیکهایی مثل code review, unit testing, و continuous integration ترکیب کنید،
میتوانید یک فرایند توسعهی قدرتمند بسازید که همیشه خروجی باکیفیت تولید کند. 💡🚀
ءStatic code analysis را جدی بگیرید.
نسخهی آیندهی شما (و تیمتان) از شما تشکر خواهد کرد. 🙌💙
آنچه بازنویسی یک پروژهی ۴۰ ساله به من دربارهی توسعهی نرمافزار آموخت 🧓💻⚙️
«وظیفهی شما این است که این سیستم را بازنویسی کنید. تمام عملیات ما روی آن اجرا میشود.
اوه، و این سیستم با APL نوشته شده.»
با این جمله، سفر من برای بازنویسی یک سیستم legacy آغاز شد. برای کسانی که با APL آشنا نیستند: این زبان برنامهنویسی مربوط به دههی ۱۹۶۰ است و بهخاطر نشانهگذاری ریاضی خاص و تواناییهای قدرتمند در array manipulation شناخته میشود. پیدا کردن توسعهدهندهای که امروز APL بداند تقریباً به سختی پیدا کردن یک floppy disk drive در یک کامپیوتر مدرن است. 🥲💾
این سیستم طی چهار دهه رشد کرده بود. ابتدا یک ابزار سادهی مدیریت موجودی بود، اما کمکم به یک سیستم ERP جامع تبدیل شد.
بیش از 460+ جدول دیتابیس. بیشمار Business rule در دل کد.Integrationهای پیچیده در هر بخش از فرآیند کسبوکار.
این سیستم ستون فقرات یک عملیات تولیدی بود که بیش از ۱۰ میلیون دلار درآمد سالانه ایجاد میکرد. 🏭💰
ماموریت ما واضح اما ترسناک بود:
مدرنسازی این سیستم با استفاده از dotNET,PostgreSQL, و React. ⚡️
اما یک نکتهی حیاتی وجود داشت:
کسبوکار باید بدون کوچکترین وقفه ادامه پیدا میکرد.
بدون downtime.
بدون data loss.
بدون اختلال در عملیات روزانه. ⛔️🕒
این فقط یک چالش فنی نبود. یک درس بزرگ بود دربارهی مدیریت پیچیدگی، فهم فرآیندهای legacy، و هدایت تعاملات سازمانی.
این داستان و درسهای آموختهشدهی آن است.
وضعیت اولیه: درک Legacy 🏛🧠
اولین چالش این بود که بفهمیم این سیستم عظیم چطور کار میکند. این codebase طی ۴۰ سال بهصورت ارگانیک رشد کرده بود و تنها توسط یک تیم توسعهی ثابت نگهداری میشد. اعضای این تیم حالا در دههی ۶۰ زندگی بودند و قصد بازنشستگی داشتند.
وقتی وارد اولین بازبینی کد شدیم، انگار یک کپسول زمان باز کرده باشیم.
سینتکس بسیار فشردهی APL باعث میشد منطق تجاری پیچیده فقط در چند خط نوشته شود.
زیبا، اگر میتوانستید آن را بخوانید.
وحشتناک، اگر نمیتوانستید.
و اکثر ما نمیتوانستیم. 😅📜
تیم اصلی در مرحلهی انتقال دانش، بیقیمت بود. آنها تمام نکتهها، edge caseها، و business ruleهایی را که طی دههها اضافه شده بود، از حفظ بودند. اما تنها بخش محدودی از این دانش را میتوان از طریق گفتگو منتقل کرد. Documentation کم بود. هر آنچه وجود داشت منسوخ شده بود.
مستندات واقعی در ذهن توسعهدهندگان اصلی بود.
ما هفتهها صرف map کردن کارکردهای سیستم کردیم:
• فرآیند اصلی تولید در بیش از 50+ جدول با وابستگیهای پیچیده پخش شده بود
• مدیریت موجودی تقریباً به همهی بخشهای دیگر سیستم وصل شده بود
• ابزارهای گزارشگیری custom طی دههها ساخته شده بودند تا نیازهای خاص را برآورده کنند
• ءIntegration با سیستمهای خارجی از طریق یک هزارتوی stored procedureها انجام میشد
• جدولهایی که با ساختارهای ساده شروع شده بودند اکنون شامل صدها ستون بودند؛ برخی ستونها دیگر استفاده نمیشدند، اما حذفشان ممکن نبود چون شاید یک گزارش نامعلوم هنوز به آنها وابسته باشد
چالش اصلیتر این بود که بین توصیف سادهی فرآیندهای کسبوکار و پیادهسازی فنی عمیقاً پیچیدهی آنها فاصلهی بزرگی وجود داشت.
کسبوکار یک workflow ساده را توضیح میداد، اما نسخهی فنی آن لایههای متعددی از edge caseها و نیازهای اضافهشده طی سالها را آشکار میکرد.
ما به یک رویکرد سیستماتیک برای فهمیدن این «هیولا» نیاز داشتیم.
شروع کردیم به map کردن business processها و پیادهسازی فنی متناظر آنها.
این کار به ما کمک کرد domainهای اصلی را شناسایی کنیم؛ domainهایی که بعدها معماری ماژولار سیستم جدید را شکل دادند.
مهمتر از آن، به ما کمک کرد مقیاس واقعی کاری که در پیش داشتیم را درک کنیم. 🧩📘
تعارض Product و Engineering ⚔️📊
ءManagement بهدنبال quick wins بود. آنها ما را تحت فشار قرار میدادند که از سادهترین کامپوننتها شروع کنیم. این موضوع میان product management و تیم توسعه تنش ایجاد کرد.
دیدگاه product management روشن بود: باید به کسبوکار پیشرفت قابلمشاهده نشان داد. آنها برای توجیه سرمایهگذاری در بازنویسی سیستم، نیاز به خروجیهای قابلنمایش داشتند. کسبوکار پول قابلتوجهی خرج میکرد و میخواست هرچه سریعتر بازدهی ببیند. 💰📈
اما تیم توسعه واقعیت متفاوتی میدید. ما میدانستیم شروع از بخشهای حاشیهای یعنی ساختوساز روی پایههای سست. هستهی منطق کسبوکار در سیستم legacy باقی میماند و این موضوع هر نقطهی integration را پیچیدهتر میکرد. این بدهی فنی در طول زمان انباشته میشد.
بهعنوان یک technical lead، من بهشدت با این رویکرد مخالفت کردم. استدلالم ساده بود: فرآیند اصلی تولید قلب کل سیستم بود. تمام قابلیتهای جانبی به آن وابسته بودند. با بهتعویقانداختن مهاجرت این core، ما یک شبکهی درهمتنیده از وابستگیها میان سیستم جدید و قدیمی ایجاد میکردیم. هر قابلیت جدیدی که مهاجرت میدادیم نیازمند یک همگامسازی پیچیده با core legacy میشد. ما داشتیم روی ماسههای روان ساختوساز میکردیم. 🏜⚠️
من توصیه کردم ابتدا روی core domain تمرکز کنیم. درست است، نمایش اولین نتایج بیشتر طول میکشید. اما یک پایهی محکم برای ادامهی کار ایجاد میشد. کسبوکار باید دیرتر منتظر «پیشرفت قابلرویت» میبود، اما کل فرآیند مهاجرت سریعتر و قابلاتکاتر انجام میشد.
هیچیک از دو طرف در اهداف خود اشتباه نمیکردند. Product management نگرانیهای موجهی دربارهی نمایش progress داشت.
تیم توسعه نگرانیهای موجهی دربارهی پایداری فنی داشت.
اما این عدمهمراستایی باعث بهوجود آمدن مصالحههایی شد که بر timeline پروژه اثر گذاشت.
تا امروز معتقدم اگر از core business logic شروع کرده بودیم، مهاجرت سریعتر تمام میشد. 🕰🔧
معماری نرمافزار: ساختن برای آینده 🏗🔮
در مرحلهی discovery، ما domainهای کسبوکاری متمایز را در سیستم شناسایی کردیم. این موضوع ما را به سمت پیادهسازی یک modular monolith هدایت کرد. هر ماژول self-contained بود اما میتوانست از طریق یک event bus مشترک با دیگر ماژولها ارتباط برقرار کند.
تصمیمات کلیدی معماری: 🧩
🔸️ءModular monolith:
هر ماژول یک business domain مستقل را نمایش میداد. این کار مسیر روشنی برای حرکت احتمالی آینده به سمت microservices ایجاد میکرد.
🔹️ءAsynchronous communication:
ماژولها از طریق eventها و با استفاده از RabbitMQ با یکدیگر ارتباط برقرار میکردند. این کار coupling را کاهش داده و resiliency را افزایش داد.
🔸️ءShared database با مرزبندی مشخص:
تمام ماژولها از یک دیتابیس PostgreSQL استفاده میکردند، اما هر ماژول جدولها و schemaهای مخصوص خود را داشت. این رویکرد جداسازی منطقی را حفظ میکرد.
🔹️ءCloud-ready design:
سیستم با استفاده از containerization روی AWS مستقر شد. یک Jenkins pipeline امکان deployment به چند environment را در چند دقیقه فراهم میکرد. ☁️🚀
چالش Data Sync 🔄💾
پیادهسازی همگامسازی دوطرفهی داده بسیار پیچیدهتر از چیزی بود که در ابتدا تصور میکردیم. دلیل اینکه نتوانستیم از راهکارهای موجودِ Change Data Capture (CDC) مانند Debezium استفاده کنیم اینها بود:
1️⃣ تبدیلهای پیچیده:
بسیاری از جدولهای legacy برای ساخت رکورد جدید نیازمند داده از چندین جدول در سیستم جدید بودند. این یک mapping سادهی یکبهیک —که ابزارهای CDC در آن عالی هستند— نبود. 🧩
2️⃣ اعمال Business Logic در فرآیند Sync:
فرآیند sync باید business ruleهای متعددی را در مرحلهی transformation اعمال میکرد. این فراتر از قابلیتهای ابزارهای replication معمول بود. ⚙️
3️⃣ نیازمندیهای دوطرفه (Bidirectional):
باید داده را در هر دو جهت همگام میکردیم، بدون آنکه دچار loop بینهایت شویم. سیستم legacy تا زمانی که برخی کامپوننتها مهاجرت نکرده بودند، همچنان source of truth باقی میماند. 🔁❗️
ما یک راهکار custom با استفاده از RabbitMQ برای انتقال پیام ساختیم. این راهکار برای نیازهای ما مناسب بود؛ اما درس بزرگ این است:
پیش از ساخت هر راهکار سفارشی، ابزارهای موجود را بهطور کامل ارزیابی کنید.
حتی اگر نتوانید آنها را تماموکمال استفاده کنید، از معماریها و الگوهای آنها چیزهای ارزشمندی خواهید آموخت. 🛠📘