JavaRush /Java Blog /Random-TL /SOLID na mga prinsipyo na gagawing mas malinis ang iyong ...
Paul Soia
Antas
Kiyv

SOLID na mga prinsipyo na gagawing mas malinis ang iyong code

Nai-publish sa grupo
Ano ang SOLID? Narito ang ibig sabihin ng SOLID acronym: - S: Single  Responsibility Principle. - O: Open-Closed na Prinsipyo  . - L: Prinsipyo ng Pagpapalit ng Liskov  . - I: Prinsipyo ng Interface Segregation  . - D: Dependency Inversion Principle  . Ang mga SOLID na prinsipyo ay nagpapayo kung paano magdisenyo ng mga module, i.e. mga brick kung saan itinayo ang application. Ang layunin ng mga prinsipyo ay magdisenyo ng mga module na: - nagtataguyod ng pagbabago - ay madaling maunawaan - ay magagamit muli Kaya, tingnan natin kung ano ang mga ito gamit ang mga halimbawa.
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())
        }
    }
}
Ano ang nakikita natin sa halimbawang ito? Ang FirebaseAuth ay ipinasa sa constructor, at ginagamit namin ito upang subukang mag-log in. Kung nakatanggap kami ng error bilang tugon, isinusulat namin ang mensahe sa isang file. Ngayon tingnan natin kung ano ang mali sa code na ito. 1. S: Single Responsibility Prinsipyo  : Ang isang klase (o function/pamamaraan) ay dapat na responsable para sa isang bagay lamang. Kung ang isang klase ay may pananagutan sa paglutas ng ilang mga problema, ang mga subsystem nito na nagpapatupad ng solusyon sa mga problemang ito ay konektado sa isa't isa. Ang mga pagbabago sa isang naturang subsystem ay humahantong sa mga pagbabago sa isa pa. Sa aming halimbawa, ang loginUser function ay may pananagutan para sa dalawang bagay - pag-login at pagsusulat ng error sa isang file. Upang magkaroon ng isang responsibilidad, ang lohika para sa pagtatala ng isang error ay dapat ilagay sa isang hiwalay na klase.
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())
        }
    }
}
Ang lohika ng pag-record ng error ay inilipat sa isang hiwalay na klase at ngayon ang paraan ng pag-loginUser ay mayroon lamang isang responsibilidad - awtorisasyon. Kung kailangang baguhin ang logic sa pag-log ng error, gagawin natin ito sa klase ng FileLogger, at hindi sa function ng loginUser 2. O: Open-Closed  Principle: Dapat na bukas ang mga entity ng software (mga klase, module, function) para sa extension , ngunit hindi para sa pagbabago. Sa kontekstong ito, ang pagiging bukas sa extension ay ang kakayahang magdagdag ng bagong gawi sa isang klase, module, o function kung kinakailangan, at ang pagsasara sa pagbabago ay ang pagbabawal sa pagbabago ng source code ng mga software entity. Sa unang tingin, ito ay mukhang kumplikado at kontradiksyon. Ngunit kung titingnan mo, ang prinsipyo ay medyo lohikal. Ang pagsunod sa prinsipyo ng OCP ay ang software ay binago hindi sa pamamagitan ng pagbabago ng umiiral na code, ngunit sa pamamagitan ng pagdaragdag ng bagong code. Iyon ay, ang orihinal na nilikha na code ay nananatiling "buo" at matatag, at ang bagong pag-andar ay ipinakilala alinman sa pamamagitan ng pagpapatupad ng mana o sa pamamagitan ng paggamit ng mga abstract na interface at polymorphism. Kaya tingnan natin ang klase ng FileLogger. Kung kailangan naming magsulat ng mga log sa isa pang file, maaari naming baguhin ang pangalan:
class FileLogger{
    fun logError(error: String) {
        val file = File("errors2.txt")
        file.appendText(text = error)
    }
}
Ngunit pagkatapos ang lahat ng mga log ay isusulat sa isang bagong file, na, malamang, hindi namin kailangan. Ngunit paano natin mababago ang ating logger nang hindi binabago ang klase mismo?
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)
    }

}
Minarkahan namin ang klase ng FileLogger at ang logError function bilang bukas, lumikha ng bagong klase ng CustomErrorFileLogger at magsulat ng bagong pagpapatupad ng pag-log. Bilang resulta, ang aming klase ay magagamit para sa pagpapalawak ng pag-andar, ngunit sarado para sa pagbabago. 3. L: Liskov Substitution Principle  . Kinakailangan na ang mga subclass ay maaaring magsilbi bilang mga kapalit para sa kanilang mga superclass. Ang layunin ng prinsipyong ito ay ang mga descendant classes ay maaaring gamitin bilang kapalit ng parent classes kung saan sila ay hinango nang hindi sinisira ang program. Kung lumalabas na sinusuri ng code ang uri ng isang klase, nilalabag ang prinsipyo ng pagpapalit. Kung isinulat namin ang kapalit na klase tulad nito:
class CustomErrorFileLogger : FileLogger() {

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

}
At ngayon, palitan natin ang klase ng FileLogger ng CustomErrorFileLogger sa repositoryo
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())
        }
    }

}
Sa kasong ito, tatawagin ang logError mula sa parent class, o kailangan mong baguhin ang tawag sa fileLogger.logError(e.message.toString()) sa fileLogger.customErrorLog(e.message.toString()) 4. I : Prinsipyo ng Interface Segregation . Lumikha ng lubos na dalubhasang mga interface na idinisenyo para sa isang partikular na kliyente. Ang mga kliyente ay hindi dapat umasa sa mga interface na hindi nila ginagamit. Mukhang kumplikado, ngunit ito ay talagang napaka-simple. Halimbawa, gawin nating interface ang klase ng FileLogger, ngunit magdagdag ng isa pang function dito:
interface FileLogger{

