تعرف على SOLID Design Principles بالعربى – الجزء الثاني

SOLID Principles in Arabic

مرحبا بك فى هذه المقالة والتى سنكمل حديثنا فيها عن SOLID Principles الجزء الثانى، واذ لم تكن قد قرأت الجزء الأول اذهب لقراءته اولا، فقد قمنا بتناول اول 3 مبادئ SOLID ومعرفة نبذه عنها.

المحتوى:

  1. مبدأ Interface Segregation.
  2. مبدأ Dependency Inversion.
  3. الملخص وفائدة مبادىء SOLID.

المتطلبات:

  • أن يكون القارئ على دراية بأساسيات لغة برمجة واحده على الأقل.
  • أن يكون القارئ ملم بمبادئ البرمجة كائنية التوجه Object Oriented Programming.

ملحوظة:

  • سيتم كتابة الأمثلة بلغة Java ولكن لا تقلق اذا لم تكن تعرف لغة Java فذا المقال عام ويمكنك فهم المبادئ وتطبيق الأمثلة بلغتك الخاصة.

مبادئ SOLID:

  1. Single responsibility
  2. Open-Closed
  3. Liskov substitution
  4. Interface segregation
  5. Dependency inversion

مبدأ Interface Segregation.

“لا يجب اجبار Class معين على تطبيق Interface به Methods لا يحتاج استخدماها في أداء مهامه”.
هذا المبدأ يشجعك على عمل Interfaces محددة ومختارة بدقه لأداء مهمه محددة، افترض ان لديك Class به العديد من ال Methods وجزء معين من تطبيقك يحتاج لاستخدام مجموعه من هذه ال Methods وجزء اخر من تطبيق يحتاج لاستخدام باقي ال Methods فان هذا المبدأ يشجعك على فصل هذا ال Interface وتقسيمة الى اثنين Interfaces اصغر كل واحد منهم به مجموعة من ال Methods على حسب حاجة تطبيقك.
“عدد من ال Interfaces الصغيرة التي تقوم بمهمه محددة افضل من Interface واحد كبير يقوم بمهام عديدة، وأي جزء من تطبيقك يجب ان لا يرى الا ال Methods التي يحتاجها فقط”

مثال توضيحي:

فلنفترض ان لدينا تطبيق ويجب على المستخدم ان يقوم بعمل تسجيل مستخدم جديد وتسجيل الدخول Register و Login لكى يستطيع استخدام التطبيق فربما يكون لدينا UserMembership Interface بيها دالة login و logout و register و forgetPasswrod كالتالي:

public interface UserMembership {
  User login(String username, String password);

  void logout(User user);

  void register(String username, String password, String phone);

  void forgetPassword(String username);
}

بعد تسجيل الدخول ربما تحتاج ان تعرض بيانات المستخدم والسماح له بعمل تسجيل الخروج logout وسيكون لديك صفحة أخرى لعمل تسجيل مستخدم جديد وصفحة لاستعادة كلمة المرور اذا نسيها المستخدم فبدلا من استخدام هذا ال interface الكبير UserMembership في جميع الصفحات وعمل override لجميع الدوال به في جميع الصفحات تقوم بفصلهم في interfaces صغيرة كالتالي:

public interface Login {
  User login(String username, String password);

  void logout(User user);
}

هنا قمنا بعمل interface Login يقوم بدور محدد وهو عمل تسجيل الدخول وتسجيل الخروج فقط الصفحة التي تقوم بعمل login وlogout تستخدم هذا ال interface فقط دون الحاجة الى عمل override لدالة register او forgetPassword.

public interface Register {
  void register(String username, String password, String phone);
}

هنا قمنا بعمل Register interface يقوم بدور محدد وهو تسجيل مستخدم جديد فقط الصفحة التي تقوم بتسجيل مستخدم جديد تستخدم هذا ال interface دون الحاجة لعمل override لدالة login او logout او forgetPassword.

public interface ForgetPassword {
  void forgetPassword(String username);
}

هنا قمنا بعمل ForgetPasword يقوم بدور محدد وهو استعادة كلمة المرور فقط الصفحة التي تقوم باستعادة كلمة المرور تستخدم هذا ال interface دون الحاجه لعمل override لدالة login او logout او register.

بذلك نكون حققنا المبدأ وفصلنا ال interface الكبير الى عدة interfaces اصغر وتقوم بدور محدد.

اليك مثال اخر يوضح مبدأ Interface Segregation:

اذا افترضنا ان لدينا تطبيق يقوم بإرسال ملف للطباعة وعمل scan لملف ورقى لاستخدامه داخل التطبيق ويقوم بإرسال ملف الى عنوان بريد الكتروني معين، قد تقوم بعمل Printer interface يقوم بهذه المهام الثلاثة كالتالي:

public interface Printer {
  void print(File file);

  Bitmap scan();

  void sendTo(File file, String email);
}

وتقوم بعمل كلاس FullPrinter يقوم بتطبيق Printer interface يمثل طابعة تقوم بعمل كل شيء كالتالي:

public class FullPrinter implements Printer {
  @Override public void print(File file) {
    // TODO: implement print logic 
  }

  @Override public Bitmap scan() {
    // TODO: implement scan logic
    return null;
  }

  @Override public void sendTo(File file, String email) {
    // TODO: implement send to email logic
  }
}

 ثم طلب منك ان تقوم بعمل نفس البرنامج يعمل على هاتف ذكى Smart phone حيث يستطيع الهاتف بعمل ()scan و ()sendTo لكنه لن يستطيع القيام بمهمة الطباعة بالتالي فانت ستقوم بعمل كلاس جديد اسمه MobileDevice يطبق Printer interface أيضا كالتالي:

public class MobileDevice implements Printer {
  @Override public void print(File file) {
    throw new UnsupportedOperationException(“Print operation not supported int MobileDevice”);
  }

  @Override public Bitmap scan() {
    // TODO: implement scan logic
    return null;
  }

  @Override public void sendTo(File file, String email) {
    // TODO: implement send to email logic
  }
}

يقوم كلاس MobileDevice بنفس مهام الطابعة الكاملة ما عدا خاصية الطباعة نفسها لذلك في دالة ()print قمنا بتمرير خطا UnsupportedOperationException حيث ان هذه العملية ليست مدعومة في الأجهزة المحمولة.

انت هنا قد أجبرت كلاس MobileDevice على عمل override لدالة ()print وهى غير مدعومة ولن يتم استخدامها وهذا مخالف مبدأ Interface Segregation والان تدرك انه يجب عليك فصل Printer interface الى عدة interfaces اصغر كالتالي:

public interface Printer {
  void print(File file);
}

هنا قمنا بتعديل Printer interface لتحتوي على دالة ()print فقط.

public interface Scanner {
  Bitmap scan();

  void sendTo(File file, String email);
}

هنا قمنا بإضافة interface جديد اسمه Scanner به دالة ()scan و ()sentTo فقط.

وسنقوم بتعديل كلاس FullPrinter وجعلناه يطبق Printer و Scanner كالتالي:

public class FullPrinter implements Printer, Scanner {
  @Override public void print(File file) {
    // TODO: implement print logic
  }

  @Override public Bitmap scan() {
    // TODO: implement scan logic
    return null;
  }

  @Override public void sendTo(File file, String email) {
    // TODO: implement send to email logic
  }
}

وسنقوم بتعديل كلاس MobileDevice بحيث يطبق Scanner interface فقط ولا يطبق Printer لأنه لا يحتاج الى دالة ()print كالتالي:

public class MobileDevice implements Scanner {
  @Override public Bitmap scan() {
    // TODO: implement scan logic
    return null;
  }

  @Override public void sendTo(File file, String email) {
    // TODO: implement send to email logic
  }
}

وبذلك نكون حققنا مبدأ Interface Segregation حيث قمنا بفصل ال interface الكبير وقسمناه الى عدة interfaces اصغر تقوم بأدوار محددة وأيضا لم نقم بإجبار كلاس MobileDevice ان يطبق دالة ()print دون استخدامها.

يمكننا انا نطلق على هذه ال interfaces الصغير المحددة الأدوار ب Role interfaces.

محمد هنيدى يمثل معاناة الجونيور فى كتابة كود نظيف.

مبدأ Dependency inversion.

قبل البدء في شرح هذا المبدأ لابد ان تعرف أولا ما معنى High-Level modules و Low-Level modules (وحدات مستوى اعلى ووحدات مستوى اقل) والوحدة هنا تعنى Class او Method، فاذا كان لديك كلاس Engine يمثل محرك سيارة وكلاس Car يمثل السيارة والسيارة تعتمد على المحرك أي ان كلاس Car يستخدم كلاس Engine ويعتمد عليه. فإننا نصف كلاس Car بأنه High-Level او مستوى اعلى ونصف كلاس Engine بأنه Low-Level او مستوى ادنى.

المبدأ ينقسم الى جزئين، الجزء الأول: وحدات المستوى الأعلى يجب الا تعتمد على وحدات المستوى الأدنى ولكن كلاهما يجب ان يعتمد على التجريد Abstractions.

الجزء الثاني: التجريد Abstractions لا يعتمد على التفاصيل، ولكن التفاصيل هي من يجب ان تعتمد على التجريد.

التجريد هنا ممكن ان يكون interface او abstract class.

مثال توضيحي:

فلنفترض اننا نقوم بعمل برنامج يحاكى فريق برمجي ينفذ يستطيع ان ينفذ برنامج معين فنحن كإدارة يكون لدينا مطور باك اند BackEnd Developer يكتب بلغة جافا ومطور فرونت اند FrontEnd Developer يكتب بلغة جافا سكريبت ولدينا برنامج يعمل هؤلاء المطورين فيه لتنفيذه كالتالي:

public class BackEndDeveloper {
    public void writeJava() {
        // TODO: implementation of writing java code here 
    }
}

هنا قمنا بعمل كلاس BackEndDeveloper يمثل مطور الباك اند ويحتوى على دالة واحد ()writeJava.

public class FrontEndDeveloper {
    public void writeJavascript() {
        // TODO: implementation of writing javascript code here
    }
}

هنا قمنا بعمل كلاس FrontEndDeveloper يمثل مطور الفرونت اند ويحتوى على دالة واحدة ()writeJavascript.

public class Project {

    private BackEndDeveloper backEndDeveloper = new BackEndDeveloper();
    private FrontEndDeveloper frontEndDeveloper = new FrontEndDeveloper();

    public void implement() {
        backEndDeveloper.writeJava();
        frontEndDeveloper.writeJavascript();
    }
}

هنا قمنا بعمل كلاس Project وهو المشروع الذى يجب تنفيذه وقمنا بداخله بإنشاء اوبجيكت جديد من كلاس BackEndDeveloper واوبجيكت جديد من كلاس FrontEndDeveloper وبداخل دالة التنفيذ ()implement قمنا بنداء دالة ()writeJava من كلاس مطور الباك اند ودالة ()writeJavascript من كلاس مطور الفرونت اند.

اننا هنا خالفنا مبدأ Dependency Inversion بشقيه الشق الأول: كلاس Project يعتبر مستوى اعلى وكلاس BackEndDeveloper و FrontEndDeveloper يعتبر كل منهما مستوى ادنى حيث كلاس Project يستخدم كلاهما في أداء مهامه ويعتمد عليهم والقاعدة تقول المستوى الأعلى يجب الا يعتمد على المستوى الأدنى.
المشكلة هنا يا صديق اننا اذا اردنا ان نضيف مطور اخر او ان نزيد عدد المطورين سنقوم بإنشاء اوبجيكت جديد لكل مطور إضافي داخل كلاس Project.

قمنا أيضا بمخالفة الشق الثاني: اذا نظرنا الى دالة ()implement فإننا سنجد ان دالة ()writeJava و ()writeJavascript كل منهما تقابل اوبجيكت مطور معين وهذه تعتبر التفاصيل التي لا يجب ان يعرفها كلاس Project.

المشكلة هنا اذا اتفقت الادارة ان مطور الباك اند سيكتب بلغة php بدلا من Java فانك ستضطر لتعديل كلاس Project لأنه يعتمد على التفاصيل.

دعنا نرى كيفية تطبيق مبدأ Dependency Inversion لحل هذه المشاكل.

سنقوم أولا بإضافة طبقة من التجريد عن طريق انشاء interface جديد يدعى Developer وبه دالة واحدة ()develop.

public interface Developer {
    void develop();
}

وسنقوم بتعديل كلاس BackEndDeveloper لكى يطبق Developer interface ونقوم بعملoverride  لدالة ()develop ونجعلها تنادى دالة ()writeJava فيسهل علينا فيما بعد تغيير اللغة التي يكتب بها المطور او إضافة لغات أخرى دون الحاجة للتغير في كلاسات المستوى الأعلى كالتالي.

