SRP를 위반하는 클래스를 살펴보자
class Employee {
fun calculatePay() {
regularHours()
}
fun reportHours() {
regularHours()
}
fun save() {}
}
fun regularHours() {
// calculateHours
}
3가지 메서드가 서로 다른 세명의 액터를 책임진다.
calculatePay()를 사용하는 회계팀에서 시간을 계산하는 방법을 바꿀 때 reportHours()를 사용하는 인사팀에 대한 고려를 잊게 되면 의도치 않은 변경이 일어나게 된다.
Employee 테이블 스키마 변경과 reportHours()의 수정이 동시에 일어나게 되면 역시 문제가 발생한다.
class PayCalculator(
private val data: EmployeeData
) {
fun calculatePay() {}
}
class HourReporter(
private val data: EmployeeData
) {
fun reportHours() {}
}
class EmployeeSaver(
private val data: EmployeeData
) {
fun saveEmployee() {}
}
너무 따로 흩어져 있어 개별 클래스를 인스턴스화하고 추적하기가 어렵기 때문에 Facade 패턴을 활용할 수 있다.
class EmployeeFacade(
private val payCalculator: PayCalculator,
private val hourReporter: HourReporter,
private val employeeSaver: EmployeeSaver,
private val data: EmployeeData
) {
fun calculatePay() {
payCalculator.calculatePay()
}
fun reportHours() {
hourReporter.reportHours()
}
fun save() {
employeeSaver.saveEmployee()
}
}
항상 예시를 드는게 기억에 잘 남는다. 퍼사드 패턴의 경우 사장이 각 담당 직원을 찾아서 일을 시키는 것보다. 중간관리자(퍼사드)에게 일을 시키면 관리자가 해당 직원에게 전달하는 방식으로 이해했다.
// SRP 위반으로 인해 OCP도 위반
class Human {
// drive()가 drive 행위 외에 car 생성 책임도 지고 있음
// complexArgs가 변경되면 Human의 drive()도 변경됨, 변경에 열려있게 되어버림
fun drive() {
val args = ""
val car = Car(args)
car.drive()
}
}
class Car(private val complexArgs: String) {
fun drive() {
}
}
생성의 책임을 분리하여 외부에서 주입하여 해결할 수 있다.
class Human {
// 이제는 Car의 상세구현이 변경되어도 Human에서 변경할 것이 없다. 변경에 닫힘
// 하지만 확장에는 닫혀있음, Car 대신 다른 탈것을 타고 싶으면 변경이 필요함
fun drive(car: Car) {
car.drive()
}
}
class Car(private val complexArgs: String) {
fun drive() {
// drive a car
}
}
하지만 변경에는 닫혀있지만 확장에 열려있지는 않다. Car 대신 다른 탈 것을 타려고 하면 마찬가지로 Human에 변경이 있어야하기 때문
class Human {
// 이제는 Car의 상세구현이 변경되어도 Human에서 변경할 것이 없다. 변경에 닫힘
// 하지만 확장에는 닫혀있음, Car 대신 다른 탈것을 타고 싶으면 변경이 필요함
fun drive(vehicle: Vehicle) {
vehicle.drive()
}
}
class Car(private val complexArgs: String): Vehicle {
override fun drive() {
// drive a car
}
}
class Truck(private val args: String): Vehicle {
override fun drive() {
// drive a truck
}
}
interface Vehicle {
fun drive()
}
그래서 더 고수준의 Vehicle 인터페이스를 통해 의존 방향성을 역전시킴
SRP, DIP를 통해 OCP 원칙을 준수하게 되면 저수준에서의 변경은 고수준에 영향을 끼치지 않게 되고 유지보수성이 향상된다.
A -> B -> C 순서로 의존성을 갖게 되면 A -> C 의존성을 갖게되는 걸 의미한다. 소프트웨어의 원칙 중에 '자신이 직접 사용하지 않는 요소에는 절대 의존해선 안된다.'라는 원칙이 있다. 중간에 interface를 통해서 내부를 은닉할 수 있다.
package cleanarchitecture
class OPS {
fun op1() {}
fun op2() {}
fun op3() {}
}
interface U1Ops {
fun op()
}
interface U2Ops {
fun op()
}
interface U3Ops {
fun op()
}
class U1OpsImplementation(private val ops: OPS) : U1Ops {
override fun op() {
ops.op1()
}
}
class User1 {
fun op(u1Ops: U1Ops) {
u1Ops.op()
}
}
User1이 OPS에 의존하지 않아 OPS의 op2, op3의 변경으로부터 자유롭다.
ISP는 언어 타입에 의존한다고 한다. 동적 타입 언어의 경우 소스코드에 선언문이 존재하지 않아 재컴파일과 재배포가 필요없다.
변동성이 큰 구현체 클래스를 참조하지 마라
대신 추상 인터페이스를 참조하라. 일반적으로 이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 추상 팩토리를 사용하도록 강제한다.
변동성이 큰 구현체 클래스로부터 파생하지 마라
'상속을 피하라'
구현체 함수를 오버라이드 하지 마라
구현체 함수를 재정의하면 소스 코드 의존성을 제거할 수 없고 의존성을 상속하게 된다.
구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 마라
팩토는 객체지향 언어에서 바람직하지 못한 의존성을 처리할 때 많이 사용되는 패턴
가장 중요한 건 결국 수정하는 비용을 줄이는 것. 본질에만 의존하여 아키텍처답게 뼈대에 집중할 것. 그 외의 구체적인 구현에 의존하게 되면 비용은 커질 수밖에 없다.