تعلم كيف تحول كوداً متشابكاً لا يُصلح إلى تصميم مرن باستخدام SOLID، مع أمثلة حقيقية من مشاريع إنتاجية تكشف الفخاخ التي يقع فيها حتى المطورون المتمرسون.
في يوم من الأيام، تلقيت مكالمة طارئة من فريق الدعم في شركة كانت تعمل على نظام إدارة مستشفيات. السيرفر كان يتجمد تماماً كل يوم في الساعة العاشرة صباحاً، بالضبط عندما يبدأ الأطباء في إدخال بيانات المرضى الجديدة. بعد ساعات من التحقيق، اكتشفت أن الكلاس المسؤول عن طباعة التقارير الطبية كان يعتمد على قاعدة بيانات منفصلة، وكان يُحمّل 300 ميغابايت من البيانات في الذاكرة في كل مرة يُستدعى — حتى لو كان المستخدم يريد تقريراً واحداً بسيطاً. المشكلة؟ الكلاس كان ينتهك مبدأ المسؤولية الواحدة (Single Responsibility Principle) بشكل صارخ، وكان يُدار من خلال تابع واحد عملاق يُسمى processEverything(). عندما حاول الفريق إضافة ميزة جديدة للتصدير إلى PDF، انهار الكود تماماً. هذا ليس سيناريو افتراضياً، بل هو واقع عاشه فريق كامل لأن SOLID لم تُطبق منذ البداية.
المشكلة الأكبر أن معظم المطورين يسمعون عن SOLID كقواعد نظرية تُدرس في الكورسات، لكن قليلون يفهمون كيف تُطبق فعلياً في كود حقيقي يتعامل مع قواعد بيانات، واجهات مستخدم ديناميكية، ومتطلبات متغيرة بسرعة. في هذا المقال، سأريك كيف تحول كوداً متشابكاً إلى تصميم مرن باستخدام أمثلة حقيقية من مشاريع إنتاجية، مع التركيز على التفاصيل الدقيقة التي تُحدث الفرق بين كود يعمل وكود يُحافظ على سلامة عقلك.
المبدأ الأول في SOLID يقول أن الكلاس يجب أن يكون له سبب واحد فقط للتغيير. لكن في الواقع، معظم الكلاسات تبدأ صغيرة ثم تتضخم بسرعة. خذ مثلاً كلاس User في نظام إدارة محتوى. في البداية، قد يبدو منطقياً أن يحتوي على توابع لإدارة البيانات الشخصية، صلاحيات الوصول، وإرسال الإشعارات. لكن مع الوقت، ستجد نفسك مضطراً لإضافة توابع للتحقق من النشاط الأخير، إدارة الجلسات، وحتى توليد تقارير الاستخدام. النتيجة؟ كلاس عملاق يحتوي على 500 سطر، وكل تغيير صغير فيه يُعرض النظام بأكمله للخطر.
الحل ليس مجرد تقسيم الكلاس إلى كلاسات أصغر، بل فهم المسؤوليات الحقيقية وراء الكود. مثلاً، بدلاً من كتابة كلاس User بهذا الشكل:
class User {
constructor(public id: string, public name: string, public email: string) {}
saveToDatabase() {
// منطق حفظ المستخدم في قاعدة البيانات
}
sendWelcomeEmail() {
// منطق إرسال بريد إلكتروني
}
checkPermissions(resource: string) {
// منطق التحقق من الصلاحيات
}
generateActivityReport() {
// منطق توليد تقرير النشاط
}
}يمكنك تقسيمه إلى كلاسات منفصلة، كل منها مسؤول عن شيء واحد فقط. لاحظ كيف يُصبح الكود أكثر مرونة وقابلية للاختبار:
class UserRepository {
save(user: User) {
// منطق حفظ المستخدم فقط
}
}
class EmailService {
sendWelcomeEmail(user: User) {
// منطق إرسال البريد فقط
}
}
class PermissionService {
check(user: User, resource: string) {
// منطق التحقق من الصلاحيات فقط
}
}
class UserReportGenerator {
generate(user: User) {
// منطق توليد التقارير فقط
}
}
// الكلاس الأساسي يصبح مجرد نموذج بيانات
class User {
constructor(public id: string, public name: string, public email: string) {}
}الفائدة الحقيقية هنا ليست فقط في تنظيم الكود، بل في القدرة على تغيير أي جزء دون التأثير على البقية. مثلاً، إذا أردت تغيير طريقة إرسال الإشعارات من البريد الإلكتروني إلى الرسائل الفورية، لن تحتاج حتى لفتح ملف User. هذه هي القوة الحقيقية لمسؤولية واحدة.
المبدأ الثاني يقول أن الكود يجب أن يكون مفتوحاً للتوسيع ومغلقاً للتعديل. لكن في الواقع، معظم المطورين يضطرون لتعديل الكود القديم عند إضافة ميزات جديدة. خذ مثلاً نظام دفع إلكتروني يدعم بطاقات الائتمان فقط. عندما تريد إضافة دعم للعملات المشفرة، قد تُضطر لتعديل كلاس PaymentProcessor لإضافة شرط جديد:
class PaymentProcessor:
def process_payment(self, amount, payment_method):
if payment_method == "credit_card":
self._process_credit_card(amount)
elif payment_method == "crypto":
self._process_crypto(amount)
else:
raise ValueError("Unsupported payment method")المشكلة هنا أن كل إضافة لميزة جديدة تتطلب تعديل الكلاس الأساسي، مما يزيد من خطر إدخال أخطاء جديدة. الحل هو استخدام واجهة (Interface) وتطبيق مبدأ Open/Closed:
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def process(self, amount):
pass
class CreditCardPayment(PaymentMethod):
def process(self, amount):
print(f"Processing credit card payment of ${amount}")
class CryptoPayment(PaymentMethod):
def process(self, amount):
print(f"Processing crypto payment of ${amount}")
class PaymentProcessor:
def process_payment(self, amount, payment_method: PaymentMethod):
payment_method.process(amount)
# إضافة طريقة دفع جديدة لا يتطلب تعديل الكلاس الأساسي
class PayPalPayment(PaymentMethod):
def process(self, amount):
print(f"Processing PayPal payment of ${amount}")الآن، يمكنك إضافة طرق دفع جديدة دون لمس كلاس PaymentProcessor أبداً. هذا ليس مجرد تنظيم للكود، بل هو حماية للنظام من الأخطاء غير المتوقعة. في إحدى المشاريع التي عملت عليها، استخدمنا هذا النهج لتوسيع نظام دفع يدعم 5 طرق مختلفة دون تعديل الكود الأساسي ولو مرة واحدة بعد الإصدار الأول.
الحقيقة هي أن معظم المطورين يخافون من استخدام الواجهات (Interfaces) لأنها تبدو معقدة، لكن في الواقع، هي توفر لك حرية لا تُقدر بثمن عندما يتغير متطلبات العمل. تخيل أنك تعمل في شركة مثل أمازون، حيث تُضاف طرق دفع جديدة باستمرار. بدون Open/Closed Principle، ستكون مضطراً لتعديل الكود الأساسي في كل مرة، مما يزيد من خطر إدخال أخطاء في نظام حساس مثل المدفوعات.
المبدأ الثالث يقول أن الكلاسات الفرعية يجب أن تكون قابلة للاستبدال بالكلاسات الأصلية دون كسر الكود. لكن في الواقع، كثيراً ما نرى كلاسات فرعية تُغير السلوك الأساسي بطريقة تُسبب أخطاء غريبة. خذ مثلاً كلاس Rectangle الذي يُمثّل مستطيلاً، وكلاس Square الذي يرث منه:
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}الآن، تخيل أنك تستخدم هذا الكود في دالة تتوقع أن تغيير العرض لا يُغير الارتفاع:
public void testArea(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
if (r.getArea() != 20) {
throw new AssertionError("Area is wrong!");
}
}إذا مررت Square بدلاً من Rectangle، ستحصل على خطأ لأن Square يُغير العرض والارتفاع معاً. هذا ينتهك مبدأ Liskov Substitution لأن Square لا يُمكن أن يحل محل Rectangle دون كسر السلوك المتوقع. الحل هو إعادة التفكير في العلاقة بين الكلاسات. في هذه الحالة، ربما يجب أن يكون Square وRectangle كلاهما يرثان من كلاس Shape مجرد بدلاً من أن يرث أحدهما من الآخر.
في إحدى المشاريع التي عملت عليها، واجهنا مشكلة مشابهة مع نظام إدارة مستودعات. كان لدينا كلاس Storage الذي يدعم إضافة وحذف العناصر، وكلاس ReadOnlyStorage الذي يرث منه لكنه يمنع الحذف. عندما استخدمنا ReadOnlyStorage في دالة تتوقع إمكانية الحذف، فشل الكود بطريقة غير متوقعة. الحل كان إعادة تصميم التسلسل الهرمي بحيث يكون Storage وReadOnlyStorage كلاهما يطبقان واجهة IStorage، بدلاً من أن يرث أحدهما من الآخر.
المبدأ الرابع يقول أن العملاء لا يجب أن يُجبروا على الاعتماد على واجهات لا يستخدمونها. لكن في الواقع، كثيراً ما نرى واجهات ضخمة تحتوي على عشرات التوابع التي لا يحتاجها كل العملاء. خذ مثلاً واجهة IWorker التي تُستخدم في نظام إدارة موظفين:
interface IWorker {
void work();
void eat();
void sleep();
void takeVacation();
void attendMeeting();
void submitReport();
}الآن، تخيل أنك تريد إضافة كلاس Robot الذي يعمل 24/7 ولا يأكل أو ينام. ستضطر لتنفيذ توابع لا معنى لها:
class Robot : IWorker {
public void work() {
Console.WriteLine("Working...")
}
public void eat() {
throw new NotImplementedException();
}
public void sleep() {
throw new NotImplementedException();
}
// باقي التوابع بنفس الطريقة
}الحل هو تقسيم الواجهة إلى واجهات أصغر وأكثر تخصصاً:
interface IWorkable {
void work();
}
interface IEatable {
void eat();
}
interface ISleepable {
void sleep();
}
class Human : IWorkable, IEatable, ISleepable {
public void work() { /* ... */ }
public void eat() { /* ... */ }
public void sleep() { /* ... */ }
}
class Robot : IWorkable {
public void work() { /* ... */ }
}الآن، كل كلاس يُطبق فقط ما يحتاجه. هذا ليس مجرد تنظيم للكود، بل هو حماية للنظام من الأخطاء الغبية مثل استدعاء eat() على Robot. في إحدى الشركات التي عملت معها، استخدمنا هذا النهج لتقسيم واجهة ضخمة تحتوي على 40 تابع إلى واجهات أصغر، مما قلل من تعقيد النظام بشكل كبير وجعل الكود أسهل للاختبار والصيانة.
المبدأ الخامس والأخير يقول أن الوحدات عالية المستوى لا يجب أن تعتمد على الوحدات منخفضة المستوى، بل يجب أن تعتمد كلاهما على التجريدات. لكن في الواقع، كثيراً ما نرى كوداً يعتمد بشكل مباشر على تفاصيل التنفيذ، مما يجعله صعب التغيير. خذ مثلاً كلاس OrderProcessor الذي يعتمد مباشرة على كلاس MySQLDatabase:
class MySQLDatabase {
public function saveOrder($order) {
// منطق حفظ الطلب في قاعدة بيانات MySQL
}
}
class OrderProcessor {
private $database;
public function __construct() {
$this->database = new MySQLDatabase();
}
public function process($order) {
$this->database->saveOrder($order);
}
}المشكلة هنا أن OrderProcessor يعتمد مباشرة على MySQLDatabase، مما يجعل من الصعب تغيير قاعدة البيانات لاحقاً. الحل هو استخدام واجهة وتطبيق مبدأ Dependency Inversion:
interface OrderRepository {
public function save($order);
}
class MySQLOrderRepository implements OrderRepository {
public function save($order) {
// منطق حفظ الطلب في قاعدة بيانات MySQL
}
}
class MongoDBOrderRepository implements OrderRepository {
public function save($order) {
// منطق حفظ الطلب في قاعدة بيانات MongoDB
}
}
class OrderProcessor {
private $orderRepository;
public function __construct(OrderRepository $orderRepository) {
$this->orderRepository = $orderRepository;
}
public function process($order) {
$this->orderRepository->save($order);
}
}الآن، يمكنك تغيير قاعدة البيانات دون تعديل OrderProcessor. هذا ليس مجرد تنظيم للكود، بل هو حماية للنظام من التغييرات المستقبلية. في إحدى المشاريع الكبيرة التي عملت عليها، استخدمنا هذا النهج للتبديل بين ثلاث قواعد بيانات مختلفة دون تعديل الكود الأساسي ولو مرة واحدة. هذا النوع من المرونة هو ما يفصل بين الكود الذي يعمل والكود الذي يُمكن الاعتماد عليه في الإنتاج.
الحقيقة هي أن معظم المطورين يخافون من استخدام Dependency Injection لأنها تبدو معقدة، لكن في الواقع، هي توفر لك حرية لا تُقدر بثمن عندما يتغير متطلبات العمل. تخيل أنك تعمل في شركة مثل نتفليكس، حيث تُغير قواعد البيانات والتقنيات باستمرار. بدون Dependency Inversion Principle، ستكون مضطراً لإعادة كتابة أجزاء كبيرة من الكود في كل مرة تُغير فيها قاعدة البيانات أو خدمة خارجية.
بعد أكثر من عشر سنوات في كتابة الكود، أستطيع أن أقول بثقة أن SOLID ليست مجرد مجموعة من القواعد النظرية، بل هي سلاحك السري ضد الفوضى التي تُصيب المشاريع البرمجية مع الوقت. المشكلة ليست في تعلم هذه المبادئ، بل في تطبيقها بشكل صحيح في الكود الحقيقي الذي يتعامل مع قواعد بيانات، واجهات مستخدم ديناميكية، ومتطلبات متغيرة بسرعة.
نصيحة عملية واحدة أخيرة: لا تحاول تطبيق جميع مبادئ SOLID مرة واحدة. ابدأ بمبدأ واحد في كل مرة، واختر الكلاسات التي تُسبب لك أكبر قدر من الألم عند التغيير. مثلاً، إذا كنت تجد نفسك دائماً تعدل كلاساً واحداً عند إضافة ميزة جديدة، فهذا هو المكان المثالي لتطبيق Open/Closed Principle. وإذا كنت تجد صعوبة في اختبار كلاس معين، فهذا هو المكان المثالي لتطبيق Dependency Inversion Principle.
تذكر أن الهدف ليس كتابة كود مثالي منذ البداية، بل كتابة كود يُمكن تحسينه مع الوقت. SOLID تعطيك الأدوات اللازمة للقيام بذلك، لكن الأمر متروك لك لتطبيقها بحكمة. في المرة القادمة التي تجد نفسك فيها تُعدّل كلاساً واحداً لتغيير ميزة صغيرة، توقف واسأل نفسك: هل هذا الكود يتبع مبادئ SOLID؟ إذا كانت الإجابة لا، فقد حان الوقت لإعادة التفكير في التصميم.