JavaRush /Blog Java /Random-PL /SOLIDNE zasady, które sprawią, że Twój kod będzie czystsz...
Paul Soia
Poziom 26
Kiyv

SOLIDNE zasady, które sprawią, że Twój kod będzie czystszy

Opublikowano w grupie Random-PL
Co to jest SOLIDNY? Oto, co oznacza akronim SOLID: - S: Zasada pojedynczej  odpowiedzialności. - O: Zasada otwarte-zamknięte  . - L: Zasada podstawienia Liskowa  . - I: Zasada segregacji interfejsów  . - D: Zasada inwersji zależności  . Zasady SOLID podpowiadają, jak projektować moduły, tj. cegły, z których zbudowana jest aplikacja. Celem tych zasad jest zaprojektowanie modułów, które: - promują zmiany - są łatwe do zrozumienia - nadają się do ponownego użycia. Przyjrzyjmy się zatem, czym one są na przykładach.
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())
        }
    }
}
Co widzimy w tym przykładzie? W konstruktorze przekazujemy FirebaseAuth, którego używamy przy próbie zalogowania. Jeśli w odpowiedzi otrzymamy błąd, zapisujemy wiadomość do pliku. Zobaczmy teraz, co jest nie tak z tym kodem. 1. S: Zasada pojedynczej odpowiedzialności  : Klasa (lub funkcja/metoda) powinna być odpowiedzialna tylko za jedną rzecz. Jeśli klasa jest odpowiedzialna za rozwiązanie kilku problemów, jej podsystemy realizujące rozwiązanie tych problemów są ze sobą połączone. Zmiany w jednym takim podsystemie pociągają za sobą zmiany w innym. W naszym przykładzie funkcja loginUser odpowiada za dwie rzeczy - logowanie i zapisanie błędu do pliku. Aby odpowiedzialność była jedna, logika rejestrowania błędu musi być umieszczona w osobnej klasie.
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())
        }
    }
}
Logika rejestracji błędów została przeniesiona do osobnej klasy i obecnie metoda loginUser ma tylko jedną odpowiedzialność – autoryzację. Jeśli zajdzie potrzeba zmiany logiki rejestrowania błędów, zrobimy to w klasie FileLogger, a nie w funkcji loginUser 2. O: Zasada Open-Closed  : Jednostki oprogramowania (klasy, moduły, funkcje) muszą być otwarte na rozszerzenie, ale nie do modyfikacji. W tym kontekście otwartość na rozszerzenie to możliwość dodania nowego zachowania do klasy, modułu lub funkcji, jeśli zajdzie taka potrzeba, a zamknięcie na zmiany to zakaz zmiany kodu źródłowego jednostek oprogramowania. Na pierwszy rzut oka wydaje się to skomplikowane i sprzeczne. Ale jeśli się temu przyjrzeć, zasada jest całkiem logiczna. Kierując się zasadą OCP, oprogramowanie zmienia się nie poprzez zmianę istniejącego kodu, ale poprzez dodanie nowego kodu. Oznacza to, że pierwotnie utworzony kod pozostaje „nienaruszony” i stabilny, a nowa funkcjonalność jest wprowadzana albo poprzez dziedziczenie implementacji, albo poprzez zastosowanie abstrakcyjnych interfejsów i polimorfizmu. Przyjrzyjmy się więc klasie FileLogger. Jeśli potrzebujemy zapisać logi do innego pliku, możemy zmienić nazwę:
class FileLogger{
    fun logError(error: String) {
        val file = File("errors2.txt")
        file.appendText(text = error)
    }
}
Ale wtedy wszystkie dzienniki zostaną zapisane w nowym pliku, którego najprawdopodobniej nie potrzebujemy. Ale jak możemy zmienić nasz rejestrator bez zmiany samej klasy?
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)
    }

}
Zaznaczamy klasę FileLogger i funkcję logError jako otwarte, tworzymy nową klasę CustomErrorFileLogger i piszemy nową implementację logowania. W rezultacie nasza klasa jest dostępna do rozszerzania funkcjonalności, ale jest zamknięta na modyfikacje. 3. L: Zasada podstawienia Liskowa  . Konieczne jest, aby podklasy mogły służyć jako zamienniki swoich nadklas. Celem tej zasady jest to, że klas potomnych można używać zamiast klas nadrzędnych, z których pochodzą, bez przerywania programu. Jeśli okaże się, że kod sprawdza typ klasy, to zostaje naruszona zasada podstawienia. Jeśli napiszemy klasę następczą w ten sposób:
class CustomErrorFileLogger : FileLogger() {

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

}
A teraz zamieńmy w repozytorium klasę FileLogger na 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())
        }
    }

}
W tym przypadku z klasy nadrzędnej zostanie wywołana logError lub będziesz musiał zmienić wywołanie na fileLogger.logError(e.message.toString()) na fileLogger.customErrorLog(e.message.toString()) 4. I : Zasada segregacji interfejsów . Twórz wysoce wyspecjalizowane interfejsy przeznaczone dla konkretnego klienta. Klienci nie powinni polegać na interfejsach, z których nie korzystają. Brzmi skomplikowanie, ale w rzeczywistości jest bardzo proste. Na przykład uczyńmy klasę FileLogger interfejsem, ale dodajmy do niej jeszcze jedną funkcję:
interface FileLogger{

    fun printLogs()

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

}
Teraz wszyscy potomkowie będą musieli zaimplementować funkcję printLogs, nawet jeśli nie potrzebujemy jej we wszystkich klasach potomnych.
class CustomErrorFileLogger : FileLogger{

    override fun printLog() {

    }

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

}
i teraz będziemy mieli puste funkcje, co niekorzystnie wpływa na czystość kodu. Zamiast tego możemy ustawić w interfejsie wartość domyślną, a następnie nadpisać funkcję tylko w tych klasach, w których jest ona potrzebna:
interface FileLogger{

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

    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)
    }

}
Teraz klasy, które będą implementować interfejs FileLogger, będą czystsze. 5. D: Zasada inwersji zależności  . Przedmiotem zależności powinna być abstrakcja, a nie coś konkretnego. Wróćmy do naszej głównej klasy:
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())
        }
    }

}
Wygląda na to, że wszystko tutaj już skonfigurowaliśmy i naprawiliśmy. Ale jest jeszcze jeden punkt, który należy zmienić. Używa to klasy FirebaseAuth. Co się stanie, jeśli w pewnym momencie będziemy musieli zmienić autoryzację i zalogować się nie przez Firebase, ale np. za pomocą jakiegoś żądania API? Wtedy będziemy musieli zmienić wiele rzeczy, a tego byśmy nie chcieli. W tym celu utwórz interfejs z funkcją SignInWithEmailAndPassword(email: String, hasło: String):
interface Authenticator{
    fun signInWithEmailAndPassword(email: String, password: String)
}
Ten interfejs jest naszą abstrakcją. A teraz dokonujemy konkretnych implementacji loginu
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) {
        //другой способ логина
    }

}
A w klasie `MainRepository` nie ma teraz zależności od konkretnej implementacji, a jedynie od abstrakcji
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())
        }
    }

}
A teraz, aby zmienić metodę autoryzacji, musimy zmienić tylko jedną linię w klasie modułu.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION