در سیستمهای کوچک، یک خطای رخداده ممکن است حداکثر باعث لغو یک عملیات ساده شود؛ اما در سیستمهای سازمانی، مدیریت نادرست خطا میتواند منجر به بروز نشت حافظه (Memory Leak)، نامعتبر شدن وضعیت دادهها (Data Inconsistency)، لو رفتن اطلاعات امنیتی حساس، و افت شدید کارایی (Performance) شود.
یک معماری مدیریت استثنای مدرن باید اهداف زیر را محقق کند:
یکی از اولین تصمیمات معماری، تفکیک صحیح انوع خطاهاست. بهطور کلی استثناها به چند گروه تقسیم میشوند:
الف) خطاهای فنی و زیرساختی (Infrastructure / Technical Exceptions)
این خطاها ناشی از مشکلات محیطی یا سیستمعامل هستند؛ مانند قطعی شبکه، پر شدن دیسک، یا عدم دسترسی به دیتابیس. برنامه معمولاً نمیتواند منطق تجاری خاصی برای جبران آن انجام دهد مگر تلاش مجدد (Retry) یا گزارش به مدیر سیستم.
ب) خطاهای دامنه و منطق تجارت (Domain / Business Exceptions)
این خطاها جزئی از فرآیند نرمافزار هستند و رخ دادن آنها پیشبینی میشود. بهعنوان مثال: «موجودی حساب کافی نیست» یا «کد تخفیف منقضی شده است». این موارد نباید با خطاهای ناشناخته زیرساختی یکسان انگاشته شوند.
ج) Checked vs. Unchecked (درسهایی از زبانها)
در زبانهایی مثل Java مفاهیم Checked و Unchecked وجود دارند، در حالی که در C# یا Kotlin تمام استثناها Unchecked هستند.
برای داشتن کدی تمیز و قابل توسعه، رعایت اصول زیر الزامی است:
۱. اصل Don't Catch What You Can't Handle
فقط زمانی یک استثنا را Catch کنید که قصد دارید روی آن اقدامی انجام دهید (مثلاً بازیابی وضعیت، تبدیل به استثنای دیگر، یا خنثیسازی). Catch کردن خطا تنها برای لوگ کردن و سپس Re-throw کردن بدون تغییر، یک آنتیپترن شایع است.
// Anti-Pattern
try {
_paymentService.Process();
} catch (Exception ex) {
_logger.LogError(ex, "Error occurred");
throw ex; // Stack trace is lost!
}
// Correct Approach
try {
_paymentService.Process();
} catch (PaymentGatewayException ex) {
// Actionable handling
_logger.LogWarning(ex, "Payment gateway failed, switching to fallback.");
_paymentService.ProcessFallback();
}
۲. حفظ Traceability و Exception Wrapping
هنگامی که خطایی در لایه پایینتر (مثل Data Access) رخ میدهد، نباید همان خطای دیتابیس به لایههای بالایی (مثل UI یا API) نفوذ کند (پیوستگی لایهها را نقض میکند). در عوض، خطا باید در لایه جاری Catch شده و درون یک Custom Domain Exception کپسوله (Wrap) شود، به شرطی که خطای اصلی به عنوان InnerException حفظ گردد.
public class OrderProcessingException : Exception
{
public OrderProcessingException(string message, Exception innerException)
: base(message, innerException) { }
}
۳. استفاده از Exception Hierarchy مشخص
به جای استفاده از کلاس پایه Exception یا RuntimeException، یک درخت ساختاریافته از استثناها برای دامنه کاربردی خود بسازید:
BaseApplicationException
├── DomainException
│ ├── InsufficientFundsException
│ └── OrderNotFoundException
└── InfrastructureException
├── DatabaseTimeoutException
└── ThirdPartyApiUnavailableException
در سیستمهای پیچیده شیگرا، ابزارهای سنتتی زبان (مانند try-catch) برای پاسخگویی به همه نیازها کافی نیستند. الگوهای زیر جزییات پیشرفتهتری را فراهم میکنند:
| الگوی طراحی | شرح و کاربرد |
| Result Pattern | به جای پرتاب Exception برای خطاهای متداول تجاری، متدها یک شیء Result برمیگردانند که حاوی وضعیت موفقیت/شکست و پیام خطاست. این کار از هزینه سنگین پرتاب Exception جلوگیری میکند. |
| Circuit Breaker | جلوگیری از تکرار درخواستها به سرویسی که دچار مشکل شده است (مانند الگوهای Polly در .NET یا Resilience4j در Java). |
| Global Exception Handler | تمرکز تمام خطاهای پیشبینینشده در یک میدلور (Middleware) یا Interceptor مرکزی در سطح API یا App. |
| Null Object Pattern | جلوگیری از بروز خطای شایع NullReferenceException با بازگرداندن یک شیء خنثی به جای null. |
پیادهسازی Result Pattern به عنوان جایگزین Exceptionهای تجاری
پرتاب استثنا (Throwing Exception) در سیستمهای با بار بالا (High Throughput) به دلیل نیازمندی به بازسازی Stack Trace از نظر حافظه و پردازنده بسیار سنگین است. برای جریانهای عادی کنترل برنامه نباید از Exception استفاده کرد.
public class Result
{
public bool IsSuccess { get; }
public T Value { get; }
public string ErrorMessage { get; }
protected Result(bool isSuccess, T value, string errorMessage)
{
IsSuccess = isSuccess;
Value = value;
ErrorMessage = errorMessage;
}
public static Result Success(T value) => new Result(true, value, null);
public static Result Failure(string message) => new Result(false, default, message);
}
در معماریهای مدرن مثل Clean Architecture، هر لایه مسئولیت خاصی در قبال خطاها دارد:
+-------------------------------------------------------+
| Presentation Layer (Controllers / Middleware) |
| --> Catch All, Log, Translate to HTTP 4xx/5xx / JSON |
+-------------------------------------------------------+
│
▼
+-------------------------------------------------------+
| Application / Use Cases Layer |
| --> Validate Input, Handle Flow & Result Objects |
+-------------------------------------------------------+
│
▼
+-------------------------------------------------------+
| Domain Layer |
| --> Throw Business Rule Exceptions (Pure Domain) |
+-------------------------------------------------------+
│
▼
+-------------------------------------------------------+
| Infrastructure Layer (DB, External APIs) |
| --> Catch Technical Exceptions & Wrap to Domain Ex |
+-------------------------------------------------------+
مدیریت خطا بدون Observability و ثبت درست دادهها ناقص است. نکات زیر در سیستمهای بزرگ الزامی است:
مدیریت استثناها در سیستمهای پیچیده شیگرا، یک موضوع اتفاقی یا یک اقدام ثانویه نیست؛ بلکه بخشی از طراحی اولیه معماری است. با تفکیک دقیق خطاهای فنی از خطاهای تجاری، استفاده از الگوهایی نظیر Result Pattern برای جریانهای کنترل عادی، کپسولهسازی خطاها در لایهها و بهرهگیری از مدیریت متمرکز (Global Handling)، میتوان سیستمهایی ساخت که نه تنها در برابر شرایط غیرمنتظره مقاوم هستند، بلکه توسعه و نگهداری آنها برای تیمهای نرمافزاری آسان و لذتبخش خواهد بود.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.