객체지향프로그래밍 (SOLID)에 이은 객체지향프로그래밍 2탄은 객체지향 생활 체조 원칙이다
객체지향 생활 체조 원칙은 따라 하다 보면 자연스럽게 객체지향적인 코드를 작성할 수 있는 규칙인데, 다음 9가지 규칙이 있다
🏃♀️ 객체지향 생활 체조 원칙
9가지 규칙을 하나씩 정리해보려고 한다
들여쓰기가 여러 개가 존재할 경우, 해당 메서드는 여러 가지 일을 할 가능성이 있다
메서드는 맡은 일이 적을수록 재사용성이 높고 디버깅이 용이하기 때문에 이 원칙은 하나의 메서드는 하나의 일만 해야한다는 의미와도 연결된다
아래 saveDiary()라는 메서드는
1) 기존에 일기가 있는지 없는지 체크하고
2) 기존에 일기가 있을 경우 위치 권한을 얻었는지를 체크해서 날씨 정보를 얻어오고 있다
func saveDiary() {
if diary == nil {
if locationManager?.authorizationStatus == .authorizedAlways || locationManager?.authorizationStatus == .authorizedWhenInUse {
setWeatherInfo()
} else {
createDiary()
}
} else {
editDiary()
}
}
이렇게 들여쓰기가 2번된 경우, 한 메서드에서 2가지 일을 하고 있을 가능성이 있다
신규 기능이 추가되거나 수정사항이 발생하면 기존 코드를 리팩터링하는 것보다 분기처리하는 조건문을 넣는게 쉽기 때문에 많이 사용한다
이 규칙은 if/else 만 사용하지 않는 것이 아니라 switch/case 구문을 포함한 분기 구문
을 지양하여 코드를 간단명료하게 만드는 것을 이야기하고 있다
user와 computer 중 승자를 결정하는 아래와 같은 코드가 있고, switch문으로 분기 처리하고 있다
func decideWinner() {
switch numberToDecideWinner {
case Decision.winNumber[0], Decision.winNumber[1]:
winner = Player.user
default:
winner = Player.computer
}
}
switch를 이용한 분기처리를 없애주려면 early return을 통해 처리해줄 수 있다
func decideWinner() {
if numberToDecideWinner == Decision.winNumber[0]
|| numberToDecideWinner == Decision.winNumber[1] {
winner = Player.user
return
}
winner = Player.computer
}
예를 들어 아래와 같은 Person 구조체가 있다고 할 때
struct Person {
var age: Int
var height: Double
var name: String
}
이 값들은 사용하기에 안전할까? (의도대로만 사용될까?)
let marisol = Person(age: -404, height: 38373, name: "marysol")
이렇게도 타입의 설계 의도와 벗어난 사용이 가능하게 된다
그래서 age를 예로 들면, 사람의 나이로 가능하다고 생각되는 0~150까지만 받고 싶다면 위와 같이 Int 원시값을 사용하는 것이 아니라, Age라는 클래스로 Wrap해서 사용할 수 있다
struct Person {
class Age {
private(set) var value: Int
init(_ value: Int) throws {
guard (0...150).contains(value) else {
throw NSError() as Error
}
self.value = value
}
}
var age: Age
}
또는 x,y 좌표와 width, height를 가지는 Frame을 구현할 경우,
아래와 같이 원시값을 포장해줄 수 있다
struct Frame {
struct Point {
var x, y: Double
}
struct Size {
var width, height: Double
}
let point: Point
let size: Size
}
이 원칙은 디미터의 법칙(Law of Demeter) 혹은 최소한의 지식 원칙(The Principle of Least Knowledge)과 연관되어있다
a.b.c 이렇게 한 줄에 점이 두 개 이상 있는 경우,
a -> b -> c의 형태로 a가 b를 통해 c를 부르는 경우는 a가 b뿐만 아니라 c까지 알고 있어야 하기 때문에 강한 결합도를 갖고 있다는 것을 의미한다고 한다
처음 프로그래밍 공부를 시작할 때부터 지켜야 한다고 들어왔던 원칙이다
애플 Programming API guidance 문서에도
명확함이 간결함보다 중요하다
라고 축약을 지양하도록 하고 있다
메서드의 이름이 긴 이유는, 책임을 너무 많이 갖고 있어서
일 수 있다
func checkValidityAndSave() {
// 유효성 검사
// 저장
}
위 코드는 2개의 책임을 갖고 있는데, 아래와 같이 각각 메서드를 나누어주어야 한다
func checkValidity() {
// 유효성 검사
}
func save() {
// 저장
}
그리고 이름을 지나치게 축약하는 경우, 혼돈을 야기할 수도 있고, 다른 사람이 알아보기 어려울 수 있기 때문에 네이밍을 줄여쓰는 것은 지양해야 한다 (예: viewController => vc)
지난번 포스팅의 SRP와 비슷한 맥락인데, (엔티티가 하나의 책임만 갖고 있으면 엔티티가 작게 유지되기 때문에)
50줄 이상되는 클래스 또는 10개 파일 이상의 패키지는 지양해야 한다는 원칙이다
패키지 (하나의 목적을 달성하기 위한 연관된 클래스들의 모임)를 작게 유지하면
패키지가 진정한 정체성을 갖게된다고 한다
새로운 인스턴스 변수를 가진 클래스는 응집도가 떨어진다
원시값을 3개 이상 갖고 있는 경우, 여러가지 일을 하고 있을 가능성이 있기 때문에 쪼갤 수 있는 가능성이 있다고 한다
struct Student {
var school: String
var grade: String
var name: String
var age: Int
}
struct Student {
struct PersonalInfo {
var name: String
var age: Int
}
struct SchoolInfo {
var school: String
var grade: String
}
var personalInfo: PersonalInfo
var schoolInfo: SchoolInfo
}
일급 컬렉션(First Class Collection): Collection을 Wrapping하면서, Collection 외 다른 변수가 없는 클래스의 상태
class IntStack {
private var stack: [Int] = []
func push(_ item: Int) {
}
func pop() -> Int? {
return stack.last
}
}
var stack = IntStack()
stack.push(1)
stack.pop()
같은 장점이 있는것 같다
캡슐화를 지키면서 객체에 메시지를 보내 스스로 상태에 대한 처리 로직을 수행하라는 의미
객체의 상태를 가져오는 접근자를 사용하는 것은 괜찮지만, 객체 바깥에서 그 결과값을 사용해 객체에 대한 결정을 내리는 것은 안된다. 한 객체의 상태에 대한 결정은 어떤 것이든 그 객체 안에서만 이루어져야 한다.
SOLID의 Open-Closed Principle 원칙과 연관이 있는 것 같다
객체지향 생활 체조 원칙이 절대적인 규칙이라고 볼 수는 없겠지만, 코드를 짤 때 한 번씩 생각해볼만한 내용인 것 같다.
참고 자료