16.10.20238 min

Hüseyin ÖzkoçAndroid DeveloperHuawei

Solid w Kotlinie

Czym są zasady SOLID i dlaczego programiści powinni z nich korzystać?

Solid w Kotlinie

Wielu programistów Kotlina nie posiada pełnej wiedzy na temat zasad SOLID, a nawet jeśli je znają, nie są świadomi, dlaczego są używane. Gotowy poznać wszystkie szczegóły?

Wprowadzenie

Witajcie drodzy miłośnicy Kotlina! Witajcie w moim nowym artykule. Dzisiaj opowiem Wam o zasadach SOLID w Kotlinie. Przede wszystkim na przykładach wyjaśnię czym są zasady SOLID i do czego służą.

Czym są zasady SOLID?

SOLID to akronim pięciu zasad projektowania, które pomagają tworzyć łatwe w utrzymaniu, skalowalne i niezawodne oprogramowania. Robert C. Martin wprowadził te zasady, aby pomóc programistom w tworzeniu wysokiej jakości kodu. Chociaż początkowo SOLID był przeznaczony do programowania obiektowego, ma on również zastosowanie do innych języków, takich jak Kotlin. Zasady te mają na celu promowanie czystego kodu i poprawę projektowania oprogramowania. Zasady SOLID:

  1. Zasada pojedynczej odpowiedzialności
  2. Zasada otwarte - zamknięte
  3. Zasada podstawiania Liskov
  4. Zasada segregacji interfejsu
  5. Zasada odwracania zależności

Jeśli jesteś gotowy, przyjrzyjmy się szczegółowo tym zasadom wraz z przykładami prawidłowego użycia i naruszeń.

Zasada pojedynczej odpowiedzialności

Zasada pojedynczej odpowiedzialności jest jedną z zasad programowania SOLID w programowaniu obiektowym. Oznacza to, że dana klasa powinna mieć tylko jeden cel do zmiany. Innymi słowy, klasa powinna posiadać tylko jedną odpowiedzialność lub zadanie. Zasada ta jest przydatna w utrzymywaniu klas i funkcji, utrzymując je uporządkowane i łatwe do zrozumienia. Gdy klasa ma wiele obowiązków, mogą one nieumyślnie wpływać na inne zadania lub zadania tej klasy, powodując nieoczekiwane zachowanie, błędy i zwiększone koszty utrzymania.

Przyjrzyjmy się teraz sytuacji naruszenia jak i poprawnemu użyciu.

Naruszenie:

// Single Responsibility Principle Violation
// In this example the System class is trying to handle many different situation at the same time. 
// This approach can cause major problems in the future.
class SystemManager {
    fun addUser(user: User) { }
    fun deleteUser(user: User) { }
    fun sendNotification(notification:String) {}
    fun sendEmail(user: User, email: String) {}
}

W tym przykładzie klasa System próbuje obsłużyć wiele różnych sytuacji w tym samym miejscu. Takie podejście może spowodować poważne problemy w przyszłości.

Prawidłowe użycie:

// Single Responsibility Principle Correct Usage:
// As seen in this example, we divided our System class into specific parts
// And placed the functions in their respective classes.

class MailManager() {
    fun sendEmail(user: User, email: String) {}
}

class NotificationManager() {
    fun sendNotification(notification: String) {}
}

class UserManager {
    fun addUser(user: User) {}
    fun deleteUser(user: User) {}
}

Jak widać w tym przykładzie, podzieliliśmy naszą klasę System na określone części i umieściliśmy funkcje w odpowiednich klasach.

Zasada otwarte - zamknięte

Zasada otwarte - zamknięte to zasada w projektowaniu obiektowym, która mówi, że klasy, moduły, funkcje i inne jednostki oprogramowania powinny być otwarte na rozszerzenia, ale zamknięte na modyfikacje. Oznacza to, że można dodawać nowe rzeczy do klasy bez zmiany jej oryginalnego kodu. Zamiast więc zmieniać samą klasę, można napisać nowy kod, który wykorzystuje istniejącą klasę do dodawania nowych funkcji. Dzięki temu kod jest łatwiejszy w utrzymaniu i ponownym użyciu.

Przyjrzyjmy się teraz sytuacji naruszenia jak i poprawnemu użyciu.

Naruszenie:

// Open/Closed Principle Violation
// In this example, when we try to add something new to our class,
// we have to rewrite our existing code, which can cause problems later on.
class Shape(val type: String, val width: Double, val height: Double)

fun calculateArea(shape: Shape): Double {
    if (shape.type == "rectangle") {
        return shape.width * shape.height
    } else if (shape.type == "circle") {
        return Math.PI * shape.width * shape.width
    }
    return 0.0
}

