객체지향 프로그래밍 (객체지향 생활체조원칙)

marisol👩🏻‍💻·2022년 7월 8일
2

객체지향프로그래밍 (SOLID)에 이은 객체지향프로그래밍 2탄은 객체지향 생활 체조 원칙이다

객체지향 생활 체조 원칙은 따라 하다 보면 자연스럽게 객체지향적인 코드를 작성할 수 있는 규칙인데, 다음 9가지 규칙이 있다

🏃‍♀️ 객체지향 생활 체조 원칙

  1. 한 메서드에 오직 한 단계의 들여쓰기만 한다
  2. else 표현을 사용하지 않는다
  3. 모든 원시 값과 문자열을 포장한다
  4. 한 줄에 점을 하나만 사용한다
  5. 이름을 줄여 쓰지 않는다(축약 금지)
  6. 모든 엔티티를 작게 유지한다
  7. 3개 이상의 스위프트 기본 데이터타입(Int, String, Double 등) 프로퍼티를 가진 타입을 구현하지 않는다
  8. 일급 콜렉션을 사용한다
  9. getter/setter를 구현하지 않는다

9가지 규칙을 하나씩 정리해보려고 한다


1️⃣ 한 메서드에 오직 한 단계의 들여쓰기만 한다

들여쓰기가 여러 개가 존재할 경우, 해당 메서드는 여러 가지 일을 할 가능성이 있다
메서드는 맡은 일이 적을수록 재사용성이 높고 디버깅이 용이하기 때문에 이 원칙은 하나의 메서드는 하나의 일만 해야한다는 의미와도 연결된다

아래 saveDiary()라는 메서드는
1) 기존에 일기가 있는지 없는지 체크하고
2) 기존에 일기가 있을 경우 위치 권한을 얻었는지를 체크해서 날씨 정보를 얻어오고 있다

func saveDiary() {
      if diary == nil {
        if locationManager?.authorizationStatus == .authorizedAlways || locationManager?.authorizationStatus == .authorizedWhenInUse {
           setWeatherInfo()
           } else {
              createDiary()
           }
      } else {
        editDiary()
    }
}

이렇게 들여쓰기가 2번된 경우, 한 메서드에서 2가지 일을 하고 있을 가능성이 있다

2️⃣ else 표현을 사용하지 않는다

신규 기능이 추가되거나 수정사항이 발생하면 기존 코드를 리팩터링하는 것보다 분기처리하는 조건문을 넣는게 쉽기 때문에 많이 사용한다
이 규칙은 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
    }

3️⃣ 모든 원시 값과 문자열을 포장한다

예를 들어 아래와 같은 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
}

4️⃣ 한 줄에 점을 하나만 찍는다

이 원칙은 디미터의 법칙(Law of Demeter) 혹은 최소한의 지식 원칙(The Principle of Least Knowledge)과 연관되어있다

a.b.c 이렇게 한 줄에 점이 두 개 이상 있는 경우,
a -> b -> c의 형태로 a가 b를 통해 c를 부르는 경우는 a가 b뿐만 아니라 c까지 알고 있어야 하기 때문에 강한 결합도를 갖고 있다는 것을 의미한다고 한다

5️⃣ 이름을 줄여 쓰지 않는다(축약 금지)

처음 프로그래밍 공부를 시작할 때부터 지켜야 한다고 들어왔던 원칙이다
애플 Programming API guidance 문서에도

명확함이 간결함보다 중요하다

라고 축약을 지양하도록 하고 있다

메서드의 이름이 긴 이유는, 책임을 너무 많이 갖고 있어서 일 수 있다

func checkValidityAndSave() {
	// 유효성 검사
    // 저장
}

위 코드는 2개의 책임을 갖고 있는데, 아래와 같이 각각 메서드를 나누어주어야 한다

func checkValidity() {
	// 유효성 검사
}

func save() {
	// 저장
}

그리고 이름을 지나치게 축약하는 경우, 혼돈을 야기할 수도 있고, 다른 사람이 알아보기 어려울 수 있기 때문에 네이밍을 줄여쓰는 것은 지양해야 한다 (예: viewController => vc)

6️⃣ 모든 엔티티를 작게 유지한다

지난번 포스팅의 SRP와 비슷한 맥락인데, (엔티티가 하나의 책임만 갖고 있으면 엔티티가 작게 유지되기 때문에)
50줄 이상되는 클래스 또는 10개 파일 이상의 패키지는 지양해야 한다는 원칙이다

패키지 (하나의 목적을 달성하기 위한 연관된 클래스들의 모임)를 작게 유지하면
패키지가 진정한 정체성을 갖게된다고 한다

7️⃣ 3개 이상의 스위프트 기본 데이터타입(Int, String, Double 등) 프로퍼티를 가진 타입을 구현하지 않는다

새로운 인스턴스 변수를 가진 클래스는 응집도가 떨어진다
원시값을 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
}

8️⃣ 일급 컬렉션을 쓴다

일급 컬렉션(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()
  • 해당 컬렉션에 필요한 모든 로직은 일급 컬렉션에서 구현할 수 있다
  • 상태와 행위를 한 곳에서 관리할 수 있다
  • 컬렉션에 명명 가능 (검색도 편하고, 명확한 표현도 가능하다)

같은 장점이 있는것 같다

9️⃣ getter/setter를 쓰지 않는다

캡슐화를 지키면서 객체에 메시지를 보내 스스로 상태에 대한 처리 로직을 수행하라는 의미

객체의 상태를 가져오는 접근자를 사용하는 것은 괜찮지만, 객체 바깥에서 그 결과값을 사용해 객체에 대한 결정을 내리는 것은 안된다. 한 객체의 상태에 대한 결정은 어떤 것이든 그 객체 안에서만 이루어져야 한다.

SOLID의 Open-Closed Principle 원칙과 연관이 있는 것 같다


객체지향 생활 체조 원칙이 절대적인 규칙이라고 볼 수는 없겠지만, 코드를 짤 때 한 번씩 생각해볼만한 내용인 것 같다.

참고 자료

0개의 댓글