객체지향 설계에서 지켜줘야 할 5개의 소프트웨어 개발 원칙
- SRP (Single Responsibility Principle) : 단일 책임 원칙
- OCP (Open Closed Principle) : 개방 폐쇄 원칙
- LSP (Liskov Substitution Principle) : 리스코프 치환 원칙
- ISP (Interface Segregation Principle) : 인터페이스 분리 원칙
- DIP (Dependency Inversion Principle) : 의존 역전 원칙
좋은 설계 : 시스템에 새로운 요구사항이나 변경사항이 있을 때, 영향을 받는 범위가 적은 구조를 말한다. 그래서 시스템에 예상하지 못한 변경사항이 발생하더라도, 유연하게 대처하고 이후에 확장성이 있는 시스템 구조를 만들 수 있다.
코드를 확장하고 유지 보수 관리하기가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있다.
이때 책임의 범위는 딱 정해져있는 것이 아니고, 어떤 프로그램을 개발하느냐에 따라 개발자마다 생각 기준이 달라질 수 있다. 따라서 단일 책임 원칙에 100% 해답은 없다.
✅ 확장에 열려있다 : 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 큰 힘을 들이지 않고 애플리케이션의 기능을 확장할 수 있음.
✅ 변경에 닫혀있다 : 새로운 변경 사항이 발생했을 때 객체를 직접적으로 수정을 제한함.
추상화 사용을 통한 관계 구축을 권장한다. 즉, 다형성과 확장을 가능케 하는 객체지향의 장점을 극대화하는 기본적인 설계 원칙
OCP 원칙을 지키지 않는다면 when 문에서 새로운 타입을 추가하거나 다운캐스팅이 필요한 상황이 발생한다.
sealed class Shape
class Circle(val radius: Double) : Shape()
class Rectangle(val width: Double, val height: Double) : Shape()
fun calculateArea(shape: Shape): Double {
return when (shape) {
is Circle -> calculateCircleArea(shape)
is Rectangle -> calculateRectangleArea(shape)
// 새로운 타입이 추가될 때마다 when 문을 수정해야 함
}
}
fun calculateCircleArea(circle: Circle): Double {
return Math.PI * circle.radius * circle.radius
}
fun calculateRectangleArea(rectangle: Rectangle): Double {
return rectangle.width * rectangle.height
}
fun main() {
val circle = Circle(5.0)
val rectangle = Rectangle(3.0, 4.0)
println("Circle Area: ${calculateArea(circle)}")
println("Rectangle Area: ${calculateArea(rectangle)}")
}
sealed class Shape
class Circle(val radius: Double) : Shape()
class Rectangle(val width: Double, val height: Double) : Shape()
interface AreaCalculator {
fun calculateArea(shape: Shape): Double
}
class CircleAreaCalculator : AreaCalculator {
override fun calculateArea(shape: Shape): Double {
if (shape !is Circle) {
throw IllegalArgumentException("Shape must be a Circle")
}
return Math.PI * shape.radius * shape.radius
}
}
class RectangleAreaCalculator : AreaCalculator {
override fun calculateArea(shape: Shape): Double {
if (shape !is Rectangle) {
throw IllegalArgumentException("Shape must be a Rectangle")
}
return shape.width * shape.height
}
}
fun main() {
val circle = Circle(5.0)
val rectangle = Rectangle(3.0, 4.0)
val circleAreaCalculator = CircleAreaCalculator()
val rectangleAreaCalculator = RectangleAreaCalculator()
println("Circle Area: ${circleAreaCalculator.calculateArea(circle)}")
println("Rectangle Area: ${rectangleAreaCalculator.calculateArea(rectangle)}")
}
부모 메서드의 오버라이딩을 조심스럽게 따져가며 해야한다. 부모 클래스와 동일한 수준의 선행 조건을 기대하고 사용하는 프로그램 코드에서 예상치 못한 문제를 일으킬 수 있다.
한번 인터페이스를 분리하여 구성해놓고 나중에 무언가 수정사항이 생겨서 또 인터페이스를 분리하는 행위를 가지지 말아야 한다. (인터페이스는 한번 구성하였으면 웬만해선 변하면 안되는 정책 개념)
인터페이스는 제약 없이 자유롭게 다중 상속(구현)이 가능하기 때문에, 분리할 수 있으면 분리하여 각 클래스 용도에 맞게 구현하라는 설계 원칙