G1 : 한 소스 파일에 여러 언어를 사용한다.
- 현실적으로는 힘들긴 하다.
- 각별한 노력을 기울여 소스파일에서 언어 수와 범위를 최대한 줄여야 한다.
G2 : 당연한 동작을 구현하지 않는다
- 최소 놀람의 원칙에 따라, 함수나 클래스는 다른 프로그래머가 당연하게 여길만한 기능을 제공해야 한다.
- 요일 문자열을 Enum으로 변환하는 다음 코드를 보자.
- Day day = DayDate.StringToDay(String dayName);
- 독자는 StringToDay() 가 Day.MONDAY 로 변환해줄 것을 기대한다.
- 대소문자는 당연히 구분하지 않을 것이라 기대한다.
- 이런 당연한 기능을 구현하지 않으면 코드를 읽거나 사용하는 사람이 더 이상 함수 이름만으로 함수 기능을 예상하기 어렵다.
G3 : 경계를 올바로 처리하지 않는다
- 직관에 의존하지 마라.
- 모든 경계 조건을 찾아내고, 모든 경계 조건을 테스트하는 테스트 케이스를 작성하라.
G4 : 안전 절차 무시
- 컴파일러 경고 (warning) 을 무시하지 말도록 하자.
- 실패하는 테스트케이스를 제끼고 나중으로 미루지 말자.
G5 : 중복
- 중복은 매우 중요한 사항이다.
- 중복을 발견하면 추상화할 기회로 간주하라.
- 중복 유형 1 ) 똑같은 코드의 반복 -> 함수로 만들어 쓴다.
- 중복 유형 2 ) 여러 모듈의 If/else switch/case -> 다형성을 이용
- 중복 유형 3 ) 알고리즘이 유사하나 코드가 서로 다른 중복 -> TEMPLATE METHOD 패턴, STRATEGY 패턴
G6 : 추상화 수준이 올바르지 못하다
- 고차원 개념 : 추상 클래스
- 저차원 개념 : 파생 클래스
- 모든 저차원 개념은 파생 클래스에 넣고, 모든 고차원 개념은 추상 클래스(기초 클래스) 에 넣는다.
- 예를 들어, 세부 구현과 관련한 상수, 변수, 유틸리티 함수는 파생 클래스에 넣어야 한다. 기초 클래스는 구현 정보에 무지해야 한다.
G7 : 기초 클래스가 파생 클래스에 의존한다.
- 기초 클래스에서 파생 클래스를 사용하면 안된다.
- 기초 클래스는 파생 클래스를 아예 몰라야 한다.
G8 : 과도한 정보
- 잘 정의된 인터페이스는 많은 함수를 제공하지 않는다. 그래서 결합도가 낮다.
- 클래스가 제공하는 메서드 수는 적을수록 좋다.
- 함수가 아는 변수도 적을수록 좋다.
- 클래스에 들어있는 인스턴스 변수도 적을수록 좋다.
- 함수의 변수, 메서드를 숨겨라.
G9 : 죽은 코드
- 죽은 코드 : 실행되지 않는 코드
- 무조건 false인 조건을 검사하는 if 문.
- throw 되는 예외 코드가 없는 try 블록 후의 catch 문
- 코드에서 제거하라.
G10 : 수직 분리
- 변수와 함수는 사용되는 위치에 가장 가깝게 배치하라. 읽기 쉽게.
- 지역 변수는 처음으로 사용하기 직전에 선언. (수직 거리가 가까운 곳)
- private 함수는 처음으로 호출한 직후에 선언.
G11 : 일관성 부족
- 어떤 개념을 특정 방식으로 구현했다면, 유사한 개념도 같은 방식으로 구현한다.
- 표기법 역시 유사하게 일관성있게 작성한다.
G12 : 잡동사니
- 비어 있는 기본 생성자는 필요없다. 이는 컴파일러가 자동으로 생성해준다.
- 아무도 사용하지 않는 변수, 함수 도 잡동사니다.
- 정보를 제공하지 못하는 주석도 잡동사니다.
G13 : 인위적 결합
- 무관한 개념을 인위적으로 결합하지 않는다.
- 일반적 enum 은 특정 클래스에 속할 이유가 없다.
- 범용 static 함수는 특정 클래스에 속할 이유가 없다.
G14 : 기능 욕심
- 클래스 매서드는 다른 클래스의 변수와 함수에관심을 가져서는 안된다.
- 메서드가 다른 객체의 참조자와 변경자를 사용해 그 객체 내용을 조작하다면, 그것이 기능 욕심이다.
- 한 클래스에서 get~() 를 통해 다른 클래스의 값을 가져와서 조작하는 것도 기능 욕심이다.
- 하지만 불가피한 상황이 있을 수 있음을 책에서 소개하는데, HourlyEmployeeReport 클래스의 reportHours 메서드가 다른 클래스의 값을 get 해와서 동작을 구현한다.
- 하지만 이를 다른 클래스로 옮기면 reportHours() 의 출력 포멧을 그 클래스가 알아야 한다. 출력 포멧이 바뀐다면 두 클래스 모두 바뀌어야 한다. 이런 문제는 객체 지향의 설계 원칙을 위반하므로 불가피한 상황이라 볼 수 있다.
G15 : 선택자 인수
- 동작을 제어하는 인수는 문제다. (대표적으로 boolean 인수)
- 인수를 통해 동작을 선택하는 대신 새로운 함수를 만드는 편이 좋다.
G16 : 모호한 의도
- 의도를 명확하게 밝힌다.
- 행 바꾸기, 매직넘버 지우기, 함수 이름짓기 등에 시간을 투자하여 보기 좋고 의도를 확실하게 표현하는 코드를 짠다.
G17 : 질못 지운 책임
- 코드는 독자가 자연스럽게 기대할 위치에 배치한다.
- PI 상수는 삼각함수를 선언한 클래스에 넣는게 맞다.
- OVERTIME_RATE 상수는 HourlyPayCalculator 클래스에 선언하는게 맞다.
- 책임이 알맞은지 확인하는 하나의 방법은 함수의 이름을 보는 것이다.
- 함수 이름에 맞는 책임을 지워 위치를 배치하거나 책임에 맞는 함수 이름을 바꾼다.
G18 : 부적절한 static 함수
- 일반적으로 static 함수보다 인스턴스 함수가 더 좋다.
- 조금이라도 의심스럽다면 인스턴스 함수로 정의한다.
- 재정의할 가능성이 있는 함수는 static 이면 안된다.
- static 함수가 재정의 할 가능성이 있다면, 인스턴스 함수로 만든다.
G19 : 서술적 변수
- 계산 단계를 여러 단계로 나누고, 중간 값으로 서술적인 변수를 사용할 수 있다.
- 서술적인 변수는 많이 써도 괜찮으며, 일반적으로 많을수록 좋다.
G20 : 이름과 기능이 일치하는 함수
- Date newDate = date.add(5);
- 위 메서드는 더하는 단위가 무엇인가. 5시간? 5일? 5주? 5달?
- 인스턴스를 변경하고 날을 더한다면, addDaysTo, increaseByDays 의 이름이 좋을 것이다.
- 인스턴스를 변경하지 않고 새 인스턴스를 반환한다면, daysLater, daysSince 가 좋을 것이다.
- 이름으로 분명하게 구현을 알 수 있어야 한다. 코드를 뒤적거리지 않아야 한다.
G21 : 알고리즘을 이해하라
- 단순히 프로그램을 '돌리기'위해 여기저기 찔러보는 식의 프로그래밍 보단,
- 알고리즘을 확실이 이해하면, 코드를 더 말끔히 짤 수 있다.
G22 : 논리적 의존성은 물리적으로 드러내라
- A 클래스와 PRICE 상수를 선언해 사용한다고 치자.
- 하지만 PRICE 상수는 논리적으로 B 클래스에 속하는 것이 맞다는 것을 깨달았다.
- 그렇다면 PRICE 는 B 클래스로 옮기고 get~() 을 통해서 물리적으로 의존성을 드러내자.
G23 : If/Else Switch/Case 보다 다형성을 사용하라
- 유형보다 함수가 더 쉽게 변하는 경우는 극히 드물다.
- 선택을 수행하는 코드에서는 다형성 객체를 생성해 switch 문을 대신한다.
G24 : 표준 표기법을 따르라
- 팀의 구현 표준을 따라야 한다.
- 인스턴스 변수 이름을 선언하는 위치
- 클래스/메서드/변수 이름을 정하는 방법
- 괄호를 넣는 위치 등을 명시
G25 : 매직 숫자는 명명된 상수로 교체하라
- 코드상에 상수로 띡 있는게 매직 넘버이다.
- 리터럴은 그 의미를 해석하기 힘들다.
- 명명된 상수를 이용하여 리터럴의 의미를 한눈에 알기 쉽게 한다.
- 이해하기가 쉬워서 코드 자체가 자명하다면, 상수 뒤로 숨길 필요는 없다. 예를 들어 radius * Math.Pi * 2 의 2는 너무나 자명하여 상수처리를 굳이 안해도 이해하기 쉽다.
- 매직넘버는 숫자 뿐만 아니라 문자열, 논리형 등 의 자료형을 포함한다.
G26 : 정확하라
- 부동 소수점으로 통화를 표현하면 안된다.
- 갱신할 가능성이 희박하다고 잠금과 트랜잭션 관리를 건너뛰면 안된다.
- List 로 선언할 변수를 ArrayList 로 선언하는건 지나친 제약이다.
- 코드에서 뭔가를 결정할 때에는 정확히 결정한다. 결정의 이유와 예외를 처리할 방법을 분명히 알아야 한다.
- 함수가 null 을 반환할 확률이 조금이라도 있다면 null 검사를 진행한다.
- 조회 결과가 하나뿐이라고 짐작한다면 하나인가를 검증한다.
- 통화를 처리해야한다면, 반올림을 통해 정수로 이용한다.
G27 : 관례보다 구조를 사용하라
- Enum 변수를 멋들어지게 사용하는 switch/case 문 보다 추상 메서드가 있는 기초클래스가 더 좋다.
- 구조 자체로 설계를 강제하는 것이 더 좋다.
G28 : 조건을 캡슐화하라
- 조건문 안의 조건을 함수로 떼어내서 함수 이름으로 조건을 표현하면 보기 좋다.
- if (a > 0) 보다 if(isPositive(a)) 가 알기 쉽다.(이 예제는 너무 간단해서 아닐 수 있지 만 이런 식이라는 말이다.)
G29 : 부정 조건은 피하라
- 부정 조건은 긍정 조건보다 이해하기 어렵다. 가능하면 긍정 조건으로 표현한다.
- if (buffer.shouldCompact() ) 가 아래의 코드보다 좋다.
- if ( ! buffer.shouldNotCompact() )
G30 : 함수는 한가지만 해야 한다
- 함수를 작게 더 작게 만들어 한가지만 하도록 만든다.
G31 : 숨겨진 시간적인 결합
- 함수 인수를 적절히 배치하여 시간적인 결합을 강제로 드러낼 수 있다.
- 코드를 아래와 같이 쓸 수 있다.
public void dive(String reason) {
saturateGradient();
reticulateSplines();
diveForMoog(reason);
}
- 하지만 이렇게 쓴다면, 중간의 메서드들의 시간적인 결합이 강제되지 않는다. 호출하는 순서가 중요하다면, 오류를 낼 수 있는 순서로 프로그래머가 호출할 수 있다.
- 아래와 같이 인수를 이용하여 명시적으로, 강제로 프로그램의 순서를 제시할 수 있다.
public void dive(String reason) {
Gradient gradient = saturateGradient();
List<Spline> splines = reticulateSplines(gradient);
diveForMoog(splines, reason);
}
- 쓸모없는 코드가 생겼다고 할 수 있겠지만, 덕분에 시간적 복잡성을 드러내게 되었다.
G32 : 일관성을 유지하라
- 코드 구조를 잡을 때는 이유를 고민하라.
- 그리고 그 이유를 코드 구조로 명백히 표현하라.
- 구조에 일관성이 없어 보인다면 남들이 맘대로 바꿔도 괜찮다고 생각한다.
- 다른 클래스의 유틸리티 가 아닌 public 클래스는 자신이 아닌 클래스 범위 안에서 선언하면 안된다. 패키지 최상위 수준에서 public 클래스로 선언하는 관례가 일반적이며, 프로젝트 전체에서 이 일관성을 유지해야 한다.
G33 : 경계 조건을 캡슐화하라
- 경계 조건은 한 곳에서 별도로 처리한다. 코드 여기저기서 처리하지 않는다.
- 한 예로, 코드 여기저기서 +1 을 하지 않는다.
- int nextLevel = level + 1;
- 위와같이 캡슐화 한 후 nextLevel 을 사용한다.
G34 : 함수는 추상화 수준을 한 단계만 내려가야 한다.
- 함수 내 모든 문장은 추상화 수준이 동일해야 한다.
- 그리고 그 추상화 수준은 함수 이름이 의미하는 작업보다 한단계 낮아야 한다.
- 추상화 수준을 분리하면 앞서 드러나지 않았던 추상화 수준이 드러나는 경우가 빈번하다.
G35 : 설정 정보는 최상위 단계에 둬라
- 추상화 최상위 단계에 둬야 할 기본값 상수나 설정 관련 상수를 저차원 함수에 숨겨서는 안된다.
- 대신 고차원 함수에서 저차원 함수를 호출할 때 인수로 넘긴다.
G36 : 추이적 탐색을 피하라
- A가 B를 사용하고, B가 C를 사용하다고 해서 A가 C를 알 이유는 없다.
- a.getB().getC().doSomething() 은 알맞지 않다.
- 이를 디미터의 법칙이라 한다. (다른 글에 정리했다.)
'책읽기 > 클린코드' 카테고리의 다른 글
냄새와 휴리스틱 - 이름 (0) | 2021.01.26 |
---|---|
냄새와 휴리스틱 - 자바 (0) | 2021.01.26 |
냄새와 휴리스틱 - 함수 (0) | 2021.01.26 |
냄새와 휴리스틱 - 환경 (0) | 2021.01.26 |
냄새와 휴리스틱 - 주석 (0) | 2021.01.26 |