    fun printLogs()

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

}
Ngayon ang lahat ng mga inapo ay kakailanganing ipatupad ang printLogs function, kahit na hindi natin ito kailangan sa lahat ng mga descendant na klase.
class CustomErrorFileLogger : FileLogger{

    override fun printLog() {

    }

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

}
at ngayon magkakaroon tayo ng mga walang laman na function, na masama para sa kalinisan ng code. Sa halip, maaari tayong gumawa ng default na halaga sa interface at pagkatapos ay i-override ang function sa mga klase lamang kung saan ito kinakailangan:
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)
    }

}
Ngayon ang mga klase na magpapatupad ng interface ng FileLogger ay magiging mas malinis. 5. D: Dependency Inversion Principle  . Ang object ng dependency ay dapat na isang abstraction, hindi isang bagay na konkreto. Bumalik tayo sa ating pangunahing klase:
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())
        }
    }

}
Mukhang na-configure at naayos na namin ang lahat dito. Ngunit may isa pang punto na kailangang baguhin. Ito ay gumagamit ng FirebaseAuth class. Ano ang mangyayari kung sa isang punto kailangan nating baguhin ang pahintulot at mag-log in hindi sa pamamagitan ng Firebase, ngunit, halimbawa, gamit ang ilang uri ng kahilingan sa API? Pagkatapos ay kailangan nating baguhin ang maraming bagay, at hindi natin gugustuhin iyon. Upang gawin ito, lumikha ng isang interface na may function na signInWithEmailAndPassword(email: String, password: String):
interface Authenticator{
    fun signInWithEmailAndPassword(email: String, password: String)
}
Ang interface na ito ay ang aming abstraction. At ngayon gumawa kami ng mga partikular na pagpapatupad ng pag-login
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) {
        //другой способ логина
    }

}
At sa klase ng `MainRepository` wala na ngayong pagdepende sa partikular na pagpapatupad, ngunit sa abstraction lamang
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())
        }
    }

}
At ngayon, upang baguhin ang paraan ng awtorisasyon, kailangan nating baguhin lamang ang isang linya sa klase ng module.
Mga komento
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION