나쁜 코드와 좋은 코드를 구별하고 더 나아가 좋은 코드로 개선할 방법을 떠올릴 수 있는 코드감각이 필요하다.
프로그래밍 과정에서 기억할 두 가지 규칙은
처음 이곳에 왔을 때보다 더 깨끗하게 만들고 떠날 것
나중에 해야지라고 할 때 나중은 결코 오지 않는다
이름을 지을 때 중요한 것은 명확한 의미를 담고 있어야 한다는 것.
이름 간의 일정한 규칙과 맥락을 가져야 한다.
또 이미 널리 사용되는 이름을 혼자 다르게 사용하거나 잘못된 추측을 하게 만드는 축약이나 개성있는 이름도 피하자.
주석보다 코드로 의도를 표현할 수 있도록 연습하자.
다양한 상황에서 주석으로 설명하는 대신 코드로 표현할 수 있다.
// 플레이어가 이벤트 대상인지 체크
if (player.lever >> 30 && player.isPromotioned) { ... }
----
if player.isEligibleForEvent() { ... }
// smodule이 우리 시스템에 의존하는가??
if (smodule.getDepedencyList().contain(subSystem.getSystem())) { ... }
-----
let moduleDependencies = smodule.getDependencyList()
let ourSystem = subSystem.getSystem()
if moduleDependecies.contain(ourSystem)
주석을 주의해야 하는 이유는 코드의 변화를 주석이 따라가지 못하는 경우가 많기 때문이다.
그 결과 코드를 정확히 설명하지 못하고 부적절한 정보를 주는 주석이 될 가능성이 생긴다.
또 부적절한 주석이 많아지면 주석을 무시하게 되는 습관을 낳는다.
코드의 의도를 알리거나 결과에 대한 경고를 알리는 주석 등은 좋다.
간혹 아래 코드처럼 닫는 괄호( }
)에 어떤 스코프에 해당하는지 설명을 달고 싶어진다면 크기를 줄일 방법을 고민해보는 것이 우선이다.
var body: some View {
ScrollView {
List {
ForEach(arr) { item in
if isPresented {
do {
let foo = try bar()
VStack {
SomeView {
HStack {
AnotherView {
Text("Nested")
}
}
if condition {
ZStack {
Text("Layered View")
}
}
}
Text(foo.description)
}
} catch {
VStack {
Text("Error occurred")
Button("Retry") {
retryAction()
}
}
}
} else {
HStack {
if shouldShow {
VStack {
Text("Alternative View")
Spacer()
SomeOtherView()
}
} else {
Text("Fallback")
}
}
}
}
}
}
}
마지막으로 지역 메서드에서 전역에 관한 주석을 달거나 너무 많은 정보 / 너무 모호한 정보 등을 주석을 다는 것도 조심할 것.
함수는 최대한 작게 만든다. 들여쓰기가 2단을 넘어가지 않도록 신경써볼 것.
특히 단 한가지의 일만 하도록 주의해야 하는데,
지정된 함수 이름 아래로 추상화 수준이 하나인 단계만 수행하는지 체크할 것.
단순히 다른 표현이 아닌 의미있는 다른 이름으로 분리해낼 수 있다면 여러 일을 하고 있는 것이라 볼 수 있음.
또는 함수 내에 섹션이 자연스럽게 나누어져도 여러 책임을 가진 것이라 볼 수 있다.
함수가 한 가지 작업만 하려면 함수 내의 모든 추상화 수준이 동일해야 한다.
동일한 추상화 수준이란 근본 개념과 세부 구현 사항이 혼재되어있지 않음을 의미한다.
func fetchUserData() -> User {
let url = URL(string: "https://api.example.com/user")! // (저수준)
var request = URLRequest(url: url) // (저수준)
request.httpMethod = "GET" // (저수준)
let data = try! URLSession.shared.data(for: request).0 // (저수준)
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] // (저수준)
return User(id: json["id"] as! Int, name: json["name"] as! String) // (고수준)
}
---
func fetchUserData() -> User {
let data = fetchUserDataFromAPI() // (고수준)
return parseUserData(data) // (고수준)
}
private func fetchUserDataFromAPI() -> Data {
let url = URL(string: "https://api.example.com/user")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
return try! URLSession.shared.data(for: request).0
}
private func parseUserData(_ data: Data) -> User {
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
return User(id: json["id"] as! Int, name: json["name"] as! String)
}
추상화 수준을 구분하는 것은 아직 어렵지만 크게 3가지로 나눈다면 다음과 같을 수 있겠다.
더 자세하게는
1. 하나의 개념적 단위를 형성하는지?
1. 저수준: 개별적인 시스템 동작을 직접 다룸
2. 중간수준: 특정 기능을 담당하는 역할을 함
3. 고수준: 위 둘을 조합하여 더 큰 개념을 의미하게 됨
2. 동사 기준으로 추상화를 나누기( 무엇을 한다(동작) vs 어떻게 한다(세부구현) )
1. 고수준: fetchUserData() -> 사용자 데이터를 가져온다 -> 추상적 개념
2. 중간수준: fetchUserDataFromAPI() -> API에서 데이터를 가져온다 -> 특정 작업 수행
3. 저수준: performHTTPRequest() -> HTTP요청을 실행한다 -> 시스템 동작
위와 같이 구분해보도록 연습해보고 추상화 수준이 동일한 함수를 구현해볼 것.
또 위에서 아래로 내려갈수록 추상화 수준이 낮아지는 게 자연스럽다.
배를 채운다
밥을 먹는다
요리를 한다
재료 준비를 한다, 재료가 없다면 주문한다
switch는 태생적으로 여러가지 일을 한다.
즉 앞서 말한 한 가지 일만 한다는 걸 위반하기 쉽다는 것인데 저자는 아래처럼 추상 팩토리에 숨기는 방식을 선호했다.
또 다형적 코드를 생성하는 곳에서만 사용할 것을 권장한다.
func foo(type: SomeType) {
switch type {
case foo:
...
case bar:
...
...
}
}
func foo(type: SomeType) {
let someValue = SomeFactory(type)
someValue...
someValue...
...
}
이름은 최대한 서술적으로 짓기를 추천한다.
인수 또한 없는 게 가장 좋고 그 뒤로 1개, 2개이다.
3개는 주의해야 하고 4개는 안 쓰는 것이 좋다고 한다.
특히 출력인수(inout이나 &등을 통해 참조하여 입력받은 인수를 직접적으로 수정하는 인수)의 사용은 결과를 예측하기 힘들게 하므로 최대한 지양할 것.
플래그를 인수로 받을 것이라면 차라리 두 개의 함수로 분리하고(flag에 따라 내부에서 두 가지 일을 할 것이 분명한 것도 이유에 포함될 듯)
단항 함수라면 함수 이름과 인수가 동사와 명사로 쌍을 이룰 수 이뤄야 좋다.
이항 함수라면 두 인수가 하나의 값을 가리키거나(point x, y처럼) 자연적인 순서가 명확한 경우에 사용할 것
자연적인 순서가 없다면 어떤 인수를 먼저 넣을지 헷갈릴수밖에 없다. 이 경우 함수 이름에 키워드를 넣어줘도 좋다고 한다.
가능하다면 두 인수를 하나의 구조체로 묶어 전달하는 것도 좋다.
func write(name) {} // 동사 - 명사 쌍
func assertEqual(expected, actual) {}
-> func assertExpectedEqualActual(expected, actual) {}
부수효과(사이드 이펙트)가 없어야 한다.
명령과 조회가 분리되어야 한다.
'동작을 시도'하고 '성공 여부'를 반환하는 경우가 없어야 하는 것.
예외, 오류 처리 또한 하나의 책임으로 do try를 쓰되 try catch를 분리하는 것이 좋다고 한다.
적절한 행길이와 빈 행 사용에 주의하기
변수는 사용하는 곳과 가장 가까이, 인스턴스 변수는 가장 상단에 두기
그리고 종속 함수는 호출하는 함수 뒤에 둘 것
들여쓰기를 무시하고자 하는 유혹에 넘어가지 말기
개인의 선호가 아닌 팀 규칙을 항상 우선시 할 것
**이름에는 의도와 맥락이 드러나고 잘못된 정보나 맥락을 가지지 않도록 하자.
검색과 발음이 쉬울수록 더욱 좋다.
주석보다 코드로 의도를 표현할 수 있는 방법을 최우선적으로 고민하고 정말 코드 이해에 대한 정보인 경우나 결과에 대한 경고를 알려야 할 경우에만 제공할 것.
전체적으로 FP를 설명하는 듯한 느낌이 많이 들었다.
SRP를 잘 지키는 순수함수를 위주로 추상화 수준을 생각하며 설계할 것.
한 문서에 사용된 양식이 다른 소스 파일에서도 유지될 것이란 믿음을 깨지 않도록 만들 것.
멋진 글입니다👍🏼 추후 정독하도록 하겠습니다.