객체지향 생활체조
- 한 메서드에 오직 한 단계의 들여쓰기만 합니다.
- else 표현을 사용하지 않습니다.
- 모든 원시 값과 문자열을 포장합니다.
- 한 줄에 점을 하나만 사용합니다.
- 이름을 줄여쓰지 않습니다. (축약금지)
- 모든 엔티티를 작게 유지합니다.
- 3개 이상의 스위프트 기본 데이터타입(Int, String, Double 등) 프로퍼티를 가진 타입을 구현하지 않습니다.
- 일급 콜렉션을 사용합니다.
- getter / setter 를 구현하지 않습니다.
1. 한 메서드에 오직 한 단계의 들여쓰기만 합니다.
- 함수 한 개에 여러 들여쓰기 블록 구문을 사용하지 않는다.
- 여러 개의 블록 구문을 사용하게 되면 해당 함수의 사용 목적에 모호성이 발생함
- 조건문 안에 조건문이 있고, 그 조건문 안에는 반복문이 있고, … 이런 구조의 코드는 대체 무슨일을 하는지 알아볼 수 없다.
- 들여쓰기를 한다는 것은 블록 구문이 여러 갈래로 나뉜다는 의미이기 때문에 자제하는 것
- 이 조건을 지키기 위해서 **메소드 추출(Extract Method) 기법**을 사용할 수 있다. 메소드 추출은 코드의 일부분을 메소드로 분리하여, 코드의 복잡도를 낮추는 기법이다. 함수를 짧고 간결하게 작성하는 것을 목표로 한다.
이 조건은 프로그래밍에 유연성을 제공한다.
2. else 표현을 사용하지 않습니다.
- if절이 중첩되면서 코드가 비선형적으로 흐르기 때문이다. 1번 조건인 ‘한 메서드에 오직 한 단계의 들여쓰기만 합니다.’ 조건과 연관이 있다. if 절이 중첩되면 인간이 읽기에 직관적이지 않을 수 있다. 즉, 코드의 동작을 파악하기 위해서는 중첩된 조건문의 흐름을 따라가기 어렵다. 이는 Arrow Anti Pattern 이다.
- 이 조건을 지키기 위해서 Ealry Return 방법을 사용할 수 있다. Ealry Return이란, 조건문 조건이 일치하면 그 즉시 반환을 하는 디자인 패턴인데 이를 활용하자.
3. 모든 원시 값과 문자열을 포장한다.
"int 값 하나 자체는 그냥 아무 의미 없는 스칼라 값일 뿐이다. 어떤 메서드가 int 값을 매개변수로 받는다면 그 메서드 이름은 해당 매개변수의 의도를 나타내기 위해 모든 수단과 방법을 가리지 않아야 한다." - 소트웍스 앤솔러지
- 프로그래밍을 할 때, 특정 문자열(”abc”)나 특정 상수(10)을 사용하지 않고 이를 변수나 객체를 활용하여 프로그래밍 코드를 작성한다.
- 원시타입 데이터는 아무런 의미를 가지고 있지 않다.
- 의미없는 원시타입 데이터를 활용하게 된다면, 나중에 조건이 바뀌거나 유지보수를 위해 수정을 할 때 에러 발생할 확률이 높아지고 그러면 유지보수 비용이 많아지게된다.
- 의미있는 변수를 활용하거나 데이터를 객체화 하게 된다면, 유지보수와 조건 추가 및 변경에 가독성이 증가하고 객체가 능동적인 행위를 갖게되어 더 객체지향적으로 코드를 작성할 수 있게 된다.
4. 한 줄에 점을 하나만 사용합니다.
- 여기서 점이란 객체 멤버에 접근하기 위한 점(.)을 의미한다.
if (person.getMoney().getValue() > 10000) {
System.out.println("당신은 부자 💰");
}
Getter를 사용하여 구현하면, 위와 같이 한 줄에 점이 2개 이상 찍힐것이다. 점이 여러개 찍혔다는 것의 의미는 무엇일까?
일단 위 코드는 점을 두개 이상 찍으면서, 결합도가 높아졌다. 위 코드를 사용하는 클래스는 Person 뿐 아니라 Money 에 대한 의존성을 추가로 갖게 된다.
"한 줄에 점을 하나만 찍으라" 는 원칙은 사실 디미터 법칙을 직관적으로 이해하기 위한 원칙이다. 디미터 법칙은 낯선 이와 이야기하지 말라(Don't Talk to Strangers) 또는 최소 지식 원칙(Principle of least knwoledge) 으로도 알려져있다.
디미터 법칙의 핵심은 객체 그래프를 따라 멀리 떨어진 객체에게 메시지를 보내는 설계를 피하라는 것이다. 이런 설계는 객체간의 결합도를 높이는 지름길이다. 많은 점이 찍혀있다는 것은, 객체가 다른 객체에 깊숙이 관여하고 있음을 의미한다. 이는 캡슐화가 깨져있다는 방증이기도 하다.
여기서 핵심은 ‘객체’이다. 객체 안에 객체에게 지속적으로 접근하는 것을 방지하란 뜻. 고차함수나 객체의 특성을 가지지 않은 자료구조, 함수 등에서 점을 사용하는 것은 예외이다.
- DTO, 자료구조와 같은 경우에는 내부 구조를 외부에 노출하는것이 당연하므로 디미터 법칙을 적용하지 않는다. 또한 Java Stream API 처럼 메소드 체이닝(method chaining)을 사용하는 경우는 디미터 법칙을 위반하지 않는다. 디미터 법칙은 결합도와 관련된 이야기이므로, 본질을 잊고 '한 줄에 점 하나'에 매몰되지 말자.
5. 이름을 줄여쓰지 않습니다.(축약금지)
"의도가 분명하게 이름을 지으라" ― 클린 코드
클래스, 메소드, 변수 이름을 축약하면 읽는 이로 하여금 혼란을 야기할 수 있다. 왜 축약하고 싶은 욕구가 생길까? 이름이 너무 길기 때문일 것이다. 이름이 왜 길까? 해당 클래스, 메소드가 너무 많은 일을 하고 있다는 신호 아닐까? 특히 클래스의 경우 단일 책임 원칙(SRP)을 위반하고 있을 수 있다.
- 클래스와 메소드의 역할을 적절하게 분리하고, 각각의 책임을 다른 객체와 메소드에 위임해보자. 역할과 책임이 줄어들어 이름도 짧게 만들 수 있을 것이다.
- 클래스와 메소드의 이름을 한두단어 정도로 짧게 유지하고, 문맥을 중복하는 이름을 자제하자. 주문을 나타내는 Order 클래스의 주문 메소드를 shipOrder() 로 명명할 필요가 있을까? 짧게 ship() 으로 하여도 의미는 통할 것이다.
6. 모든 엔티티를 작게 유지합니다.
- 타입은 작아야 한다.
- 타입을 만들 때 첫 번째 규칙은 크기이다. 두 번째 규칙도 크기이다. 더 작아야 한다.
- SOLID의 단일 책임 원칙은 타입이나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다.
- 타입은 책임, 즉 변경할 이유가 하나여야 한다는 의미이다.
- 응집도 - 타입은 인스턴스 프로퍼티 수가 적어야 한다.
- 응집도를 유지하면 작은 타입 여럿이 나온다.
50줄이 넘는 클래스와, 파일이 10개 이상인 패키지를 지양하자는 원칙이다. 보통 50줄이 넘는 클래스는 한가지 일을 하고 있지 않으며, 코드의 이해와 재사용을 어렵게 만든다. SRP 원칙은 SOLID 중 가장 지키기 쉬우면서, 효과가 좋은 원칙이다.
- 타입을 작게 유지하면 변경하기 쉬운 타입이 된다.
- 요구사항은 변화하기 마련이다. 따라서 코드도 변한다.
- 구현 타입에 의존하게 되면 테스트가 어려우며, 변화에 빠르게 대응하기 어렵다. 변화에 빠르게 대응하려면 DIP 원칙을 지키는 습관을 가져야한다.
- SOLID의 DIP 원칙은 타입이 상세한 구현이 아니라 추상화(프로토콜 등)에 의존해야 한다는 원칙이다.
- 테스트가 가능할 정도로 시스템 결합도를 낮추면 유연성과 재사용성도 더 높아진다.
패키지 내의 파일의 수도 줄여야지 하나의 목적을 달성하기 위한 연관된 클래스의 집합임이 드러나게된다. 이는 높은 응집도를 위함이다.
- 예외 타입
- 인스턴스는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 메서드만 공개합니다
- 자료 구조는 자료를 그대로 공개하며 별다른 메서드는 제공하지 않습니다
- 자료 구조체의 전형적인 형태는 공개 프로퍼티만 있고 메서드가 없는 인스턴스입니다. 이런 자료 구조체를 Data Transfer Object(DTO)라고 합니다.
7. 3개 이상의 스위프트 기본 데이터타입(Int, String, Double 등) 프로퍼티를 가진 타입을 구현하지 않는다.
인스턴스 변수가 많아질수록 클래스의 응집도는 낮아진다는 것을 의미한다. 마틴 파울러는 대부분의 클래스가 인스턴스 변수 하나만으로 일을 하는 것이 마땅하다고 한다. 하지만, 몇몇 경우에는 두개의 변수가 필요할 때가 있다고 한다. 클래스는 하나의 상태(인스턴스 변수)를 유지하고 관리하는 것과 두개의 독립된 변수를 조율하는 두가지 종류로 나뉜다고 한다.
이 원칙은 클래스의 인스턴스 변수를 최대 2개로 제한하는 것은 굉장히 엄격하지만, 최대한 클래스를 많이 분리하게 강제함으로써 높은 응집도를 유지할 수 있게하는 원칙이다.
→ 3개 이상의 프로퍼티를 사용하지 말라는 것이 아니라, 기본 데이터 타입의 프로퍼티를 지양하라는 뜻.
struct Animal {
var weight: Double
var height: Double
var numberOfLegs: Int
var age: Int
var gender: String
let jong: String
var iq: Int
var name: String
}
struct Animal {
struct PhysicalInfo {
var weight: Double
var height: Double
var numberOfLegs: Int
}
struct BioInfo {
enum Gender { case male, female, unknown }
struct Age {
var value: Int
init?(value: Int) {
let commonAgeRange = (0...300)
guard commonAgeRange.contains(value) else { return nil }
self.value = value
}
}
var age: Age
var gender: Gender
let jong: String
}
var iq: Int
var name: String
var physicalInfo: PhysicalInfo
var bioInfo: BioInfo
}
8. 일급 컬렉션을 사용합니다.
- '원칙3. 모든 원시값과 문자열을 포장한다.' 원칙과 비슷한 원칙이라고 생각한다. 컬렉션 또한 클래스로 포장하지 않으면, 의미없는 객체의 모음일 뿐이다.
일급 컬렉션
struct ABC {
var value: [Int]
}
단순히 var value: [Int] 로 선언된 변수를 사용하게 된다면, 문법적으로는 에러가 아니지만 논리적으로 맞지 않는 코드를 작성하게 될 여지가 높다.
즉, 자유도가 너무 높아져버리면 이후 유지보수할 때 정해진 논리나 개념의 변수를 문법적으로는 문제없도록 코드가 수정되었지만 프로그램을 실행하는 과정에서 논리적인 오류가 발생하게 된다.
예를 들어, 스택을 구현하고자 한다면
value.append(num) 을 사용해야하지만, value.insert(num, at: 5) 을 사용해도 문법적으로는 오류가 아니지만, 논리적으로는 오류가 발생된다.
따라서, 컬렉션을 사용할 때 일급 컬렉션을 활용하여 사용 목적에 맞게 한다.
class Stack<Item> {
private var items: [Item] = []
func push(_ item: Item) {
items.append(item)
}
func pop() -> Item {
return items.removeLast()
}
}
9. getter / setter를 구현하지 않는다.
- 객체에게 일을 시킨다.
- 프로퍼티의 값을 직접 빼오거나 직접 셋팅하지 않도록 한다.
- 캡슐화를 잘 하는 것.
객체의 상태를 가져오는 접근자(accessor)를 사용하는 것은 괜찮지만, 객체 바깥에서 그 결과값을 사용해 객체에 대한 결정을 내리는 것은 안된다.
// Gameprivate int score;
public void setScore(int score) {
this.score = score;
}
public int getScore() {
return score;
}
// Usage
game.setScore(game.getScore() + ENEMY_DESTROYED_SCORE);
위의 코드에서 getScore() 메서드가 어떤 결정을 내리는 용도로 사용되고 있다. Game 인스턴스에 그 책임을 부여하는 대신 말이다.
이보다 더 나은 코드를 작성해보자. getter/setter 를 제거하고 다른 메서드를 만들 것이다. 기억해야 할 것은 클래스에 무엇을 할 지 말 해야지, 물어보면 안된다는 것이다. 아래의 수정된 코드에서는 game에게 score를 수정하라고 말하고 있다.
// Gamepublic void addScore(int delta) {
score += delta;
}
// Usage
game.addScore(ENEMY_DESTROYED_SCORE);
score를 어떻게 수정할지 결정하는 것은 Game의 책임 하에 있다.
이 경우 getScore()는 남겨두어서 UI에서 score를 표시하기 위해 사용할 수도 있다. 그러나 setters는 허용되지 않는다.
마무리
객체지향 생활체조에 나와있는 원칙 혹은 규칙들은 프로그래밍을 하는데 코드의 가독성과 협업능력을 올려주는 이점이 있다.
그리고 우리가 코딩을 '객체지향'이라는 것을 간과하면서 코딩을 한다. 무엇이 객체지향인가? 어떤 것이 객체지향이 아닌가?
처음 코드작성 시에는 객체에 대하여 객체지향의 조건들을 모두 지키면서 코드 작성을 시작하지만 개발을 지속하면서 어느 샌가 모르게 객체가 능동성이 사라지고, 객체에 모호성이 발견되면서 결국 의도가 분명해지지 않은 객체가 생성된다. 처음 키보드에 손을 올렸을 때 우린 이런 것을 기대하진 않았을 것이다.
이 모든 규칙을 한번에 동시에 적용하기란 어려울 것이다. 그러기에 토이 프로젝트를 하면서 한 단계를 중점으로 코드를 작성하고 그 다음날에는 두번째 규칙을 중점으로 코드를 작성하면서 점차 몸에 학습하는 훈련을 진행해야겠다는 생각을 했다.
훈련을 통해 잘 읽히는 코드를 작성하고 결국 주석이 없어도 해석이 가능한 문서를 만들 것이다.
출처
https://hudi.blog/thoughtworks-anthology-object-calisthenics/
'Swift' 카테고리의 다른 글
애플 Text엔진 TextKit (2) | 2024.09.08 |
---|---|
TCA - Binding (0) | 2023.08.17 |
TCA - Environment는 어디갔나? (0) | 2023.07.10 |
TCA(The Composable Architecture)란? (1) | 2023.07.06 |
Swift 네트워크 추상화 - URL 처리방법 (0) | 2023.05.09 |