W tym przykładzie, kiedy próbujemy dodać coś nowego do naszej klasy, musimy przepisać istniejący kod, co później może powodować trudności.

Prawidłowe użycie:

// Open/Closed Principle Correct Usage
// As in correct usage, instead of changing the class itself,
// we wrote new classes using our existing class 
// and implemented our functions under new classes.

interface Shape {
    fun area(): Double
}

class Rectangle(val width: Double, val height: Double) : Shape {
    override fun area() = width * height
}

class Circle(val radius: Double) : Shape {
    override fun area() = Math.PI * radius * radius
}

fun calculateArea(shape: Shape) = shape.area()

Zgodnie z prawidłowym użyciem, zamiast zmieniać samą klasę, napisaliśmy nowe klasy wykorzystujące naszą istniejącą klasę i zaimplementowaliśmy nasze funkcje w nowych klasach.

Zasada podstawiania Liskov

Zasada podstawiania Liskov jest ważną zasadą w programowaniu obiektowym. Mówi ona, że jeśli masz program, który działa z określonym typem obiektu, powinieneś być w stanie używać dowolnego podtypu tego obiektu bez większych problemów. Oznacza to, że wszystkie metody i właściwości w klasie głównej powinny również działać dla wszystkich podklas bez konieczności zmiany czegokolwiek.

Przyjrzyjmy się teraz sytuacji naruszenia jak i poprawnemu użyciu.

Naruszenie:

// Liskov Substitution Principle Violation:
// As we saw in this example, the method we wrote in our main class should work properly in its subclasses according to the Liskov principle, 
// but when our subclass inherited from our superclass, our fly method did not work as expected.

open class Bird {
    open fun fly() {}
}

class Penguin : Bird() {
    override fun fly() {
        print("Penguins can't fly!")
    }
}

Jak mogliśmy widzieć w tym przykładzie, metoda, którą napisaliśmy w naszej głównej klasie, powinna działać poprawnie w jej podklasach zgodnie z zasadą Liskova, ale kiedy nasza podklasa odziedziczyła po naszej superklasie, nasza metoda fly nie działała zgodnie z oczekiwaniami.

Prawidłowe użycie:

// Liskov Substitution Principle Correct Usage
// As you can see in this example, all the things we write in the superclass will be valid in the subclasses, 
// because we have implemented the method that is not valid for subclasses by creating an interface and implementing it where we need it.

open class Bird {
    // common bird methods and properties
}

interface IFlyingBird {
    fun fly(): Boolean
}

class Penguin : Bird() {
    // methods and properties specific to penguins
}

class Eagle : Bird(), IFlyingBird {
    override fun fly(): Boolean {
        return true
    }
}

 

Jak możemy zauważyć w tym przykładzie, wszystkie rzeczy, które piszemy w superklasie, będą ważne w podklasach, ponieważ zaimplementowaliśmy metodę, która nie jest ważna dla podklas, tworząc interfejs i implementując ten interfejs tam, gdzie go potrzebujemy.

Zasada segregacji interfejsu

Zasada segregacji interfejsu to zasada tworzenia programów komputerowych. Mówi o tym, że kiedy tworzymy różne części programu, nie powinniśmy tworzyć ich wszystkich w ten sam sposób. Zamiast tego należy je zredukować i uczynić bardziej szczegółowymi, aby inne części programu nie musiały polegać na rzeczach, których nie potrzebują. To pozwala na tworzenie kodu, który jest łatwiejszy do zmiany i utrzymania, ponieważ każda część robi tylko to, co musi.

Przyjrzyjmy się teraz sytuacji naruszenia jak i poprawnemu użyciu.

Naruszenie:

// Interface Segregation Principle Violation
// When we look at our example, we see that the interface we created contains many methods.
// If we do everything inside a common interface, we may have made unnecessary use in the places that implement our interface.
// Instead, we can divide our system into smaller interface parts.

interface Animal {
    fun swim()
    fun fly()
}

class Duck : Animal {
    override fun swim() {
        println("Duck swimming")
    }

    override fun fly() {
        println("Duck flying")
    }
}

class Penguin : Animal {
    override fun swim() {
        println("Penguin swimming")
    }

    override fun fly() {
        throw UnsupportedOperationException("Penguin cannot fly")
    }
}

 

Gdy spojrzymy na nasz przykład, zobaczymy, że stworzony przez nas interfejs zawiera wiele metod. Jeśli zrobimy wszystko wewnątrz wspólnego interfejsu, możemy niepotrzebnie wykorzystać miejsca, które implementują nasz interfejs. Zamiast tego możemy podzielić nasz system na mniejsze części interfejsu.

Prawidłowe użycie:

