Post #734
279
Channel Public Channel
C# C# Geeks (.NET)
@csharpgeeks
- Subscribers
- 550
- Photos
- 157
- Videos
- 4
- Links
- 177
Showing posts older than #735 · Back to latest
Older Posts 20 shown
Post #733
317
📊 نتیجه نهایی
وقتی یک تراکنش Fail میشود...
دیگر لازم نیست:
❌ وارد لاگ Gateway شوید.
❌ بعد Payment Service.
❌ بعد Background Job.
❌ بعد RabbitMQ.
❌ بعد Notification Service.
فقط کافی است در Seq این شناسه را جستجو کنید:
a17d59d8-78d2-4b1f-bd89-40d3f52b44f3
و کل مسیر درخواست را ببینید.
از اولین HTTP Request...
تا آخرین Consumer...
کاملاً مرتب و به ترتیب زمانی.
🔭 اگر بخواهیم یک قدم جلوتر برویم...
ءCorrelation ID فقط برای لاگها نیست.
اگر از OpenTelemetry استفاده کنید...
همین شناسه تبدیل به یک Distributed Trace میشود.
در آن صورت میتوانید ببینید:
⏱️ هر سرویس چقدر زمان صرف کرده است.
📡 کدام درخواست به کدام سرویس ارسال شده است.
🐢 گلوگاه سیستم دقیقاً کجاست.
💥 کدام Span با خطا مواجه شده است.
یعنی علاوه بر مشاهده لاگها، کل سفر یک درخواست را بهصورت تصویری دنبال میکنید.
💡 مهمترین نکته
در سیستمهای Monolith معمولاً Debug کردن فقط با یک فایل Log انجام میشود.
اما در معماریهای Microservices یا حتی Modular Monolith که از Message Broker، Outbox Pattern و Background Processing استفاده میکنند، بدون Correlation ID عملاً هر سرویس جزیرهای جداگانه است.
ءCentralized Logging، Correlation ID و OpenTelemetry فقط ابزارهای لاگگیری نیستند؛ آنها ستون فقرات Observability سیستم هستند. هرچه سیستم بزرگتر و توزیعشدهتر شود، ارزش این سه ابزار بیشتر نمایان میشود، چون در زمان بروز Incident، تفاوت بین چند دقیقه و چند ساعت عیبیابی را رقم میزنند.
🔖 هشتگها:
#aspnetcore #microservices #serilog #opentelemetry #observability #logging
Post #732
248
🔍 وقتی یک تراکنش وسط مسیر Fail میشود، واقعاً از کجا شروع به Debug میکنید؟فرض کنید یک کاربر روی دکمه پرداخت کلیک میکند. چند ثانیه بعد...
❌ پرداخت ناموفق میشود.
حالا سؤال اینجاست:
مشکل دقیقاً کجا رخ داده است؟
آیا API Gateway درخواست را دریافت نکرده؟
آیا Validation در Command Handler شکست خورده؟
آیا داده داخل Database ذخیره نشده؟
آیا Outbox پردازش نشده؟
آیا پیام وارد RabbitMQ نشده؟
یا Consumer پیام را دریافت کرده ولی پردازش آن Fail شده؟
اگر هر سرویس فقط لاگ خودش را داشته باشد...
عملاً وارد یک کابوس خواهید شد.
😵 سناریوی رایج
فرض کنید سیستم شما از این بخشها تشکیل شده است:
Client
│
▼
API Gateway (Ocelot)
│
▼
Payment Service
│
▼
Command Handler
│
▼
Database + Outbox
│
▼
Background Job
│
▼
RabbitMQ
│
▼
Notification Service
حالا برای پیدا کردن علت خطا باید:
🔹 لاگ Gateway را باز کنید.
🔹 بعد لاگ Payment Service را بررسی کنید.
🔹 بعد لاگ Background Service را.
🔹 بعد لاگ RabbitMQ Consumer را.
🔹 بعد لاگ Notification Service را.
هر کدام روی یک Instance جدا... با Timestampهای مختلف... و فقط امیدوار باشید که ساعت همه سرورها Sync باشد.
📌راهحل؟🎯 تجربه واقعی در Hub پرداخت Dr. Link
ءCentralized Logging + Correlation ID + OpenTelemetry
در هاب پرداخت Dr. Link تعداد زیادی سرویس مستقل وجود دارد.
پرداخت فقط یک درخواست HTTP نیست.
بعد از ثبت درخواست، چندین اتفاق پشت سر هم رخ میدهد:
✅ ذخیره اطلاعات
✅ اجرای Business Ruleها
✅ ثبت Domain Event
✅ ذخیره در Outbox
✅ ءPublish شدن پیام توسط MassTransit
✅ پردازش توسط سرویسهای دیگر
اگر Correlation نداشته باشیم...
ردیابی مسیر تقریباً غیرممکن میشود.
🆔 همه چیز از Correlation ID شروع میشود
به محض اینکه درخواست وارد Ocelot API Gateway میشود...
یک Correlation ID تولید میشود.
مثلاً:
a17d59d8-78d2-4b1f-bd89-40d3f52b44f3
این شناسه همراه درخواست به تمام سرویسها ارسال میشود.
X-Correlation-Id:
a17d59d8-78d2-4b1f-bd89-40d3f52b44f3
از این لحظه...
تمام لاگهای مربوط به این درخواست همین شناسه را خواهند داشت.
🌐 اما داستان فقط HTTP نیست...
قسمت سخت ماجرا زمانی شروع میشود که سیستم وارد دنیای Asynchronous Messaging میشود.
در همین نقطه رد درخواست گم میشود.
📨 ءCorrelation ID داخل پیام هم قرار میگیرد
هنگام Publish کردن پیام توسط MassTransit همان Correlation ID داخل Envelope پیام ذخیره میشود.
یعنی پیام چیزی شبیه این خواهد داشت:
CorrelationId
a17d59d8-78d2-4b1f-bd89-40d3f52b44f3
در نتیجه...
وقتی Consumer پیام را دریافت میکند...
همان شناسه هنوز همراه پیام است.
هیچ بخشی از مسیر گم نمیشود.
📝 ءSerilog هم به صورت خودکار این شناسه را وارد Logها میکند
به کمک یک Serilog Enricher
ءCorrelation ID مستقیماً از:
✅ HTTP Header
یا
✅ MassTransit Message
خوانده میشود.
در نتیجه دیگر لازم نیست بنویسیم:
Log.Information(
"Payment {CorrelationId}",
correlationId);
تمام Logها به صورت خودکار این مقدار را خواهند داشت.
⚙️ نقش MediatR Pipeline Behavior
قبلاً برای Cross-Cutting Concernها از Pipeline Behavior استفاده کرده بودیم.
حالا همان Pipeline یک مسئولیت جدید هم دارد.
در ابتدای اجرای هر Command:
یک Logging Scope باز میکند.
BeginScope()
CorrelationId
از این لحظه...
هر لاگی که داخل:
Repository
Domain Service
Application Service
EF Core
یا هر بخش دیگری ثبت شود...
به صورت خودکار همین Correlation ID را خواهد داشت.
بدون اینکه حتی یک بار آن را دستی پاس بدهیم.
Post #731
273
#فرمون_دادن
بعضی وقتها فکر میکنیم ارزش یک مهندس را تعداد تصمیمهایی که میگیرد مشخص میکند.
اما واقعیت این است که...
خیلی از مهندسان باتجربه، بیشتر از اینکه تصمیم بگیرند،
تصمیم نگرفتن را بلدند.
آنها هر ایدهای را تبدیل به Feature نمیکنند.
هر تکنولوژی جدیدی را وارد پروژه نمیکنند.
هر مشکلی را هم فوراً به Refactoring ربط نمیدهند.
چون یک چیز را خوب میدانند.
هر تصمیمی که امروز میگیری،
از فردا به بعد باید هزینه نگهداریاش را هم پرداخت کنی.
هر کلاس جدید...
هر سرویس جدید...
هر Dependency جدید...
هر Feature جدید...
تا روزی که پروژه زنده است، از تو هزینه میگیرد.
به همین دلیل، بلوغ مهندسی فقط در این نیست که بدانی چه چیزی را اضافه کنی.
بلوغ واقعی این است که بدانی به چه چیزی باید «نه» بگویی.
چون در نهایت،
پروژهها بیشتر از اینکه زیر بار کمبود قابلیتها خم شوند،
زیر بار تصمیمهایی خم میشوند که هیچوقت لازم نبود گرفته شوند.
بعضی وقتها فکر میکنیم ارزش یک مهندس را تعداد تصمیمهایی که میگیرد مشخص میکند.
اما واقعیت این است که...
خیلی از مهندسان باتجربه، بیشتر از اینکه تصمیم بگیرند،
تصمیم نگرفتن را بلدند.
آنها هر ایدهای را تبدیل به Feature نمیکنند.
هر تکنولوژی جدیدی را وارد پروژه نمیکنند.
هر مشکلی را هم فوراً به Refactoring ربط نمیدهند.
چون یک چیز را خوب میدانند.
هر تصمیمی که امروز میگیری،
از فردا به بعد باید هزینه نگهداریاش را هم پرداخت کنی.
هر کلاس جدید...
هر سرویس جدید...
هر Dependency جدید...
هر Feature جدید...
تا روزی که پروژه زنده است، از تو هزینه میگیرد.
به همین دلیل، بلوغ مهندسی فقط در این نیست که بدانی چه چیزی را اضافه کنی.
بلوغ واقعی این است که بدانی به چه چیزی باید «نه» بگویی.
چون در نهایت،
پروژهها بیشتر از اینکه زیر بار کمبود قابلیتها خم شوند،
زیر بار تصمیمهایی خم میشوند که هیچوقت لازم نبود گرفته شوند.
Post #730
265
Post #729
302
🖼 چرا شرکتهای بزرگ یک Media Service جداگانه میسازند؟
اوایل پروژه همه چیز ساده است.
کاربر یک تصویر آپلود میکند، فایل داخل wwwroot ذخیره میشود و مسیر آن داخل دیتابیس قرار میگیرد.
اما وقتی سیستم بزرگتر میشود، ناگهان هر سرویس شروع میکند به مدیریت فایلها، آپلود تصاویر، Resize کردن، ساخت Thumbnail، حذف فایلها، CDN، Storage و دهها مسئولیت دیگر.
همینجاست که مفهوم Media Service وارد میشود.
به جای اینکه هر سرویس خودش مسئول مدیریت فایلها باشد، یک سرویس مرکزی فقط روی مدیریت Media تمرکز میکند.
🏗 معماری با Media Service
Upload File
↓
Media Service
↓
Object Storage (S3, MinIO)
↓
CDN / Image Cache
┌─────────┼─────────┐
↓ ↓ ↓
Service A Service B Service C
تمام سرویسها فقط از Media Service درخواست Upload یا Download میکنند و دیگر درگیر جزئیات ذخیرهسازی فایل نیستند.
✅ مزایا
1️⃣ جداسازی مسئولیتها (Single Responsibility)
ءBusiness Serviceها فقط روی منطق کسبوکار تمرکز میکنند. Media Service فقط فایل مدیریت میکند.
2️⃣ یکپارچگی Storage
تمام فایلها در یک محل ذخیره میشوند.
فرقی ندارد از:
• Local Storage
• MinIO
• Amazon S3
• Azure Blob Storage
• Google Cloud Storage
استفاده کنید.
تمام سرویسها فقط با Media Service صحبت میکنند.
3️⃣ حذف کدهای تکراری
دیگر لازم نیست هر سرویس جداگانه پیادهسازی کند:
• Upload Endpoint
• Delete File
• Resize Image
• Compress Image
• Generate Thumbnail
• Virus Scan
4️⃣ توسعهپذیری بیشتر
اگر امروز از Local Storage استفاده میکنید و فردا بخواهید به S3 مهاجرت کنید،
فقط Media Service تغییر میکند.
هیچ Business Serviceای نیاز به تغییر ندارد.
5️⃣ مدیریت سادهتر فایلها
تمام عملیات مربوط به فایل در یک نقطه انجام میشود:
• Versioning
• Metadata
• Tags
• File Ownership
• Expiration
• Access Policy
6️⃣ ءPerformance بهتر
ءMedia Service میتواند:
• Image Resize
• Compression
• Lazy Loading
• CDN Integration
• Cache
را به صورت متمرکز مدیریت کند.
❌ معایب
1️⃣ Single Point Of Failure
اگر Media Service از دسترس خارج شود،
آپلود و دانلود فایلها نیز متوقف میشود.
2️⃣ پیچیدگی بیشتر
دیگر فقط ذخیره فایل مطرح نیست.
موضوعاتی مانند:
• CDN
• Object Storage
• Streaming
• Chunk Upload
• Signed URL
• Image Processing
نیز وارد سیستم میشوند.
3️⃣ هزینه بیشتر
نگهداری Storage، CDN و Backup هزینه دارد.
4️⃣ مدیریت امنیت
دسترسی مستقیم کاربران به فایلها باید کنترل شود.خصوصاً برای فایلهای Private.
📋 نکاتی که حتماً باید رعایت شوند
🔸 فایلها داخل Database ذخیره نشوند (بهجز موارد خاص)
🔸 از Object Storage استفاده شود.
🔸 فایلهای Private با Signed URL ارائه شوند.
🔸 محدودیت حجم فایل اعمال شود.
🔸 ءMIME Type اعتبارسنجی شود.
🔸 نام فایلها به GUID یا Hash تبدیل شوند.
🔸 ءThumbnailها به صورت Async تولید شوند.
🔸 فایلهای بلااستفاده پاکسازی شوند.
🔸 از CDN برای فایلهای عمومی استفاده شود.
🔸 تمام عملیات Upload و Delete لاگ شوند.
⭐️ برای بهتر شدن Media Service چه کارهایی انجام دهیم؟
✅ Chunk Upload برای فایلهای بزرگ
✅ Resume Upload
✅ Background Image Processing
✅ Automatic Compression
✅ Automatic Thumbnail Generation
✅ Video Transcoding
✅ Signed URL
✅ CDN Integration
✅ Metadata Search
✅ Duplicate Detection (Hash)
✅ Virus Scanning
✅ Versioning
✅ Lifecycle Management
✅ Backup & Replication
💡 مهمترین نکته
ءMedia Service فقط یک سرویس Upload فایل نیست.
در سیستمهای بزرگ، این سرویس مسئول مدیریت کل چرخهی عمر فایلها است؛ از لحظهی آپلود تا حذف، آرشیو، Cache، امنیت، پردازش تصویر و توزیع از طریق CDN.
هرچه زودتر مدیریت فایلها را از Business Serviceها جدا کنید، توسعه سیستم سادهتر، مقیاسپذیری بیشتر و نگهداری زیرساخت بسیار آسانتر خواهد شد.
🔖 هشتگها:
#mediaservice #objectstorage #minio #amazons3
Post #727
274
🚨 قابلیت جدید field در C# 14 همیشه هم بیخطر نیست!یکی از جذابترین قابلیتهای C# 14 معرفی کلیدواژه
field است.به کمک آن دیگر لازم نیست برای هر Property یک Backing Field جداگانه تعریف کنید.
قبلاً مینوشتیم:
private decimal _price;
public decimal Price
{
get => _price;
set => _price = value;
}
حالا میتوانیم بنویسیم:
public decimal Price
{
get;
set => field = value;
}
کد تمیزتر، خواناتر و Boilerplate کمتر...
اما یک نکته مهم وجود دارد که خیلیها به آن توجه نمیکنند. 👇
⚠️ مشکل از کجا شروع میشود؟
وقتی از
field استفاده میکنید، دیگر فیلدی به نام _price وجود ندارد.کامپایلر خودش یک Backing Field مخفی ایجاد میکند.
چیزی شبیه:
<Price>k__BackingField
این یعنی اگر جایی از پروژه مستقیماً به
_price وابسته باشد...ممکن است برنامه بدون هیچ خطای کامپایل، در Runtime دچار مشکل شود.
🔥 یکی از رایجترین قربانیها: EF Core
فرض کنید قبلاً این Mapping را نوشتهاید:
builder.Property(x => x.Quantity)
.HasField("_quantity");
بعد تصمیم میگیرید از قابلیت جدید C# 14 استفاده کنید.
public int Quantity
{
get;
set => field = value;
}
نتیجه؟
هنگام اجرای برنامه با خطای:
InvalidOperationException
روبهرو خواهید شد.
چون دیگر فیلدی به نام
_quantity وجود ندارد.⚠️ ءReflection هم ممکن است از کار بیفتد
کدهایی مانند:
GetField("_quantity")یا
GetRuntimeField(...)
بعد از Refactor مقدار
null برمیگردانند.و معمولاً چند خط بعد...
به یک
NullReferenceException ختم میشوند.✅ قبل از استفاده از field این موارد را بررسی کنید🔹 تمام
HasField(...)های پروژه🔹 ءAttributeهای
BackingField🔹 استفاده از
Reflection🔹 ءMapperهای سفارشی
🔹 ءSerializerهایی که روی Fieldها کار میکنند
🔹 ءUnit Testهایی که به نام Field وابستهاند
💡 نکته مهم
استفاده از
field فقط یک Refactor ظاهری نیست.اگر زیرساخت پروژه به نام Backing Field وابسته باشد، در واقع دارید Contract داخلی برنامه را تغییر میدهید.
و این دسته از خطاها معمولاً نه توسط کامپایلر شناسایی میشوند و نه توسط بسیاری از Unit Testها؛ بلکه اولین بار در محیط Production خودشان را نشان میدهند.
🎯 جمعبندی
قابلیت
field در C# 14 یکی از بهترین امکانات جدید زبان است و باعث کاهش Boilerplate و خواناتر شدن کد میشود.اما قبل از مهاجرت، مطمئن شوید پروژه شما به نام Backing Fieldها وابسته نیست؛ در غیر این صورت، یک Refactor ساده میتواند به خطاهای Runtime، شکست Mappingهای EF Core و مشکلات Reflection منجر شود.
🔗Source
🔖 هشتگها:
#dotnet #csharp14 #EFCore #Reflection #Runtime #DotNetTips
Post #726
261
#فرهنگ_مهندسی (Engineering Culture)
یکی از نشانههای یک تیم مهندسی بالغ، این نیست که کمتر خطا میکند.
این است که از خطاها چیزی پنهان نمیکند.
در بعضی تیمها، وقتی Incident رخ میدهد، اولین سؤال این است:
«کارِ چه کسی بود؟»
اما در تیمهای مهندسی بالغ، سؤال فرق میکند:
«چطور ممکن است دوباره این اتفاق نیفتد؟»
این تفاوت، کوچک به نظر میرسد.
اما فرهنگ تیم را از ریشه تغییر میدهد.
وقتی افراد از سرزنش شدن بترسند،
مشکلات را دیرتر گزارش میکنند.
ریسکها را پنهان میکنند.
و گاهی ترجیح میدهند باگ را موقتاً دور بزنند تا اینکه درباره آن صحبت کنند.
اما وقتی بدانند هدف، پیدا کردن مقصر نیست،
بلکه پیدا کردن علت است،
اطلاعات سریعتر به اشتراک گذاشته میشود.
و تیم، با هر Incident قویتر از قبل میشود.
به همین دلیل است که Blameless Postmortem فقط یک جلسه بعد از Incident نیست.
یک طرز فکر است.
طرز فکری که میگوید:
«انسانها اشتباه میکنند؛ وظیفه سیستم این است که اجازه ندهد یک اشتباه کوچک، به یک فاجعه بزرگ تبدیل شود.»
فرهنگ مهندسی از جایی شروع میشود که اعضای تیم،
برای گفتن حقیقت، احساس امنیت کنند.
چون تیمی که از اشتباهاتش یاد میگیرد،
خیلی سریعتر از تیمی رشد میکند که فقط سعی میکند اشتباهاتش را پنهان کند.
یکی از نشانههای یک تیم مهندسی بالغ، این نیست که کمتر خطا میکند.
این است که از خطاها چیزی پنهان نمیکند.
در بعضی تیمها، وقتی Incident رخ میدهد، اولین سؤال این است:
«کارِ چه کسی بود؟»
اما در تیمهای مهندسی بالغ، سؤال فرق میکند:
«چطور ممکن است دوباره این اتفاق نیفتد؟»
این تفاوت، کوچک به نظر میرسد.
اما فرهنگ تیم را از ریشه تغییر میدهد.
وقتی افراد از سرزنش شدن بترسند،
مشکلات را دیرتر گزارش میکنند.
ریسکها را پنهان میکنند.
و گاهی ترجیح میدهند باگ را موقتاً دور بزنند تا اینکه درباره آن صحبت کنند.
اما وقتی بدانند هدف، پیدا کردن مقصر نیست،
بلکه پیدا کردن علت است،
اطلاعات سریعتر به اشتراک گذاشته میشود.
و تیم، با هر Incident قویتر از قبل میشود.
به همین دلیل است که Blameless Postmortem فقط یک جلسه بعد از Incident نیست.
یک طرز فکر است.
طرز فکری که میگوید:
«انسانها اشتباه میکنند؛ وظیفه سیستم این است که اجازه ندهد یک اشتباه کوچک، به یک فاجعه بزرگ تبدیل شود.»
فرهنگ مهندسی از جایی شروع میشود که اعضای تیم،
برای گفتن حقیقت، احساس امنیت کنند.
چون تیمی که از اشتباهاتش یاد میگیرد،
خیلی سریعتر از تیمی رشد میکند که فقط سعی میکند اشتباهاتش را پنهان کند.
Post #725
279
🧅 معماری Onion در برابر 🧼 معماری Clean؛ تفاوت در چیست؟
هر دو معماری حول یک ایدهی مشترک شکل گرفتهاند:
• جهت وابستگیها به سمت مرکز است.
• هستهی سیستم شامل موجودیتهای دامنه (Domain Entities) و قوانین کسبوکار (Business Rules) است.
• هستهی سیستم باید از هرگونه وابستگی به مسائل خارجی مستقل باشد.
ایدهی اصلی در اینجا کنترل Coupling (وابستگی) است.
موجودیتهای دامنه و قوانین کسبوکار در مرکز سیستم قرار دارند. آنها به هیچ چیز وابسته نیستند و این موضوع ارزشمند است، زیرا باعث میشود پایدار باقی بمانند. اجزای اطراف Domain ممکن است تغییر کنند، اما این تغییرات نباید روی موجودیتهای دامنه یا قوانین کسبوکار تأثیر بگذارند.
به خاطر داشته باشید که Coupling به میزان وابستگی متقابل بین ماژولها و کامپوننتهای یک سیستم گفته میشود.
هر دو معماری Clean و Onion تلاش میکنند به اهداف یکسانی برسند، اما با رویکردی کمی متفاوت.
در هر دو، لایهی Domain وجود دارد.
در Clean Architecture، هسته شامل Use Caseها است، در حالی که در Onion Architecture، از Domain Services و Application Services استفاده میشود.
در بیرونیترین لایه نیز دغدغههای مربوط به Infrastructure قرار میگیرند.
هر تفاوتی که میان این دو مشاهده میکنید، زمانی که نوبت به پیادهسازی آنها در یک پروژه واقعی میرسد، به سرعت کمرنگ میشود؛ که این موضوع کاملاً طبیعی است، زیرا هر دو در حال پیادهسازی مجموعهای از اصول یکسان هستند.
عامل مهم دیگری که باید به آن توجه کنید Cohesion (انسجام) است. Cohesion به میزان ارتباط و هماهنگی اجزای داخلی یک ماژول (کلاس، تابع، کامپوننت و...) برای انجام یک هدف مشخص و واحد گفته میشود.
در واقع، همین مفهوم مهمترین نیروی محرک Vertical Slice Architecture محسوب میشود.
Post #724
262
(#باور_غلط_یا_واقعیت؟)
«هرچه تعداد Design Patternهایی که در پروژه استفاده کنیم بیشتر باشد، کیفیت طراحی هم بالاتر است.»
ءDesign Patternها قرار نیست کیفیت طراحی را ایجاد کنند.آنها قرار است یک مسئله تکرارشونده را با یک راهحل شناختهشده حل کنند.
اگر مسئله وجود نداشته باشد، Pattern هم ارزشی ایجاد نمیکند.
بارها دیدهام پروژههایی که پر از Factory، Strategy، Decorator، Mediator و... هستند.
اما تغییر دادن یک قابلیت ساده در آنها، ساعتها زمان میبرد.نه به خاطر پیچیدگی مسئله...
بلکه به خاطر پیچیدگیای که خودمان ساختهایم.
از طرف دیگر، پروژههایی را هم دیدهام که شاید حتی یک Pattern را نتوان روی آنها نامگذاری کرد.
اما خوانا بودند.
تستپذیر بودند.
و تغییر دادنشان ساده بود.
نکته مهم اینجاست: Design Pattern، هدف نیست. فقط یک ابزار است.
همانطور که چکش برای هر چیزی ساخته نشده، Design Pattern هم برای هر کدی مناسب نیست.
مهندسان باتجربه معمولاً هنگام طراحی از خودشان نمیپرسند:
«کدام Pattern را اینجا استفاده کنم؟»
آنها میپرسند: «اینجا چه مشکلی دارم؟»
اگر Pattern آن مشکل را حل کند، از آن استفاده میکنند.
اگر نه، سادهترین راهحل را انتخاب میکنند.
کدی که هیچ Pattern مشهوری ندارد اما بهراحتی قابل فهم، تغییر و نگهداری است،
از کدی که فقط برای نمایش دانش الگوهای طراحی نوشته شده،ارزش مهندسی بیشتری دارد.چون در مهندسی نرمافزار،سادگی یک مزیت است؛ نه نشانه کمتجربگی.
❌ باور غلط
«هرچه تعداد Design Patternهایی که در پروژه استفاده کنیم بیشتر باشد، کیفیت طراحی هم بالاتر است.»
✅ واقعیت
ءDesign Patternها قرار نیست کیفیت طراحی را ایجاد کنند.آنها قرار است یک مسئله تکرارشونده را با یک راهحل شناختهشده حل کنند.
اگر مسئله وجود نداشته باشد، Pattern هم ارزشی ایجاد نمیکند.
بارها دیدهام پروژههایی که پر از Factory، Strategy، Decorator، Mediator و... هستند.
اما تغییر دادن یک قابلیت ساده در آنها، ساعتها زمان میبرد.نه به خاطر پیچیدگی مسئله...
بلکه به خاطر پیچیدگیای که خودمان ساختهایم.
از طرف دیگر، پروژههایی را هم دیدهام که شاید حتی یک Pattern را نتوان روی آنها نامگذاری کرد.
اما خوانا بودند.
تستپذیر بودند.
و تغییر دادنشان ساده بود.
نکته مهم اینجاست: Design Pattern، هدف نیست. فقط یک ابزار است.
همانطور که چکش برای هر چیزی ساخته نشده، Design Pattern هم برای هر کدی مناسب نیست.
مهندسان باتجربه معمولاً هنگام طراحی از خودشان نمیپرسند:
«کدام Pattern را اینجا استفاده کنم؟»
آنها میپرسند: «اینجا چه مشکلی دارم؟»
اگر Pattern آن مشکل را حل کند، از آن استفاده میکنند.
اگر نه، سادهترین راهحل را انتخاب میکنند.
💡 جمعبندی
کدی که هیچ Pattern مشهوری ندارد اما بهراحتی قابل فهم، تغییر و نگهداری است،
از کدی که فقط برای نمایش دانش الگوهای طراحی نوشته شده،ارزش مهندسی بیشتری دارد.چون در مهندسی نرمافزار،سادگی یک مزیت است؛ نه نشانه کمتجربگی.
Post #723
207
Forwarded from thisisnabi.dev [Farsi] (Nabi Karampour)

اگر قرار باشه دوباره درگیر ساختار های دست و پاگیر در فرایند توسعه نرم افزار بشیم باز رسیدن به خروجی زمانبر خواهد بود.
و این دوباره تمرکز ما رو از نیت اصلی دور میکنه.
چند روز پیش با یک پروداکت منیجر بصورت تمرینی روی یک PRD کار می کردم و با هم pair شده بودیم، یک داشبورد کامل رو با درنظر گرفتن اکثر جزئیات بیزینسی در 4.5 ساعت کار آماده کردیم.
فرایند ها بنظرم کمی به این سمت میره که بیزینس دیگه از شما نمیپزیره یک کار رو چند ماه طول بدین، باید خیلی سریع به فکر ارائه خروجی باشید.
برای همین بشخصه تمرکزم روی تغییر مدل کاری تیم و چیدن ساختاری برای رسیدن سریعتر به خروجی هست.
حتی یک موضوعی که این روزها فکرم رو درگیر کرده اینه که دیگه اینقدر راحت مثل قبل نیرو در اختیارمون قرار نمیدن،
دیدگاه رفته به سمتی که چرا یه بیزینس میلیارد دلاری توی سیلیکون ولی با 10 نفر پیش میره، شما هم برید به اون سمت، کارهای دست و پاگیرتون رو با AI پیش ببرید.
این تغییر نشونه بیکارشدن ما نیست ولی بیزینس چی براش کمتر هزینه داشته باشه میره به اون سمت، تا بوده همین بوده.
شما سعی کنید مسبب این کاهش هزینه باشید.
و این دوباره تمرکز ما رو از نیت اصلی دور میکنه.
چند روز پیش با یک پروداکت منیجر بصورت تمرینی روی یک PRD کار می کردم و با هم pair شده بودیم، یک داشبورد کامل رو با درنظر گرفتن اکثر جزئیات بیزینسی در 4.5 ساعت کار آماده کردیم.
فرایند ها بنظرم کمی به این سمت میره که بیزینس دیگه از شما نمیپزیره یک کار رو چند ماه طول بدین، باید خیلی سریع به فکر ارائه خروجی باشید.
برای همین بشخصه تمرکزم روی تغییر مدل کاری تیم و چیدن ساختاری برای رسیدن سریعتر به خروجی هست.
حتی یک موضوعی که این روزها فکرم رو درگیر کرده اینه که دیگه اینقدر راحت مثل قبل نیرو در اختیارمون قرار نمیدن،
دیدگاه رفته به سمتی که چرا یه بیزینس میلیارد دلاری توی سیلیکون ولی با 10 نفر پیش میره، شما هم برید به اون سمت، کارهای دست و پاگیرتون رو با AI پیش ببرید.
این تغییر نشونه بیکارشدن ما نیست ولی بیزینس چی براش کمتر هزینه داشته باشه میره به اون سمت، تا بوده همین بوده.
شما سعی کنید مسبب این کاهش هزینه باشید.
Post #722
337
#مهندس_فکر_کن
قسمت:6️⃣
🎯 مصاحبهکننده
فرض کن در ASP.NET Core این کد را نوشتهای:
این متد روزانه ۱ میلیون بار فراخوانی میشود.
🔸️سؤال
این کد چه مشکلی دارد؟
چرا در Production توصیه نمیشود؟
راهحل استاندارد در ASP.NET Core چیست؟
چیزی که ازت انتظار دارم
اگر فقط بگویی:
کافی نیست.
باید توضیح بدهی:
مشکل دقیق چیست؟
چرا اتفاق میافتد؟
قسمت:6️⃣
🎯 مصاحبهکننده
فرض کن در ASP.NET Core این کد را نوشتهای:
public async Task<string> GetDataAsync()
{
using var client = new HttpClient();
return await client.GetStringAsync("https://api.example.com");
}
این متد روزانه ۱ میلیون بار فراخوانی میشود.
🔸️سؤال
این کد چه مشکلی دارد؟
چرا در Production توصیه نمیشود؟
راهحل استاندارد در ASP.NET Core چیست؟
چیزی که ازت انتظار دارم
اگر فقط بگویی:
"از IHttpClientFactory استفاده میکنیم."
کافی نیست.
باید توضیح بدهی:
مشکل دقیق چیست؟
چرا اتفاق میافتد؟
Post #721
279
Post #720
335
⏰ چرا شرکتهای بزرگ یک Reminder Service جداگانه میسازند؟
اوایل پروژه همه چیز ساده به نظر میرسد.
کاربر یک Task ایجاد میکند و برای آن یک زمان یادآوری تعیین میکند.
در زمان مقرر هم همان سرویس اصلی یک Notification ارسال میکند و تمام.
اما وقتی تعداد کاربران به صدها هزار یا میلیونها نفر میرسد، این رویکرد دیگر پاسخگو نیست.
اگر میلیونها Reminder برای ساعت ۹ صبح برنامهریزی شده باشند چه اتفاقی میافتد؟
همینجاست که مفهوم Reminder Service وارد معماری سیستم میشود.
به جای اینکه هر سرویس خودش مسئول زمانبندی و ارسال Reminder باشد، یک سرویس مستقل فقط مسئول مدیریت زمان، زمانبندی و اجرای Reminderها خواهد بود.
🏗 معماری سنتی
User
│
▼
Task Service
│
├── Database
│
└── Send Reminder
هر سرویس خودش:
• زمان Reminder را ذخیره میکند.
• ءScheduler مخصوص خودش را اجرا میکند.
• ءNotification ارسال میکند.
🏗 معماری با Reminder Service
User
│
▼
Task Service
│
Save Reminder Request
│
▼
Reminder Service
│
┌───────────┴────────────┐
▼ ▼
Schedule Engine Delay Queue
│ │
└───────────┬────────────┘
▼
Notification Service
│
┌───────────┼────────────┐
▼ ▼ ▼
Email SMS Push
تمام سرویسها فقط درخواست ایجاد Reminder را ثبت میکنند.
اجرای Reminder کاملاً بر عهده Reminder Service خواهد بود.
✅ مزایا
1️⃣ جداسازی مسئولیتها (Separation of Concerns)
ءBusiness Service فقط منطق کسبوکار را مدیریت میکند.
زمانبندی و اجرای Reminderها کاملاً جدا میشود.
2️⃣ مدیریت میلیونها Reminder
ءReminder Service میتواند میلیونها Reminder را:
✅ زمانبندی کند.
✅ اولویتبندی کند.
✅ دستهبندی کند.
بدون اینکه سرویسهای اصلی تحت فشار قرار بگیرند.
3️⃣ ءRetry خودکار
اگر Notification ارسال نشود:
• ءRetry انجام میشود.
• ءExponential Backoff اعمال میشود.
• در صورت شکست نهایی وارد Dead Letter Queue میشود.
4️⃣ مقیاسپذیری مستقل
اگر تعداد Reminderها زیاد شود فقط Reminder Service Scale میشود.
نیازی نیست کل سیستم Scale شود.
5️⃣ پشتیبانی از انواع Reminder
یک سرویس میتواند انواع مختلف Reminder را مدیریت کند:
📧 Email Reminder
📱 SMS Reminder
🔔 Push Notification
📅 Calendar Reminder
💬 Slack Reminder
6️⃣ مدیریت زمان (Time Zone)
یکی از سختترین قسمتهای Reminder همین موضوع است.Reminder Service میتواند:
• Time Zone کاربران
• Daylight Saving
• Local Time
را به صورت متمرکز مدیریت کند.
7️⃣ قابلیت Delay Scheduling
مثلاً:
Send after 5 minutes
Send tomorrow at 9 AM
Send every Monday
Send after 30 days
Send every month
همه این موارد توسط Reminder Service مدیریت میشوند.
❌ معایب
1️⃣ پیچیدگی بیشتر
دیگر با یک Scheduler ساده طرف نیستید.
مباحثی مانند:
• Distributed Scheduling
• Delay Queue
• Cron Engine
• Retry
• Dead Letter Queue
و...
وارد سیستم میشوند.
2️⃣ Single Point of Failure
اگر Reminder Service از کار بیفتد،
تمام Reminderهای سیستم متوقف خواهند شد.
بنابراین باید High Availability داشته باشد.
3️⃣ مدیریت دقیق زمان
Clock Drift
Time Zone
Leap Year
Leap Second
Daylight Saving
همگی باید به درستی مدیریت شوند.
4️⃣ جلوگیری از ارسال تکراری
اگر سرویس هنگام ارسال Crash کند،
نباید Reminder دوباره برای کاربر ارسال شود.
بنابراین باید: Idempotency
و Delivery Tracking
وجود داشته باشد.
📋 نکاتی که حتماً باید رعایت شوند
🔸 استفاده از UTC برای ذخیره زمان
🔸 تبدیل Time Zone فقط هنگام نمایش
🔸 استفاده از Delay Queue
🔸 پشتیبانی از Retry
🔸 استفاده از Idempotency Key
🔸 ثبت کامل Audit Log
🔸 پشتیبانی از Cron Expression
🔸 مانیتورینگ Queueها
🔸 قابلیت Cancel کردن Reminder
🔸 قابلیت Reschedule
⭐️ برای بهتر شدن Reminder Service چه کارهایی انجام دهیم؟
✅ Quartz.NET یا Hangfire برای Job Scheduling
✅ RabbitMQ Delay Queue یا Kafka Delay Topic
✅ Outbox Pattern
✅ Distributed Lock
✅ High Availability
✅ Retry Policy
✅ Dead Letter Queue
✅ Monitoring و Alerting
✅ Metrics با OpenTelemetry
✅ Sharding برای Reminderهای حجیم
Post #719
366

Post #718
417
C# Geeks (.NET) #مهندس_فکر_کن قسمت:4️⃣ 🎯مصاحبهکننده: ءCache Stampede چیست؟ فرض کن یک کلید در Cache داری: Product:123 و TTL آن 5 دقیقه است. در لحظهای که این کلید منقضی میشود، همزمان 5000 درخواست برای همان محصول میآید. اتفاقی که میافتد: Client 1 ----\ Client 2 -----\….NET Tips Cache Stampede: وقتی کش به جای محافظت، به دشمن سیستم تبدیل میشود کش (Cache) یکی از رایجترین ابزارهایی است که توسعهدهندگان داتنت برای بهبود کارایی سرویسها به کار میگیرند. ایده پشت آن سادهاست: نتیجه یک عملیات پرهزینه — مثل یک کوئری سنگین تجمیعی (Aggregation Query) یا فراخوانی یک سرویس بیرونی — را برای مدتی نگه میداریم…
Post #717
410
Post #716
432
#تحلیل_و_طرز_تفکر (Engineering Mindset)
یکی از عادتهایی که سعی کردم در خودم از بین ببرم، این بود که هر وقت یک راهحل خوب پیدا میکنم، سریع عاشقش نشوم.
چون بهترین راهحل امروز، ممکن است بدترین تصمیم شش ماه بعد باشد.
بارها دیدهام تیمی که با اطمینان از یک معماری دفاع میکرد، چند ماه بعد برای حذف همان معماری جلسه گذاشت.
نه چون اشتباه بود.
چون شرایط عوض شده بود.
تعداد کاربران تغییر کرده بود.
نیازهای بیزینس تغییر کرده بود.
تیم بزرگتر شده بود.
یا محدودیتهای جدیدی به پروژه اضافه شده بود.
این تجربه یک چیز را به من یاد داد.
در مهندسی نرمافزار، خیلی از تصمیمها «درست» یا «غلط» نیستند.آنها وابسته به شرایط هستند.
به همین دلیل، هر وقت از یک تصمیم دفاع میکنم، سعی میکنم به جای گفتن: «این بهترین راهحل است.» بگویم:
«با دانشی که امروز داریم و محدودیتهای فعلی، این بهترین انتخاب است.»
این جمله یک تفاوت مهم ایجاد میکند.
چون اجازه میدهد اگر فردا واقعیت تغییر کرد، تصمیممان را هم تغییر دهیم.
بدون اینکه احساس کنیم شکست خوردهایم.
در مهندسی نرمافزار، بلوغ یعنی به تصمیمهایت متعهد باشی... اما به آنها وابسته نشوی.
یکی از عادتهایی که سعی کردم در خودم از بین ببرم، این بود که هر وقت یک راهحل خوب پیدا میکنم، سریع عاشقش نشوم.
چون بهترین راهحل امروز، ممکن است بدترین تصمیم شش ماه بعد باشد.
بارها دیدهام تیمی که با اطمینان از یک معماری دفاع میکرد، چند ماه بعد برای حذف همان معماری جلسه گذاشت.
نه چون اشتباه بود.
چون شرایط عوض شده بود.
تعداد کاربران تغییر کرده بود.
نیازهای بیزینس تغییر کرده بود.
تیم بزرگتر شده بود.
یا محدودیتهای جدیدی به پروژه اضافه شده بود.
این تجربه یک چیز را به من یاد داد.
در مهندسی نرمافزار، خیلی از تصمیمها «درست» یا «غلط» نیستند.آنها وابسته به شرایط هستند.
به همین دلیل، هر وقت از یک تصمیم دفاع میکنم، سعی میکنم به جای گفتن: «این بهترین راهحل است.» بگویم:
«با دانشی که امروز داریم و محدودیتهای فعلی، این بهترین انتخاب است.»
این جمله یک تفاوت مهم ایجاد میکند.
چون اجازه میدهد اگر فردا واقعیت تغییر کرد، تصمیممان را هم تغییر دهیم.
بدون اینکه احساس کنیم شکست خوردهایم.
در مهندسی نرمافزار، بلوغ یعنی به تصمیمهایت متعهد باشی... اما به آنها وابسته نشوی.
Post #715
361
🧵 یکی از چالشهای همیشگی و آزاردهنده برای توسعهدهندگان در هنگام پردازش متون، مدیریت کاراکترهای پایان خط یا همان خط جدید (Newline) بوده است.😓 تفاوت در نحوه ذخیرهسازی خطوط جدید در سیستمعاملهای مختلف و همچنین استانداردهای گوناگون انکودینگ، همواره باعث بروز باگهای پنهان در پردازش متن و عبارات باقاعده (Regex) میشود.
🎉 خوشبختانه مایکروسافت در NET 11. با معرفی یک قابلیت جدید و کاربردی به نام
RegexOptions.AnyNewLine، این گره کور را باز کردهاست.در این مقاله، به بررسی این چالش قدیمی، مشکلات راهکارهای سنتی و نحوه حل اصولی آن در داتنت ۱۱ میپردازیم.
📌 چالش تاریخی: تنوع استانداردهای خط جدید
از گذشته تا به امروز، سیستمعاملهای مختلف روشهای متفاوتی برای تعریف پایان یک خط داشتهاند.
به طور سنتی، سه فرمت اصلی وجود داشت:
🪟 ویندوز (Windows): استفاده از ترکیب
\r\n (CRLF)🐧 یونیکس و لینوکس (Unix & Linux): استفاده از
\n (LF)🍎 مکاواس قدیمی (Older MacOS): استفاده از
\r (CR)🌍 با ظهور استاندارد Unicode، این تنوع باز هم فراتر رفت و سه کاراکتر دیگر نیز به این جمع اضافه شدند:
🔹 Next Line (
\u0085 - NEL)🔹 Line Separator (
\u2028 - LS)🔹 Paragraph Separator (
\u2029 - PS)💡 حال تصور کنید متنی ترکیبی دارید که از منابع مختلف (وبسرویسها، فایلهای آپلود شده توسط کاربران با سیستمعاملهای گوناگون و دیتابیسها) جمعآوری شده و شامل انواع این کاراکترهاست:
var text = "The Quick\r\nBrown Fox\u0085Jumped Over\u2028The Lazy Dog\nBigly";
🎯 هدف ما این است که خطوط این متن چندخطی (Multiline) را به صورت تفکیکشده استخراج کنیم.
🔍 بررسی عملکرد در نسخههای قدیمی داتنت
در NET 10. و نسخههای پیش از آن، برای استخراج خطوط معمولاً از الگوی زیر استفاده میشد:
var oldLines = Regex.Matches(text, @"^.*$", RegexOptions.Multiline)
.Select(r => r.Value)
.ToArray();
در این عبارت باقاعده:
📍 علامت
^ نشاندهنده ابتدای خط است.📍 الگوی
.* هر کاراکتری به جز \n را به تعداد دلخواه جستجو میکند.📍 علامت
$ نشاندهنده انتهای خط است.📍 گزینه
RegexOptions.Multiline به موتور پردازش اعلام میکند که متن ورودی ساختار چندخطی دارد.❓ مشکل کجاست؟
⚠️ موتور Regex داتنت به طور سنتی فقط کاراکتر
\n (LF) را به عنوان جداکننده خط در حالت Multiline میشناسد.در نتیجه اجرای کد بالا روی متن نمونه، خروجی مطلوبی نخواهد داشت:
❌ کاراکترهای یونیکد مانند
\u0085 یا \u2028 اصلاً شناسایی نمیشوند و خطوط متصل به آنها جدا نخواهند شد.❌ در مورد ویندوز (
\r\n) نیز، از آنجا که فقط \n به عنوان پایان خط در نظر گرفته میشود، کاراکتر \r (Carriage Return) به عنوان بخشی از متن خط اول استخراج میشود و معمولاً باید با متدهایی مانند Trim() حذف شود.🚀 راهکار داتنت ۱۱: پرچم RegexOptions.AnyNewLineمایکروسافت در نسخه NET 11.، پرچم جدیدی به نام
RegexOptions.AnyNewLine به کلاس Regex اضافه کرده است.✨ با فعالسازی این گزینه، لنگرهای سر خط (
^)، ته خط ($)، پایان رشته (\Z) و همچنین کاراکتر نقطه (.)، به صورت هوشمند تمامی استانداردهای خط جدید (اعم از ویندوز، لینوکس و انواع یونیکدها) را به رسمیت میشناسند.کد بهینهشده در NET 11. به شکل زیر تغییر میکند:
var text = "The Quick\r\nBrown Fox\u0085Jumped Over\u2028The Lazy Dog\nBigly";
// استفاده همزمان از حالت چندخطی و پرچم جدید AnyNewLine
var newLines = Regex.Matches(
text,
@"^.*$",
RegexOptions.Multiline | RegexOptions.AnyNewLine)
.Select(r => r.Value)
.ToArray();
foreach (var line in newLines)
{
Console.WriteLine($"Line: {line}");
}
✅ چرا این راهکار بسیار کارآمدتر است؟
🌍 1. شناسایی کامل Unicode
تمام کاراکترهای پایان خط یونیکد، بدون نیاز به نوشتن Patternهای پیچیده و طولانی، به طور خودکار پردازش میشوند.
⚡️ 2. یکپارچهسازی اتمیک \r\n
برخلاف ترفندهای قدیمی (مانند استفاده از \r?$) که کاراکتر \r را به عنوان بخشی از تطابق (Match) برمیگرداندند،
گزینه AnyNewLine با ترکیب \r\n به عنوان یک موجودیت واحد (Atomic) برخورد میکند و از ورود کاراکتر مزاحم \r به خروجی نهایی جلوگیری میکند.
🎯 جمعبندی
✨ معرفی قابلیت RegexOptions.AnyNewLine در NET. 11 گام مهمی در جهت بهبود پایداری و سادگی پردازش متون چندزبانه و بینالمللی است.
✅ با بهکارگیری این پرچم، دیگر نیازی به نوشتن متدهای کمکیِ سنگین برای نرمالسازی خطوط پیش از اعمال Regex یا استفاده از Patternهای پیچیده و غیراستاندارد نخواهید داشت.
Post #714
329
#اشتباهات_مهندسی (Engineering Mistakes)
یکی از اشتباهاتی که خیلی آرام وارد پروژه میشود، این است که به جای حذف کردن، فقط اضافه میکنیم.
یک Configuration جدید...
یک Feature Flag جدید...
یک Service جدید...
یک جدول جدید...
یک Endpoint جدید...
کمکم سیستم بزرگتر میشود.
اما سؤال اینجاست:
آخرین باری که چیزی را از سیستم حذف کردیم، کی بود؟
جالب است که اضافه کردن، همیشه حس پیشرفت میدهد.
اما حذف کردن، شجاعت میخواهد.
چون باید مطمئن باشی چیزی را که سالها وجود داشته، دیگر کسی نیاز ندارد.
تیمهای باتجربه فقط قابلیت جدید توسعه نمیدهند.
به همان اندازه، روی حذف چیزهایی که دیگر ارزش ایجاد نمیکنند هم وقت میگذارند.
کدی که دیگر استفاده نمیشود.APIهایی که هیچ Client فعالی ندارند.Featureهایی که سالهاست کسی از آنها استفاده نکرده است.
هر چیزی که حذف نمیشود، هزینه نگهداری دارد.
شاید امروز دیده نشود.
اما در هر Refactoring، هر Deploy و هر تغییر، خودش را نشان میدهد.
در مهندسی نرمافزار،
رشد یک سیستم فقط به چیزهایی که به آن اضافه میکنیم وابسته نیست.
گاهی کیفیت واقعی یک سیستم را،
چیزهایی تعیین میکنند که جرئت حذف کردنشان را داشتهایم.
یکی از اشتباهاتی که خیلی آرام وارد پروژه میشود، این است که به جای حذف کردن، فقط اضافه میکنیم.
یک Configuration جدید...
یک Feature Flag جدید...
یک Service جدید...
یک جدول جدید...
یک Endpoint جدید...
کمکم سیستم بزرگتر میشود.
اما سؤال اینجاست:
آخرین باری که چیزی را از سیستم حذف کردیم، کی بود؟
جالب است که اضافه کردن، همیشه حس پیشرفت میدهد.
اما حذف کردن، شجاعت میخواهد.
چون باید مطمئن باشی چیزی را که سالها وجود داشته، دیگر کسی نیاز ندارد.
تیمهای باتجربه فقط قابلیت جدید توسعه نمیدهند.
به همان اندازه، روی حذف چیزهایی که دیگر ارزش ایجاد نمیکنند هم وقت میگذارند.
کدی که دیگر استفاده نمیشود.APIهایی که هیچ Client فعالی ندارند.Featureهایی که سالهاست کسی از آنها استفاده نکرده است.
هر چیزی که حذف نمیشود، هزینه نگهداری دارد.
شاید امروز دیده نشود.
اما در هر Refactoring، هر Deploy و هر تغییر، خودش را نشان میدهد.
در مهندسی نرمافزار،
رشد یک سیستم فقط به چیزهایی که به آن اضافه میکنیم وابسته نیست.
گاهی کیفیت واقعی یک سیستم را،
چیزهایی تعیین میکنند که جرئت حذف کردنشان را داشتهایم.