انتقال للمقال
وقت القراءة: ≈ 5 دقائق

مبدأ الـ Open/Closed

السلام عليكم ورحمة الله وبركاته

يمكنك متابعة السلسلة بالترتيب أو الانتقال مباشرة إلى أي مقال:


المقدمة

مبدأ الـ Open/Closed هو المبدأ الثاني من مبادئ الـ SOLID وهو مبدأ يركز على جعل الكود مفتوحًا للتوسعة ومغلقًا للتعديل

اسم Open/Closed هو اختصار لـ Open for extension and closed for modification ومعناه أن الكلاس قابل ليتم وراثته وممنوع التعديل عليه

فمبدأ الـ Open/Closed يركز على أننا لدينا كلاس أساسي يقوم بتنفيذ وظيفته على أكمل وجب
المبدأ يقول لك ألا تقوم بالتعديل على هذا الكلاس الأساسي طالما هو يقوم بتنفيذ وظيفته
وإذا أردت إضافة وظائف جديدة للكلاس كدوال جديدة أو مميزات جديدة قم بوراثته في كلاس آخر جديد وقم بعمل ما تريده فيه

المبدأ يشجع على استخدام الـ Interface أو Abstract Class لتعريف الدوال الأساسية والأمور الأساسية التي ستكون عامة عند الجميع
ولو أراد كلاس معين إضافة شيء جديد خاص به أو تعديل شيء ما، يقوم بعمل implement للـ Interface أو extend للـ Abstract Class ويقوم بإضافة وتعديل ما يريده

ستقول لي أن هذا اشبه بالأمور التي تعلمناها في مقالة الـ الـ Inheritance ووراثة الكلاسات في البرمجة ومقالة مفهوم الـ Polymorphism في تعدد الأشكال هذا صحيح لأنه كما قلنا أن الـ SOLID هي أفكار ومبادئ لا أكثر وأنت تستخدمها وتطبقها على قدر المستطاع بالطريقة التي تناسبك
والـ Inheritance والـ Polymorphism هما أحد الأفكار التي تساعدك على تطبيق مبدأ الـ Open/Closed

مثال يناقض المبدأ

فتخيل معي كلاس يدعى ProductService وهو كلاس يهتم بالمنتجات بشكل عام
تخيل معي أن هذا الكلاس لديه دالة تدعى calculateProductPrice تقوم بحساب المنتجات وتطبيق بعض الخصومات بحسب نوع المنتج
فلو كان من منتجات الألبان يتم خصم مبلغ معين ولو كان منتج أجهزة كهربائية يتم تطبيق خصم مختلف

الأمر سيشبه شيء كهذا

class ProductService {
  public calculateProductPrice(product: Product) {
    if (product.type === "food") {
      return product.price - 10;
    } else if (product.type === "electric") {
      return product.price - 20;
    } else if (product.type === "clothes") {
      return product.price - 30;
    }
    // else if (...) { }
    // else if (...) { }
    // ... etc.
  }

  // ...
}

الآن كلاس الـ ProductService قد يكون كلاس جميل ويطبق مبدأ الـ Single Responsibility بشكل جيد
لكن به مشكله صغيرة وهي أنه يحتوي على دالة calculateProductPrice التي تقوم بتطبيق الخصومات على المنتجات بحسب نوعه
وكما تلاحظ فالدالة تحتوي على if و else if وهذا يعني أنه يمكن أن يكون هناك تعديلات كثيرة على الدالة وسنضع if و else if كل مرة

بالتالي فالكلاس في هذه الدالة لا يطبق مبدأ الـ Open/Closed لأنه سيتم تعديل هذه الدالة على مدار الساعة بشكل دائم
والمبدأ ينص على أننا يجب أن نجعل الأشياء قابلة لتوسع لكن في نفس الوقت لا يتم تعديل الدالة مجددًا
لكن كيف نجعل الدالة calculateProductPrice قابلة للتوسع وفي نفس اللحظة ا نقوم بتعديلها ؟

مثال يوافق المبدأ

الحل بكل بساطة هو أن نقوم بعمل Interface أو Abstract Class يكون به الدوال العامة لكل منتج
وكل نوع منتجات معينة سيقوم بعمل implement لهذا الـ Interface أو extend للـ Abstract Class ويقوم بتعديل وإضافة ما يريده

فمثلا سنقوم بعمل Interface يدعى IProduct يحتوي على دالة calculatePrice
وسنقوم بعمل implement لهذا الـ Interface في كلاسات المنتجات المختلفة مثل FoodProduct, ElectricProduct, ClothesProduct وغيرها
وكل كلاس سيقوم بعمل implement للدالة calculatePrice بالطريقة التي يريدها

interface IProduct {
  calculatePrice(): number;
}

class FoodProduct implements IProduct {
  public calculatePrice() {
    return this.price - 10;
  }
}

class ElectricProduct implements IProduct {
  public calculatePrice() {
    return this.price - 20;
  }
}

class ClothesProduct implements IProduct {
  public calculatePrice() {
    return this.price - 30;
  }
}

الآن نستطيع أن نذهب للدالة calculateProductPrice التي كان بها مشكلة الـ if و else if

class ProductService {
  public calculateProductPrice(product: Product) {
    if (product.type === "food") {
      return product.price - 10;
    } else if (product.type === "electric") {
      return product.price - 20;
    } else if (product.type === "clothes") {
      return product.price - 30;
    }
    // else if (...) { }
    // else if (...) { }
    // ... etc.
  }

  // ...
}

ونقوم بتعديلها لتقوم بالتعامل مع الـ IProduct وتستدعي دالة calculatePrice منه

class ProductService {
  public calculateProductPrice(product: IProduct) {
    return product.calculatePrice();
  }