// Interface Segregation Principle Correct Usage
// As we saw in the correct usage example, dividing the system into smaller interfaces and using them where we needed them made it much easier to change the system in the future.

interface CanSwim {
    fun swim()
}

interface CanFly {
    fun fly()
}

class Duck : CanSwim, CanFly {
    override fun swim() {
        println("Duck swimming")
    }

    override fun fly() {
        println("Duck flying")
    }
}

class Penguin : CanSwim {
    override fun swim() {
        println("Penguin swimming")
    }
}

Jak widzieliśmy w prawidłowym przykładzie użycia, podzielenie systemu na mniejsze interfejsy i użycie ich tam, gdzie były potrzebne, znacznie ułatwiło zmianę systemu w przyszłości.

Zasada odwracania zależności

Zasada odwracania zależności to zasada SOLID, która mówi, że moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych, ale oba powinny zależeć od abstrakcji. Oznacza to, że klasy powinny zależeć od abstrakcji, a nie od konkretnych implementacji. Ideą tej zasady jest oddzielenie od siebie komponentów, dzięki czemu kod jest bardziej modułowy, łatwiejszy do testowania i łatwiejszy w utrzymaniu.

Przyjrzyjmy się teraz sytuacji naruszenia jak i poprawnemu użyciu.

Naruszenie:

// Dependency Inversion Principle Violation
// As we can see in this example, each of our payment methods is processed separately in our Service class in a hard code way.
// Instead of a hard code implementation, the system needed to be DEPEND to an abstract structure.

class PaymentService {
    private val paymentProcessorPaypal = PaypalPaymentProcessor()
    private val paymentProcessorStripe = StripePaymentProcessor()

    fun processPaymentWithPaypal(amount: Double): Boolean {
        return paymentProcessorPaypal.processPayment(amount)
    }

    fun processPaymentWithStripe(amount: Double): Boolean {
        return paymentProcessorStripe.processPayment(amount)
    }
}

class PaypalPaymentProcessor {
    fun processPayment(amount: Double): Boolean {
        // Process payment via Paypal API
        return true
    }
}

class StripePaymentProcessor {
    fun processPayment(amount: Double): Boolean {
        // Process payment via Stripe API
        return true
    }
}


fun main() {
    val paymentService = PaymentService()
    println(paymentService.processPaymentWithPaypal(50.0)) // Process payment via Paypal API
    println(paymentService.processPaymentWithStripe(50.0)) // Process payment via Stripe API
}

Jak widzimy w tym przykładzie, każda z naszych metod płatności jest przetwarzana oddzielnie w naszej klasie Service za pomocą sztywnego kodu. Zamiast implementacji sztywnego kodu, system musiał być zależny od abstrakcyjnej struktury.

Prawidłowe użycie:

// Dependency Inversion Principle Correct Usage
// In the correct usage example, we did not have to implement hard code about our payment methods in our Service class,
// because we set up an abstract structure with the interface that we created.

interface PaymentProcessor {
    fun processPayment(amount: Double): Boolean
}

class PaypalPaymentProcessor : PaymentProcessor {
    override fun processPayment(amount: Double): Boolean {
        // Process payment via Paypal API
        return true
    }
}

class StripePaymentProcessor : PaymentProcessor {
    override fun processPayment(amount: Double): Boolean {
        // Process payment via Stripe API
        return true
    }
}

class PaymentService(private val paymentProcessor: PaymentProcessor) {
    fun processPayment(amount: Double): Boolean {
        return paymentProcessor.processPayment(amount)
    }
}

fun main() {
    val paymentProcessor = PaypalPaymentProcessor()
    val paymentService = PaymentService(paymentProcessor)
    println(paymentService.processPayment(50.0)) // Process payment via Paypal API
}

W prawidłowym przykładzie użycia nie musieliśmy implementować sztywnego kodu dotyczącego naszych metod płatności w naszej klasie Service, ponieważ utworzyliśmy abstrakcyjną strukturę z interfejsem, który stworzyliśmy.

Podsumowanie

W rezultacie zasady SOLID są niezbędne do tworzenia łatwego w utrzymaniu, skalowalnego i wydajnego oprogramowania w Kotlinie. Wykorzystując unikalne funkcje i konstrukcje Kotlina, programiści mogą projektować modułowe, luźno powiązane systemy, które są zgodne z tymi wytycznymi. Przestrzeganie zasad SOLID nie tylko poprawia testowalność kodu, ale także zachęca do kultury ciągłego doskonalenia i najlepszych praktyk. Ostatecznie zastosowanie tych zasad w rozwoju Kotlina skutkuje oprogramowaniem wyższej jakości, które można skutecznie utrzymywać i dostosowywać do zmieniających się wymagań.

Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>