ما هو الصلبة؟ إليك ما يرمز إليه اختصار SOLID: - S:  مبدأ المسؤولية الفردية . - س: مبدأ مفتوح ومغلق  . - ل : مبدأ استبدال ليسكوف  . - الأول: مبدأ فصل الواجهة  . - د : مبدأ انعكاس التبعية  . تنصح مبادئ SOLID بكيفية تصميم الوحدات، على سبيل المثال. الطوب الذي تم بناء التطبيق منه. الغرض من هذه المبادئ هو تصميم الوحدات التي: - تعزز التغيير - يسهل فهمها - قابلة لإعادة الاستخدام. لذلك، دعونا ننظر إلى ماهيتها من خلال الأمثلة.
class MainRepository(
    private val auth: FirebaseAuth
) {
    suspend fun loginUser(email: String, password: String) {
        try {
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            val file = File("errors.txt")
            file.appendText(text = e.message.toString())
        }
    }
}
ماذا نرى في هذا المثال؟ يتم تمرير FirebaseAuth في المُنشئ، ونستخدمه لمحاولة تسجيل الدخول. إذا تلقينا خطأ في الرد، نكتب الرسالة إلى ملف. الآن دعونا نرى ما هو الخطأ في هذا الرمز. 1. S: مبدأ المسؤولية الفردية  : يجب أن تكون الفئة (أو الوظيفة/الطريقة) مسؤولة عن شيء واحد فقط. إذا كان الفصل مسؤولاً عن حل العديد من المشكلات، فإن أنظمته الفرعية التي تنفذ حل هذه المشكلات تكون مرتبطة ببعضها البعض. تؤدي التغييرات في أحد هذه الأنظمة الفرعية إلى تغييرات في نظام فرعي آخر. في مثالنا، تكون وظيفة تسجيل الدخول للمستخدم مسؤولة عن شيئين: تسجيل الدخول وكتابة خطأ في الملف. لكي تكون هناك مسؤولية واحدة، يجب وضع منطق تسجيل الخطأ في فئة منفصلة.
class FileLogger{
    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }
}
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: FileLogger,
) {
    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }
}
تم نقل منطق تسجيل الأخطاء إلى فئة منفصلة والآن تتحمل طريقة تسجيل الدخول للمستخدم مسؤولية واحدة فقط - التفويض. إذا كانت هناك حاجة إلى تغيير منطق تسجيل الأخطاء، فسنفعل ذلك في فئة FileLogger، وليس في وظيفة تسجيل الدخولUser 2. O: مبدأ مفتوح ومغلق  : يجب أن تكون كيانات البرامج (الفئات والوحدات والوظائف) مفتوحة للامتداد، ولكن ليس للتعديل. في هذا السياق، الانفتاح على الامتداد هو القدرة على إضافة سلوك جديد إلى فئة أو وحدة نمطية أو وظيفة إذا دعت الحاجة إلى ذلك، والانفتاح على التغيير هو حظر تغيير الكود المصدري للكيانات البرمجية. للوهلة الأولى، يبدو هذا معقدًا ومتناقضًا. ولكن إذا نظرت إليها، فإن المبدأ منطقي تماما. اتباع مبدأ OCP هو أن البرنامج لا يتغير عن طريق تغيير التعليمات البرمجية الموجودة، ولكن عن طريق إضافة تعليمات برمجية جديدة. وهذا يعني أن الكود الذي تم إنشاؤه في الأصل يظل "دون تغيير" ومستقرًا، ويتم تقديم وظائف جديدة إما من خلال وراثة التنفيذ أو من خلال استخدام واجهات مجردة وتعدد الأشكال. لذلك دعونا نلقي نظرة على فئة FileLogger. إذا أردنا كتابة سجلات لملف آخر، يمكننا تغيير الاسم:
class FileLogger{
    fun logError(error: String) {
        val file = File("errors2.txt")
        file.appendText(text = error)
    }
}
ولكن بعد ذلك سيتم كتابة كافة السجلات إلى ملف جديد، والذي، على الأرجح، لا نحتاج إليه. ولكن كيف يمكننا تغيير المُسجل الخاص بنا دون تغيير الفئة نفسها؟
open class FileLogger{

    open fun logError(error: String) {
        val file = File("error.txt")
        file.appendText(text = error)
    }

}

class CustomErrorFileLogger : FileLogger() {

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
نضع علامة على فئة FileLogger ووظيفة logError على أنهما مفتوحتان، وننشئ فئة CustomErrorFileLogger جديدة ونكتب تطبيق تسجيل جديد. ونتيجة لذلك، فإن فئتنا متاحة لتوسيع الوظائف، ولكنها مغلقة للتعديل. 3. ل: مبدأ استبدال ليسكوف  . من الضروري أن تكون الفئات الفرعية بمثابة بدائل لفئاتها الفائقة. الغرض من هذا المبدأ هو أنه يمكن استخدام الفئات التابعة بدلاً من الفئات الأصلية التي اشتقت منها دون كسر البرنامج. إذا اتضح أن الكود يتحقق من نوع الفصل، فسيتم انتهاك مبدأ الاستبدال. إذا كتبنا الطبقة اللاحقة مثل هذا:
class CustomErrorFileLogger : FileLogger() {

    fun customErrorLog(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
والآن دعونا نستبدل فئة FileLogger بـ CustomErrorFileLogger في المستودع
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: CustomErrorFileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        }catch(e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
في هذه الحالة، سيتم استدعاء logError من الفئة الأصلية، أو سيتعين عليك تغيير الاستدعاء من fileLogger.logError(e.message.toString()) إلى fileLogger.customErrorLog(e.message.toString()) 4. : مبدأ فصل الواجهة . إنشاء واجهات متخصصة للغاية مصممة لعميل معين. يجب ألا يعتمد العملاء على واجهات لا يستخدمونها. يبدو الأمر معقدًا، لكنه في الواقع بسيط جدًا. على سبيل المثال، دعونا نجعل فئة FileLogger واجهة، ولكن نضيف إليها وظيفة أخرى:
interface FileLogger{

    fun printLogs()

    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }

}
الآن سيُطلب من جميع الفئات التابعة تنفيذ وظيفة printLogs، حتى لو لم نكن بحاجة إليها في جميع الفئات التابعة.
class CustomErrorFileLogger : FileLogger{

    override fun printLog() {

    }

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
والآن سيكون لدينا وظائف فارغة، وهو أمر سيء بالنسبة لنظافة الكود. بدلاً من ذلك، يمكننا إنشاء قيمة افتراضية في الواجهة ثم تجاوز الوظيفة فقط في تلك الفئات التي تكون مطلوبة فيها:
interface FileLogger{

    fun printLogs() {
        //Howая-то дефолтная реализация
    }

    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }

}
class CustomErrorFileLogger : FileLogger{

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
الآن ستصبح الفئات التي ستقوم بتنفيذ واجهة FileLogger أكثر نظافة. 5. د: مبدأ انعكاس التبعية  . يجب أن يكون موضوع التبعية تجريدًا، وليس شيئًا ملموسًا. دعنا نعود إلى صفنا الرئيسي:
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: FileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
يبدو أننا قمنا بالفعل بتكوين كل شيء وإصلاحه هنا. ولكن لا تزال هناك نقطة أخرى تحتاج إلى تغيير. هذا يستخدم فئة FirebaseAuth. ماذا سيحدث إذا احتجنا في مرحلة ما إلى تغيير التفويض وتسجيل الدخول ليس من خلال Firebase، ولكن، على سبيل المثال، باستخدام نوع ما من طلبات API؟ ثم سيتعين علينا تغيير الكثير من الأشياء، ونحن لا نريد ذلك. للقيام بذلك، قم بإنشاء واجهة باستخدام الوظيفة SignInWithEmailAndPassword(email: String,password: String):
interface Authenticator{
    fun signInWithEmailAndPassword(email: String, password: String)
}
هذه الواجهة هي تجريدنا. والآن نقوم بتنفيذ تطبيقات محددة لتسجيل الدخول
class FirebaseAuthenticator : Authenticator{

    override fun signInWithEmailAndPassword(email: String, password: String) {
        FirebaseAuth.getInstance().signInWithEmailAndPassword(email, password)
    }

}
class CustomApiAuthenticator : Authenticator{

    override fun signInWithEmailAndPassword(email: String, password: String) {
        //другой способ логина
    }

}
وفي فئة "MainRepository" لا يوجد الآن اعتماد على التنفيذ المحدد، ولكن فقط على التجريد
class MainRepository (
    private val auth: Authenticator,
    private val fileLogger: FileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        }catch(e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
والآن، لتغيير طريقة التفويض، نحتاج إلى تغيير سطر واحد فقط في فئة الوحدة النمطية.