Kotlin SOLID principles

Kotlin SOLID principles

Kotlin SOLID principles

Many Kotlin developers don't fully understand SOLID principles, and even if they do, they don't know why they are using it. Are you ready to learn all the details?

introduce

Hello dear Kotlin lovers! Welcome to my new article. Today I'm going to talk about the SOLID principles in Kotlin. First, I'll explain with examples what the SOLID principles are and what they are used for.

What are the SOLID principles?

SOLID is an acronym for five design principles that help create maintainable, scalable, and robust software . Robert C. Martin introduced these principles to guide programmers in writing high-quality code . Although originally intended for object-oriented programming, SOLID is also applicable to other languages ​​such as Kotlin. These principles are intended to promote clean code and improve software design . SOLID principles are as follows:

  • Single Responsibility Principle
  • Open and close principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principles
    Now, if you're ready, let's take a closer look at these principles with examples of their correct use and violations.

Single Responsibility Principle

The Single Responsibility Principle (SRP) is a part of the SOLID programming principles in object-oriented programming. It indicates that a particular class should have only one changed purpose. ** This means that a class should have only one responsibility or job. SRP is useful for maintaining classes and functions by keeping them organized and easy to understand. **When a class has multiple responsibilities, those responsibilities may inadvertently affect other tasks or jobs of the class, leading to unexpected behavior, bugs, and increased maintenance costs.

Let's take a look at the violations and their correct usage.

violation:

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

Violation of the Single Responsibility Principle
In this example, the system classes try to handle many different situations in the same place. This approach may cause major problems in the future.

Correct usage:

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

The Single Responsibility Principle Used Correctly
As shown in this example, we divide the System class into specific sections and place the functions in their respective classes.

Open and close principle

The open/closed principle is a rule in object-oriented design that states that classes, modules, functions, and other software entities should be open for extension but closed for modification. This means you can add new content to a class without changing its original code. Therefore, without changing the classes themselves, you can write new code that uses existing classes to add new functionality. Doing this makes the code easier to maintain and reuse.

Let's take a look at the violations and their correct usage.

violation:

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

Violation of the Open-Closed Principle
In this example, when we try to add something new to the class, we have to rewrite existing code, which can cause problems later.

Correct usage:

// 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()

The correct usage of the principle of opening and closing
According to the correct usage, we do not change the class itself, but use the existing class to write a new class, and implement our functions under the new class.

Liskov Substitution Principle

The Liskov substitution principle is an important rule in object-oriented programming. It says that if you have a program that can handle objects of a certain type, you should be able to use any subtype of that object without problem. This means that all methods and properties in the main class should also be available in all subclasses without changing anything.

Let's take a look at the violations and their correct usage.

violation:

// 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!")
    }
}

Violation of the Liskov Substitution Principle
As we can see in this example, the methods we write in the main class should work fine in its subclasses according to the Liskov principle, but when our subclass inherits from the superclass, our fly method is not working as expected.

Correct usage:

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

Correct usage of the Liskov Substitution Principle
As you can see in this example, everything we write in the superclass will be valid in the subclass because we do this by creating an interface and implementing that interface wherever we need it To implement a method that is not valid for subclasses.

Interface Segregation Principle

The interface segregation principle is a rule for making computer programs. It says that when we make different parts of a program, we shouldn't make them the same way. Instead, we should make them smaller and more specific, so that other parts of the program don't have to depend on things they don't need. This helps us write code that is easier to change and maintain because each part does only what it needs to do.

Let's take a look at the violations and their correct usage.

violation:

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

Violation of the Interface Segregation Principle
When we looked at our example, we saw that the interface we created contained many methods. If we do everything inside a public interface, we may be making unnecessary uses where the interface is implemented. Instead, we can divide our system into smaller interface parts.

Correct usage:

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

The Principle of Interface Segregation Used Correctly
As we saw in the example of correct usage, dividing the system into smaller interfaces and using them where we need them makes it easier to change the system in the future.

Dependency Inversion Principle

The Dependency Inversion Principle is a SOLID principle that states that high-level modules should not depend on low-level modules, but both should depend on abstractions. This means that classes should depend on abstractions, not concrete implementations. The idea behind DIP is to decouple components from each other, which makes the code more modular, easier to test, and easier to maintain.

Let's take a look at the violations and their correct usage.

violation:

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

Violation of the Dependency Inversion Principle
As we can see in this example, each of our payment methods is hard-coded to handle individually in our service class. Systems need to rely on abstract structures, not hard-coded implementations.

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
}

In the correct usage example, we don't have to implement hardcode about our payment method in our service class, because we set up an abstract structure with the interface we created.

in conclusion

Therefore, SOLID principles are essential to create maintainable, scalable, and efficient software in Kotlin. Using Kotlin's unique features and constructs, developers can design modular, loosely coupled systems that follow these guidelines. Adhering to SOLID principles not only improves the testability of your code, but also encourages a culture of continuous improvement and best practices. Ultimately, adopting these principles in Kotlin development results in higher quality software that can be maintained efficiently and adapted to changing needs.

insert image description here

reference

[Kotlin Language] https://kotlinlang.org/
[SOLID Principles in Kotlin] https://proandroiddev.com/solid-design-principles-in-kotlin-79100c670df1
[SOLD Principles: The Way of Kotlin] https:// medium.com/the-android-caf%C3%A9/solid-principles-the-kotlin-way-ff717c0d60da
[SOLD principles and Kotlin examples] https://codersee.com/solid-principles-with-kotlin-examples/
[Adopt SOLID principles, write clean and maintainable code] https://bootcamp.uxdesign.cc/adopting-solid-principles-for-clean-and-maintainable-code-with-kotlin-51e615c6b315

Guess you like

Origin blog.csdn.net/u011897062/article/details/130879774
Recommended