리스코프 치환 원칙

김지민·2023년 5월 29일
0

Solid

목록 보기
3/5

리스코프 치환 원칙 (Liskov Substitution Principle)

자식 클래스는 부모 클래스의 행동과 계속 호환되어아 햡니다

개념

서브타입은 언제나 기반타입으로 교체 할 수 있어야 한다
즉, 부모 클래스의 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 대신 사용했을 때 코드가 원래 의도대로 작동해야 한다

리스코프 치환 원칙같은 경우 여러 방식으로 해석 가능한 다른 원칙들과 달리 형식적인 요구사항이 있다

1. 자식 클래스의 메서드의 매개변수 유형들은 부모 클래스의 메서드의 매개변수 유형들보다 더 추상적이거나 추상화 수준이 같아야 한다.

class Animal { }
class Dog: Animal { }
class Bulldog: Dog { }

class ParentClass {
	func feed(_ a: Dog) { }
}	
    
class FirstChildClass: ParentClass {
	func feed(_ a: Animal) { }
}
    
class SecondChildClass: ParentClass {
	func feed(_ a: Bulldog) { }
}

부모클래스는 Dog 타입에 먹이를 주는 함수가 있다
첫번째 자식클래스에서는 매개변수로 Dog 의 상위클래스인 Animal을 받는다
두번째 자식클래스에서는 매개변수로 Dog 의 하위클래스인 Bulldog을 받는다
만약 부모클래스의 객체 대신 자식클래스의 객체를 전달한다고 생각해보자

첫번째 자식클래스는 모든 동물들에게 먹이를 줄 수 있으므로 클라인트가 전달하는 모든 Dog 에게 먹이를 줄 수 있다

반면 두번째 자식클래스에서는 매개변수를 불독으로만 제한했다
따라서 해당 메서드는 불독 이외의 다른 종의 Dog에는 먹이를 주지 못하며 부모클래스와 호환되지 못한다

2. 자식클래스의 메서드의 반환 유형은 부모클래스의 메서드의 반환 유형의 하위유형이거나 일치해야 한다

class Animal { }
class Dog { }
class Booldog { }
  
class ParentClass {
	func buyDog() -> Dog {
		return Dog()
	}
}	
    
class FirstChildClass: ParentClass {
	func buyDog() -> Animal {
		return Animal()
	}
}
    
class SecondChildClass: ParentClass {
	func buyDog() -> Booldog {
		return Booldog()
	}
}

부모클래스에는 buyDog() 이라는 메서드가 있고 Dog을 반환한다
첫번째 자식클래스는 Dog의 하위유형인 Animal 을 반환하고 두번재 자식클래스는 상위유형인 Bulldog 을 반환한다

첫번째 자식클래스는 어떠한 동물이든 다 반환한다
Dog을 위해 설계된 구조에 알 수 없는 다른 동물을 받기때문에 문제가 된다

반면 두번째 자식클래스에서는 Booldog을 반환한다
이는 Dog이니까 아무문제 없다

3. 자식클래스의 메서드는 부모 클래스에서 던지지 않을거라 예상되는 예외 유형(에러)를 던져서는 안됩니다

즉 예외 유형들은 부모 메서드가 이미 던질 수 있는 예외 유형들의 하위유형 혹은 일치해야 합니다

enum SomeError: Error {
	case someError
}

class ParentClass {
	func doSomething(_ a: String) { }
}

class ChildClass {
	func doSomething(_ a: String) throws {
		if a > 10 {
			throw SomeError.someError
		}
	}
}

예상치 못한 예외는 앱 전체를 충돌시킬 수 있다

대부분의 현대 프로그래밍 언어들은 위 규칙들이 언어에 내장되어 있어서 해당 규칙들을 위반하는 프로그램은 컴파일 할 수 없다

이제부터 설명하는 규칙들은 컴파일러로 못잡는 규칙들이다

4. 자식클래스는 사전 조건들을 강화해서는 안된다

enum SomeError: Error {
	case someError(String)
}

class ParentClass {
    func doSomething(_ a: Int) throws {
        if a < 0 {
            throw SomeError.someError("음수이면 안됩니다")
        }
    }
}

class FirstChildClass: ParentClass {
    override func doSomething(_ a: Int) throws {
        if a <= 0 {
            throw SomeError.someError("0보다 커야합니다")
        }
    }
}

부모클래스의 doSomething 함수는 파라미터로 받은 숫자가 음수이면 안된다는 조건이 있다
자식클래스에서 doSomething 함수를 재정의하면서 0이면 안된다는 조건이 추가됐다

해당 함수에 음수들이 전달될때 잘 작동하던 클라이언트 코드는 이 자식 클래스 객체와 작업하기 시작하면 문제가 생길 수 있다
부모클래스와 동일한 수준의 조건을 기대하고 사용하는 프로그램 코드에서 예상치 못한 문제가 발생할 수 있기 때문이다

5. 자식클래스는 사후 조건들을 약화해서는 안된다

enum SomeError: Error {
    case someError(String)
}

class ParentClass {
	func doSomething(_ a: Int) throws -> Int {
		if a < 0 {
		throw SomeError.someError("음수이면 안됩니다")
        }        
        return a
	}
}

class ChildClass: ParentClass {
    override func doSomething(_ a: Int) throws -> Int {
        return a
    }
}

부모클래스의 doSomething 함수는 반환할 값이 유효한 값인지 검사하고 있다
자식클래스는 doSomething 함수를 재정의하면서 해당 조건을 제거하여 조건을 약화시켰다

이 역시 음수를 반환할거라고 예상하지 못하는 클라이언트 코드에서는 오작동을 일으킬것이다

6. 부모 클래스의 불변속성들은 보존되어야 한다

불변속성이란 객체가 해당 객체로 이해되기 위해 갖추어야 하는 조건들이다
즉 부모 클래스의 데이터의 값의 조건은 자식 클래스에서도 계속 유지되어야 한다는 것이다

class ParentClass {
	var num: Int = .zero
	var _num: Int {
		get {
			return num
		}
		set {
			if newValue >= 0 {
				num = newValue
			}
		}
	}
}

class ChildClass: ParentClass {
	func doSomething(_ a: Int) {
		num = a
	}
}

부모클래스의 num 변수는 항상 0 혹은 양수만을 가질 수 있다
그러나 자식클래스의 doSomething 함수에서 아무런 조건없이 num에 값을 할당해주고 있다

그로인해 num 에 음수가 할당될 수 없다는 부모클래스의 불변속성이 깨져버렸다


A가 B를 상속받았으면 B로서도 역할을 할 수 있어야 한다

profile
iOS 신입으로 일하고 있습니다

0개의 댓글