개발 공부/아키텍처

SOLID 원칙을 알아보자 3 : 리스코프 치환 원칙 - kotlin

yong_DD 2023. 8. 9. 19:53

SOLID 원칙이란?

Single Responsibility Principle

→ 

단일 책임 원칙

Open-Closed Principle 

→ 

개방-폐쇄 원칙

Liskov Substitution Principle 

→ 

리스코프 치환 원칙

Interface Segregation Principle 

→ 

인터페이스 분리 원칙

Dependency Inversion Principle

→ 

의존성 역전 원칙


Liskov Substitution Principle 

 리스코프 치환 원칙 

서브 타입이 상위 타입을 변경 없이 치환할 수 있어야하며, 치환해도 정상적으로 동작해야한다.

 

[예제 코드]

open class Rectangle {
    open var width = 0
    open var height = 0

    fun getArea() : Int {
        return width * height
    }
}

class Square : Rectangle() {
    override var width: Int = 0
        set(value) {
            println("width $field, $value")
            if(field!=value) {
                field = value
                height = value
            }
        }

    override var height: Int = 0
        set(value) {
            println("height $field, $value")
            if(field!=value) {
                field = value
                width = value
            }
        }

}

Rectangle은 width와 height이 있고 getArea를 통해 사이즈를 알 수 있다.

Rectangle을 상속한 Square는 width나 height 가 동일하다고 생각을 해서 widht의 값을 set할 때 height 값도 같이 변경하게 만들었다.

 

이와 같은 방식으로 만들어서 Rectangle을 써야할 때 대신 Square를 쓰게되면 다음과 같은 결과를 얻게 된다.

    val rect = Rectangle()
    rect.height = 20
    rect.width = 10
    println(rect.getArea()) // 200

    val rect2 = Square()
    rect2.height = 20
    rect2.width = 10
    println(rect2.getArea()) // 100

이렇게 되면 리스코프 치환 원칙에 위배되는 것이다. 

Square를 쓰더라도 Rectangle과 같은 값이 나와야 한다.

(정상적으로 동작해야 한다.)

 

하위 타입으로 치환하더라도 똑같은 값, 똑같은 행동을 해야한다.

 

[예제 코드2]

open class Payment {
    open var price = 0
    open var count = 0

    fun getTotalPrice() : Int {
        return price * count
    }
}

class DiscountPayment : Payment() {
    var discount = 0
    fun getTotalDiscountPrice() : Int {
        return getTotalPrice() * (discount /100)
    }
}

가격과 개수로 총 가격을 얻는 Payment 가 있고, Payment를 상속 받아 discountPrice를 알 수 있는 DiscountPayment가 있다.

DiscountPayment는 Payment의 기본 속성을 건드는 부분이 없기 때문에 다음과 같은 결과를 얻는다.

 

    val payment = Payment()
    payment.price = 100
    payment.count = 2
    println(payment.getTotalPrice()) // 200

    val payment2 = DiscountPayment()
    payment2.price = 100
    payment2.count = 2
    println(payment.getTotalPrice()) // 200

Payment의 하위인 DiscountPayment를 사용하더라도 동일한 결과를 얻을 수 있다.

 

 

하위 객체로 치환하더라도 반드시 아무런 문제가 없어야 하기 때문에, 상위 객체를 완벽히 대체 할 수 있는 상황일 때만 상속을 해야할 것 같다.