يا هلا بكل المبرمجين والمبرمجات، معكم أخوكم أبو عمر.
بتذكر مرة، قبل كم سنة، كنا شغالين على نظام دفع إلكتروني حساس جداً لأحد العملاء الكبار. كان المشروع ضخم، والـ deadline “على الأبواب”، والضغط النفسي واصل للسما. كان قلبي مقبوض، مش من ضغط الشغل، لأ… من جزء معين في الكود كان عامل زي “الغول” اللي كلنا بنخاف نقرب عليه: وحدة معالجة عملية الدفع.
كانت الوحدة عبارة عن سلسلة من العمليات: التحقق من بيانات المستخدم، ثم التحقق من رصيد البطاقة، ثم التواصل مع بوابة الدفع، ثم حجز المبلغ، ثم تأكيد العملية، ثم إرسال إيصال… وكل خطوة من هدول ممكن تفشل لعشرات الأسباب! زميلنا اللي كتب الكود الأول (الله يسهل عليه) كان مستخدم الـ try-catch بكثافة. مش بس try-catch وحدة، لأ… كانت عبارة عن try-catch جوا try-catch جوا try-catch… إشي بنسميه بالعامية “هرم الهلاك” أو الـ “Pyramid of Doom”.
لما كان يصير خطأ، كنا نقعد ساعات بس عشان نعرف الخطأ صار وين بالضبط! الرسائل كانت عامة “حدث خطأ أثناء الدفع”، والمستخدم والعميل معصّبين، وإحنا مش فاهمين إشي. الوضع صار لا يُطاق. في ليلة من الليالي، وأنا براجع الكود للمرة المليون، تذكرت نمط برمجي كنت قرأت عنه في البرمجة الوظيفية اسمه “Result Pattern”. قلت لحالي: “يا جماعة الخير، ليش ما نجرب إشي جديد؟ ما هو إحنا أصلاً غرقانين”.
وهيك كانت بداية رحلتنا لإنقاذ المشروع، والخروج من جحيم الـ try-catch. خلوني أحكيلكم كيف.
ما هو جحيم الـ try-catch المتشعبة؟
قبل ما نحكي عن الحل، خلينا نفهم المشكلة كويس. لما يكون عندك سلسلة عمليات كل وحدة بتعتمد على اللي قبلها، وكل وحدة ممكن تفشل، الشكل الطبيعي اللي بيلجأ إله المبرمج هو استخدام try-catch. المشكلة بتصير لما تتشعب هاي العمليات.
تخيل معي هذا السيناريو (الكود بلغة C# كمثال، لكن الفكرة تنطبق على أي لغة):
public string ProcessPayment(int userId, decimal amount)
{
try
{
var user = GetUserById(userId); // قد تفشل
try
{
var card = GetUserCreditCard(user); // قد تفشل
try
{
var gateway = new PaymentGateway();
var transactionId = gateway.Charge(card, amount); // قد تفشل
try
{
var receipt = SaveReceipt(transactionId, amount); // قد تفشل
return "Payment Successful! Receipt ID: " + receipt.Id;
}
catch (Exception ex)
{
// فشل في حفظ الإيصال، لازم نعمل rollback
gateway.Rollback(transactionId);
// Log the error
Console.WriteLine("Error saving receipt: " + ex.Message);
return "Error: Payment was charged but failed to save receipt.";
}
}
catch (Exception ex)
{
// فشل في عملية الدفع نفسها
Console.WriteLine("Error charging card: " + ex.Message);
return "Error: Payment failed.";
}
}
catch (Exception ex)
{
// فشل في جلب البطاقة
Console.WriteLine("Error getting credit card: " + ex.Message);
return "Error: Could not retrieve user's card.";
}
}
catch (Exception ex)
{
// فشل في جلب المستخدم
Console.WriteLine("Error getting user: " + ex.Message);
return "Error: User not found.";
}
}
مشاكل هذا النهج
- صعب القراءة: الكود صار عبارة عن هرم مقلوب. صعب جداً تتبع “المسار السعيد” (Happy Path) وهو المسار اللي كل شي فيه بينجح.
- فقدان سياق الخطأ: كل
catchبتتعامل مع الخطأ بشكل منفصل. لما نوصل للمستخدم النهائي، كل اللي بنعرفه هو “حدث خطأ”. ضاعت كل التفاصيل المهمة. - تكرار الكود: لاحظ كيف كود الـ logging والتعامل مع الأخطاء ممكن يتكرر.
- صعب التعديل: تخيل لو بدنا نضيف خطوة جديدة في النص! بدك تفتح
try-catchجديدة وتزيد الطين بلة.
باختصار، هذا الكود غير قابل للصيانة (Unmaintainable) وهش (Brittle). أي تغيير صغير ممكن يكسره.
الحل السحري: نمط النتيجة (Result Pattern)
هنا تدخلت البرمجة الوظيفية لإنقاذ الموقف. فكرة نمط الـ Result بسيطة جداً: بدلاً من رمي استثناء (throw exception) عند حدوث خطأ، الدالة تُرجع كائن (object) يصف النتيجة، سواء كانت نجاحاً أو فشلاً.
هذا الكائن عادةً ما يكون له حالتان:
Success(أوOk): ويحمل القيمة الناتجة عن العملية الناجحة.Failure(أوError): ويحمل معلومات مفصلة عن الخطأ الذي حدث.
هذا يجعل احتمالية الفشل جزءاً صريحاً من “عقد” الدالة (function signature)، بدل ما يكون “قنبلة موقوتة” مخفية ممكن تنفجر بأي لحظة على شكل exception.
كيف نطبّق نمط الـ Result؟
أول شي، بدنا ننشئ كلاس بسيط يمثل هذا النمط. هذا مثال بسيط بلغة C#:
// كلاس أساسي للنتيجة
public class Result
{
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error Error { get; }
protected Result(bool isSuccess, Error error)
{
IsSuccess = isSuccess;
Error = error;
}
public static Result Ok() => new Result(true, Error.None);
public static Result Ok(T value) => new Result(value, true, Error.None);
public static Result Fail(Error error) => new Result(false, error);
public static Result Fail(Error error) => new Result(default, false, error);
}
// كلاس النتيجة مع قيمة (Generic)
public class Result : Result
{
public T Value { get; }
protected internal Result(T value, bool isSuccess, Error error)
: base(isSuccess, error)
{
Value = value;
}
}
// كلاس بسيط لتمثيل الخطأ
public record Error(string Code, string Message)
{
public static readonly Error None = new Error(string.Empty, string.Empty);
public static readonly Error NullValue = new Error("Error.Null", "Value was null.");
}
الكود اللي فوق يمكن يبين معقد شوي، بس فكرته بسيطة: عملنا نوع اسمه Result ممكن يحمل قيمة ناجحة (من أي نوع T) أو يحمل خطأ. الأهم هو الدوال المساعدة Ok() و Fail() اللي بتسهل علينا إنشاء هاي الكائنات.
إعادة بناء الكود باستخدام نمط Result
الآن، لنعيد كتابة دوالنا لترجع Result بدلاً من رمي الاستثناءات.
// 1. الدالة الأولى أصبحت ترجع Result<User>
public Result<User> GetUserById(int userId)
{
var user = _userRepository.Find(userId);
if (user == null)
{
return Result.Fail<User>(new Error("User.NotFound", $"User with ID {userId} not found."));
}
return Result.Ok(user);
}
// 2. الدالة الثانية أصبحت ترجع Result<Card>
public Result<Card> GetUserCreditCard(User user)
{
var card = _cardRepository.GetForUser(user.Id);
if (card == null || card.IsExpired)
{
return Result.Fail<Card>(new Error("Card.Invalid", "User's card is invalid or expired."));
}
return Result.Ok(card);
}
// وهكذا... كل الدوال تتبع نفس النمط
لاحظت الفرق؟ الآن، أي مبرمج يقرأ توقيع الدالة public Result<User> GetUserById يعرف فوراً إنها ممكن تفشل، ويعرف نوع القيمة اللي بترجعها في حال النجاح. ما في مفاجآت!
اللحظة السحرية: تجميع العمليات (Chaining)
“طيب يا أبو عمر، هيك صار عندي بدل كل دالة بترمي exception، دالة بترجع object وبدي أعمل if (result.IsSuccess) في كل خطوة. ما حلينا المشكلة، بس غيرنا شكلها!”.
كلامك صحيح 100% لو وقفنا هون. لكن القوة الحقيقية لنمط Result تكمن في قدرته على “التسلسل” أو “التجميع” (Chaining). خلينا نرجع لدالة ProcessPayment ونشوف كيف بتصير “إشي مرتب” بمعنى الكلمة.
سنضيف بعض الدوال المساعدة (Extension Methods) لكلاس Result لتسهيل الربط:
public static class ResultExtensions
{
// دالة تربط نتيجة بنتيجة أخرى
public static Result Bind(this Result result, Func<TIn, Result> func)
{
if (result.IsFailure)
{
return Result.Fail(result.Error);
}
return func(result.Value);
}
// دالة لتنفيذ شي في حالة النجاح
public static Result Tap(this Result result, Action action)
{
if(result.IsSuccess)
{
action(result.Value);
}
return result;
}
}
والآن، شاهد السحر وهو يحدث في دالة ProcessPayment الجديدة:
public Result ProcessPayment(int userId, decimal amount)
{
return GetUserById(userId)
.Bind(user => GetUserCreditCard(user))
.Bind(card => ChargeCard(card, amount))
.Bind(transactionId => SaveReceipt(transactionId, amount))
.Bind(receipt => CreateSuccessMessage(receipt));
}
// ... دوال مساعدة
private Result ChargeCard(Card card, decimal amount) { /* ... */ }
private Result SaveReceipt(string transactionId, decimal amount) { /* ... */ }
private Result CreateSuccessMessage(Receipt receipt)
{
return Result.Ok($"Payment Successful! Receipt ID: {receipt.Id}");
}
شفت الجمال؟ الكود تحول من هرم متشعب إلى خط مستقيم واضح وسهل القراءة. كل خطوة هي دالة Bind. هاي الدالة بتعمل شي بسيط: إذا كانت النتيجة اللي قبلها ناجحة، بتنفذ الدالة اللي بعدها. إذا كانت فاشلة، بتتجاهل كل الخطوات التالية وبترجع الخطأ الأول اللي صار.
هذا المفهوم يسمى أحياناً بـ Railway Oriented Programming. تخيل عندك سكتين حديد: سكة النجاح (خضراء) وسكة الفشل (حمراء). الكود ببدأ على السكة الخضراء، وفي كل محطة (دالة Bind)، بنفحص النتيجة. إذا نجحت، بنكمل على نفس السكة الخضراء. إذا فشلت، بنحول القطار فوراً على السكة الحمراء، وبنمشي عليها لنهاية الرحلة بدون ما نمر على أي محطة خضراء ثانية.
نصائح من الخِبرة (من أبو عمر)
- مش كل خطأ لازم يكون Result: الأخطاء نوعين. النوع الأول هو أخطاء متوقعة (Domain Errors) زي “المستخدم غير موجود” أو “رصيد غير كافي”. هاي مثالية لنمط Result. النوع الثاني هو أخطاء كارثية وغير متوقعة (System Errors) زي “لا يوجد ذاكرة” أو “فشل الاتصال بقاعدة البيانات”. هاي الأخطاء من الأفضل تركها كـ Exceptions، لأنها تعني أن النظام في حالة غير مستقرة ويجب أن يتوقف. لا تستخدم Result لكل شيء.
- وحّد أنواع الأخطاء: لا تجعل كل دالة ترجع نوع خطأ مختلف. أنشئ مجموعة موحدة من كائنات الأخطاء (
Errorobjects) في مشروعك مثلErrors.Validation,Errors.NotFound,Errors.Permission. هذا يجعل التعامل مع الأخطاء في الطبقات العليا (مثل الـ API Controller) أسهل بكثير. - ابدأ صغيراً: لا تحاول إعادة كتابة مشروعك كله مرة واحدة. اختر وحدة (module) معينة تعاني من مشاكل في معالجة الأخطاء وابدأ بتطبيق النمط فيها. عندما يرى الفريق الفائدة، سيتحمس الجميع لتبنيه.
- الـ
Resultليس حلاً لكل المشاكل: هو أداة قوية جداً، لكنه مجرد أداة. استخدمه بحكمة وفي المكان المناسب. أحياناً، جملةif/elseبسيطة تكون كافية وأكثر وضوحاً.
الخلاصة… والزبدة
الانتقال من النهج الإجباري (Imperative) المعتمد على try-catch إلى النهج الوظيفي (Functional) المعتمد على Result هو أكثر من مجرد تغيير في الكود، هو تغيير في طريقة التفكير. أنت تجبر نفسك على التفكير في كل مسارات الفشل المحتملة بشكل صريح، مما ينتج عنه كود أكثر متانة (Robust) وموثوقية.
في مشروعنا هذاك، بعد ما طبقنا نمط Result، قلت نسبة الأخطاء الغامضة بشكل هائل، وصار تعديل الكود وإضافة ميزات جديدة متعة بدل ما يكون كابوس. صرنا قادرين نرجع للمستخدم رسائل خطأ دقيقة ومفيدة، وفريق الدعم الفني صار يقدر يحدد المشكلة من أول نظرة على الـ log.
لا تخف من تجربة أنماط جديدة. البرمجة عالم واسع، ودائماً هناك طريقة أفضل وأكثر أناقة لكتابة الكود. جرب نمط Result في مشروعك القادم، وأنا متأكد أنك ستشكرني لاحقاً. 👍
بالتوفيق يا جماعة الخير!