public class BackEndDeveloper implements Developer {

    @Override
    public void develop() {
        writeJava();
    }

    public void writeJava() {
        // TODO: implementation of writing java code here
    }
}

وسنقوم بعمل نفس الشيء مع كلاس FrontEndDeveloper حيث يطبق Developer interface ويقوم بعمل override لدالة ()develop ونجعلها تنادى اللغة التي يكتب بها المطور ()writeJavascript ويسهل علينا تغييرها لاحقا كالتالي:

public class FrontEndDeveloper implements Developer {

    @Override
    public void develop() {
        writeJavascript();
    }

    public void writeJavascript() {
        // TODO: implementation of writing javascript code here
    }
}

ثم نقوم بتعديل كلاس Project لكى لا يكون معتمد على كلاسات أخرى بشكل مباشر كالتالي:

public class Project {

    private List<Developer> developers;

    public Project(List<Developer> developers) {
        this.developers = developers;
    }

    public void implement() {
        for (Developer developer : developers) {
            developer.develop();
        }
    }
}

هنا قمنا بعمل تمرير قائمة من Developer interface المجرد عبر ال constructor في كلاس Project سميناها developers.
وفى دالة تنفيذ البرنامج ()implement قمنا بعمل loop على قائمة المطورين developers وقمنا بنداء دالة ()develop لكل مطور وبذلك نكون قد حققنا مبدأ Dependency Inversion بشقيه فكلاس Project هنا لا يعتمد على كلاسات المستوى الأدنى بشكل مباشر ولكن يعتمد على التجريد فلن يتأثر اذا قمنا تغيير عدد المطورين او نوعهم، وأيضا كلاسات المستوى الأدنى BackEndDeveloper و FrontEndDeveloper تفاصيلهم تعتمد على التجريد حيث ان أي مطور يمكن ان يكتب أي لغة او يكتب عدة لغات وهذا لن يؤثر على كلاس المستوى الاعلى Project.

الملخص وفائدة مبادئ SOLID

ان مبادئ SOLID تساعدك على بناء برامج مستقرة وقابلة للتطوير والاضافة والاختبار Testing وتجنبك مشاكل كثيرة قد تحدث عند التعديل او الإضافة على برنامجك، لقد تم اختبار هذه المبادئ منذ عشرات السنين على يد مبرمجين خبراء فيمكنك الوثوق بهذه المبادئ.

جميع هذه المبادئ (SOLID) تمكنك من كتابة كود نظيف Clean Code وتطبيق افضل الممارسات البرمجية Best Practices اثناء عمل برامجك.

انت ربما تقول في نفسك لماذا احتاج الى Clean Code او مبادئ SOLID يمكنني كتابة الكود كله في مكان واحد بشكل عشوائي ويقوم بتنفيذ نفس المطلوب، والاجابة انك يجب ان تضع نفسك مكان صاحب العمل في كل مرة يطلب من المطور عمل تعديل معين يجد التطبيق به مشكلة أخرى في صفحة مختلفة وهكذا باستمرار فانه لن يثق في هذا المطور وربما يبحث عن مطور اخر أكثر خبرة.

تخيل معي انك ذهبت الى ورشة اصلاح السيارات لإصلاح الزجاج الكهربائي في سيارتك وعند استلام السيارة وجدت الزجاج الكهربائي يعمل بكفاءة لكن السيارة لا تعمل فأنت على الاغلب لن تتعامل مع هذه الورشة مرة أخرى، تذكر دائما ان Clean Code يعنى بيزنس افضل.

وفى النهاية طبعا نوجه كل الشكر والتقدير ل Uncle Bob راعى ال Clean Code في العالم.

تحية ل Uncle BOb.

لا تتردد فى السؤال والاستفسار على الجروب الخاص بنا

الكاتب: Muhammad Helmi

Senior Android Developer love coding and excited to share my knowledge with others, Interested in Technology, Marketing, History, Movies And TV Shows.

(9) تعليقات

  1. جزاك الله خيرا يا هندسه شكرا علي المجهود العظيم الي حضرتك بذلته في توصيل المعلومات بشكل سلس
    معلش ممكن لينك الجروب يا بشمهندس

اترك ردا