Programs that references an object from a base class
must be able to use an object of a derived class without behavior differences and without knowing about its existence.Base와 Base를 상속받는 객체가 있을 때,
특정 프로그램에서 Base를 받아와 사용한다면,
Base를 사용하든, 상속받은 객체를 사용하든, 차이점이 없어야 하고, 차이점이 보여서도 안된다.
class Rectangle {
var width: Int
var height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
}
func area() -> Int {
return width * height
}
}
class Square: Rectangle {
override var width: Int {
didSet {
super.height = width
}
}
override var height: Int {
didSet {
super.width = height
}
}
}
func main() {
let square = Square(width: 10, height: 10)
let rectangle: Rectangle = square
rectangle.height = 7
rectangle.width = 5
print(rectangle.area())
// As a rectangle we should expect the area as 7 x 5 = 35, but we got 5 x 5 = 25
}
main()에서 Rectangle 타입으로 두고 rectangle 변수에 Square 타입 값을 할당하면
최종적으로 print 되는 값은 25가 됨
이 상황에서는, Square를 Rectangle 타입에 할당했지만, rectangle.height = 7 과 같은 코드는
Square에서 동작하는 것과 동일하게 동작
즉, Rectangle 타입으로 적어뒀고, 그렇게 알고 있는데, Square로 동작한다는 점에서 LSP를 위반함
추상화를 하기 위해 보통 Polymorphism(다형성)에 도움을 받음
(e.g. 마켓 앱에서 살 수 있는 모든 제품을 API로 받아올 때 다양한 아이템들을 추상화해서 받기)
위 예시에서 rectangle 이라는 변수가 어떤 타입이 될 수 있는지 모름
정사각형인지, 직사각형인지 등등 …
그렇기 때문에 우리는 rectangle 처럼 동작할 것이라고 예상할 수 있음
하지만, 위 예시에서 실상은 rectangle 로 동작하지 않고, square 로 동작함
protocol Geometrics {
func area() -> Int
}
class Rectangle: Geometrics {
var width: Int
var height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
}
func area() -> Int {
return width * height
}
}
class Square: Geometrics {
var edge: Int
init(edge: Int) {
self.edge = edge
}
func area() -> Int {
return edge * edge
}
}
func main() {
let rectangle: Geometrics = Rectangle(width: 10, height: 10)
print(rectangle.area())
let rectangle2: Geometrics = Square(edge: 5)
print(rectangle2.area())
}
만약 상속 받으면서 내부 내용물을 정할 수 있다면, 손쉽게 따를 수 있을 것임
그래서 Protocol을 사용 !
class Shape {
func doSomething() {
// do something relate to shape that is irrelevant to this example, actually
}
}
class Square: Shape {
func drawSquare() {
// draw the square
}
}
class Circle: Shape {
func drawCircle() {
// draw the circle
}
}
func draw(shape: Shape) {
if let square = shape as? Square {
square.drawSquare()
} else if let circle = shape as? Circle {
circle.drawCircle()
}
}
코드가 이런 형식으로 되어 있으면, draw(shape:) 메소드가 실행될 때,
Square 타입이 들어오거나, Circle 타입이 들어오거나, Shape 타입이 들어올 때 다른 방식으로 실행됨
추가로, 이 예시는 OCP를 위반하기도 하는데,
Triangle이라는 Shape을 상속받는 새로운 타입을 만들 때,
draw(shape:) 메소드에 Triangle인 경우에도 추가해야 하기 때문
protocol Shape {
func draw()
}
class Square: Shape {
func draw() {
// draw the square
}
}
class Circle: Shape {
func draw() {
// draw the circle
}
}
func draw(shape: Shape) {
shape.draw()
}
프로토콜로 Shape 타입을 draw() 메소드와 함께 만들어 둔다면,
각 타입을 만들 때, 타입별로 필요한 메소드를 내부에 적어둘 수 있고,
draw(shape:)에서도 어떤 Shape이건 동일하게 동작함을 볼 수 있음
추가적으로, Triangle 타입을 추가한다 하더라도, draw(shape:)에 변화를 줄 필요가 없기 때문에,
OCP를 만족한다고도 볼 수 있음