  // ...
}

هكذا الآن الدالة calculateProductPrice أصبحت تقوم بتطبيق الخصومات على المنتجات بحسب نوعها بدون الحاجة للـ if و else if
وأيضًا أصبحت تطبق مبدأ الـ Open/Closed بامتياز
الآن الدالة تعتمد على الـ IProduct وهو الـ Interface الذي يحتوي على دالة calculatePrice
وأيًا الـ object الذي يتم إرساله للدالة سيقوم بتطبيق الدالة calculatePrice بالطريقة التي يريدها هذا الـ object
سواء كان هذا الـ object من نوع FoodProduct أو ElectricProduct أو ClothesProduct أو غيرها

وإن أردت إضافة نوع منتجات جديدة بخصومات مختلفة فقط قم بعمل implement للـ IProduct وقم بتعديل الدالة calculatePrice بالطريقة التي تريدها
دون أن تقوم بتعديل أي شيء في الدالة calculateProductPrice في الـ ProductService

الدالة أصبحت كما ينص المبدأ دالة مفتوحة دائمًا للتوسع واستقبال أي نوع مهما كان وأصبحت مغلقة تمامًا بحيث لن يتم تعديلها مجددًا

يمكنك أن تتطبق فكرة المبدأ بطرق مختلفة سواء على مستوى الدوال أول على مستوى الكلاسات
لأن المبدأ مجرد فكرة وأنت توظفها بالشكل الذي يناسبك
يمكن أن يكون لديك كلاس يدعى EmployeeService به دوال متعددة تخدم وظيفته وأنت اختبرتها وتأكدت منها والكلاس يقوم بواجبه على أكمل شكل ممكن
وأنت الآن مضطر لإضافة بعض المميزات الجديدة فمثلا تريد أن تضيف إلى الـ EmployeeService خانة لتخزين عدد الساعات الاضافية التي يعملها بعض الموظفين خارج اطار ساعات عمله الطبيعة
وتريد إضافة دوال جديدة تحسب وتخدم على فكرة الساعات الجديدة وحساب راتبه وتلك الأمور
فبدلًا من أن تقوم بتعديل الكلاس الأساسي EmployeeService يمكنك أن ترثه في كلاس جديد يدعى SlaveEmployeeService وتقوم بتعديل وإضافة ما تريده فيه

هكذا حققت المبدأ بطريقة مختلفة على مستوى الكلاسات بحيث أنك ثبت الكلاس الأساسي EmployeeService ولم تقم بالتعديل عليه وقمت بعمل كلاس جديد SlaveEmployeeService وقمت بتعديل وإضافة ما تريده فيه
هكذا ينص المبدأ لكن طبقناه بشكل آخر Open to extension and closed to modification

خلاصة مبدأ الـ Open/Closed

يمكننا أن نلخص مبدأ الـ Open/Closed في أنه يركز على تقليل التعديل على الكلاسات والدوال على قدر المستطاع عن طريق جعله مفتوحة للتوسيع ومغلقة للتعديل
بمعنى أنك يجب أن تكون قادرًا على إضافة ميزات جديدة دون الحاجة لتعديل الكود الحالية، طالما الكود يعمل فلا تعدل عليه

يمكنك أن تطبقه على أكثر من شكل وحالة سواء على مستوى الدوال أو على مستوى الكلاس ككل
على مستوى الدالة إذا كانت تعتمد على أكثر من نوع مختلفة من شيء معين مثل نوع المنتج او رتبة المستخدم
وتجد نفسك تقوم بعمل كود مختلف ليناسب كل نوع وتقوم بعمل if else لكل نوع
هنا إذا كنت تستطيع جعل الدالة تستقبل هذا الشيء الذي يضم أنواع مختلفة وتستقبل كـ object ويكون interface أو abstract class
ثم ترمي مسؤولية الـ implementation على الكلاسات الفرعية التي سيمثلها هذا الـ object

فبدلًا من

class PermissionService {
  public isAllowTo(user: User, action: string) {
    if (user.role === "admin") {
      return true;
    } else if (
      user.role === "editor" &&
      (action === "read" || action === "write" || action === "edit")
    ) {
      return true;
    } else if (user.role === "viewer" && action === "read") {
      return true;
    } else {
      return false;
    }
  }
}

تجعله هكذا

class PermissionService {
  public isAllowTo(user: IUser, action: string) {
    return user.isAllowTo(action);
  }
}

interface IUser {
  isAllowTo(action: string): boolean;
}

class Admin implements IUser {
  public isAllowTo(action: string) {
    return true;
  }
}

class Editor implements IUser {
  public isAllowTo(action: string) {
    return action === "read" || action === "write" || action === "edit";
  }
}

class Viewer implements IUser {
  public isAllowTo(action: string) {
    return action === "read";
  }
}

// ... ContentCreator, MediaBuyer, etc.

أو لو كانت الدالة تقوم بواجبها على أكمل شكل ولا تريد أن تعدلها أبدًا
أو لو كان الكلاس ككل يقوم بواجبه بالشكل المطلوب واختبرته وكتبت أكثر من unit test وكل شيء يعمل بشكل جيد
ولا تريد تعديله لكن تريدان تضيف عليه او تضيف أشياء جديدة للدوال
فيمكنك وراثة هذا الكلاس وتقوم بعمل override للدوال التي تريد تغيرها وتضيف الأشياء التي تريدها
وتبقي الكلاس الأساسي كما هو دون تعديل


رسالة خاصة

أرسل ملاحظاتك أو رأيك بشكل خاص — لن يظهر للآخرين

التعليقات

شاركنا رأيك في هذه المقالة أو اسأل عن أي شيء يخصها