[CleanCode] 3장 함수

eunsol Jo·2021년 8월 23일
0

🧹  CleanCode

목록 보기
3/4
post-thumbnail

작게 만들어라!

  • if / else / while 문에 블록은 한줄이어야 한다. → 감싸는 함수(enclosing function)가 작아진다.
  • 거기서 함수를 호출하도록 한다. → 블록안의 함수 이름을 적절하게 짓는다면, 코드를 이해하기 쉬워진다.
  • 함수내 들여쓰기는 1~2단 내로 제한한다.

한 가지만 해라!

  • 추상화 수준이 하나이면, 한가지만 하는 것이다.
    → 함수를 만드는 이유는 결국 큰 개념을 다음 추상화 수준에서 여러단계로 나누어 하기 위함이다.
  • 함수내에 의미 있는 다른 함수로 추출이 가능하다면, 여러 작업을 하고 있는 것이다.

함수 당 추상화 수준은 하나로!

  • 한가지만 하기 위해서는 함수내 추상화 수준이 하나여야한다.
  • 한 함수내 추상화 수준이 여러개이면, 읽기가 어렵다.
    → 근본 개념 + 세부사항이 뒤섞이기 시작하면, 깨진창문 처럼 세부사항이 증가한다.
    위에서 아래로 코드 읽기 : 내려가기 규칙
  • 한 단씩 함수내 추상화 수준을 낮춰야 한다.

Switch문

  • switch문은 작게 만들기 어렵다.
  • 한가지 작업만 하는 switch문을 만들기도 어렵다.
switch (e.type) {
	case COMMITSSIONED:
		return calculateCommissionedPay(e);
	case HOURLY:
		return calculateHourlyPay(e);
	case SALARIED:
		return calculateSalariedPay(e);
	default:
		throw new InvalidEmployeeType(e.type);
}

위 코드의 문제점

  • 함수가 길다
  • 새 직원 유형 추가시 더 길어진다.
  • 한가지 작업만 하지 않는다. SRP를 위반
  • 새 직원 유형 추가시, 코드 변경 필요. OCP를 위반 (구현체 변경시 클라이언트 코드 변경)

switch문을 허용하는 하나의 케이스. 다형적 객체를 생성하는 코드.

상속관계로 숨긴후 다른코드에 들어내지 않는다.

public interface EmployeeFactory {
	public Employee makeEmployee(EmployeeRecorder r) throws InvalidEmployType;
}
---------------------------------------
public class EmployeeFactoryImpl implements EmployeeFactory {
	public Employee makeEmployee(EmployeeRecorder r) throws InvalidEmployType{
		// switch문
	}
}

서술적인 이름을 사용하라!

  • 주석을 쓰는거 보다 서술적 이름을 쓰는것이 좋다.
  • 서술적인 이름은 개발자 머릿속 설계에도 도움이 된다.
  • 일관성 있는 이름 → 짐작하는 대로 함수를 찾음

함수 인수

  • 인수는 이해를 어렵게 만든다.
    • 0개(무항) : 이상적인 인수
    • 1개(단항) / 2개(이항)
    • 3개(삼항) : 가능한 피한다. → 유효한 조합으로 테스트가 부담스러워짐.
    • 4개이상(다항) : 사용시 특별한 이유가 필요
  • 출력인수는 입력인수보다 이해하기 어렵다.

< 많이 쓰는 단항 형식 >

  1. 인수에 질문을 던지는 경우
    • boolean fileExists("MyFile")
  2. 인수를 변환해 결과를 반환하는 경우
    • InputStream fileOpen("MyFile")
  3. 이벤트
    • void passwordAttemptFailedNtimes(int attempts)
    • 주의) 이름과 문맥을 주의해, 이벤트 함수라는 것이 드러나야함.

< 피해야할 단항 형식 >

  • 변환함수에서 출력인수 사용
    • void method(StringBuffer out) (X) → StringBuffer method(StringBuffer in) (O)

< 플래그 인수 >

  • 플래그 인수는 추하다. → 함수가 한번에 여러가지를 한다는 의미 = true이면 A를 false이면 B를한다.
    • render(boolean isSuite) → renderForSuite() 와 renderForSingleTest()로 분리가 바람직

< 이항 함수 >

  • 단항보다 이해하는데 시간이 소요된다. 가급적 단항함수로 바꾼다.
  • 자연적인 순서도 없다면 위험성이 있다.
    • assertEquals(expected, actual) : expected와 actual간의 순서가 없어, 반대로 쓰는 실수가 잦다.

이항함수가 적절한 경우

  • new Point(0, 0) : x, y 직교 좌표계의 자연적 순서가 존재한다.

< 삼항함수 >

  • 이항함수 보다도 더 이해하는 시간이 소요된다.

< 인수객체 >

  • 인수가 2-3개 된다면, 클래스로 만들어 보는걸 고려한다.

< 인수목록 >

  • 인수개수가 가변적인 함수도 필요.
    • String.format("%s worked %.2f" hours.", name, hours);
    • public String format(String format, Object... args) → 사실상 이항함수라 할수 있다.

< 동사와 키워드 >

  • 함수+인수 : 동사+명사
    • writeField(name)
  • 키워드 추가
    • assertEqual(expected, actual) → assertEqualExpectedAndActual(expected, actual)
    • 인수의 순서를 기억할 필요가 없다.

부수 효과를 일으키지 마라!

부수효과는 거짓말이다.
함수에서 한가지를 하겠다 해놓고, 남몰래 다른것도 하는 것이니.

  • 시간적인 결합(temporal coupling) + 순서 종속성(order dependency) 를 발생
  • 차라리 한가지만 하지 못할꺼면, 함수명에 부수효과 까지 작성 해라.

< 출력인수 >

출력인수는 피한다.
함수에서 상태를 변경한다면, 함수가 속한 객체 상태를 변경하는 방식을 택한다.

  • appendFooter(s) = void appendFooter(StringBuffer reporter); (X)
  • report.appendFooter(); (O) *this를 사용.

명령과 조회를 분리하라!

함수는 객체의 상태를 변경 or 정보를 반환. 둘다하면 안됨.

// ASIS - 의도가 모호하다. set하는 함수인가? username이 unclebob인지 확인하는 함수인가?
if(set("username", "unclebob")... 

// TOBE
if(attributeExists("username")) {
	setAttribute("username", "unclebob");
}

오류 코드보다 예외를 사용하라!

명령함수에서 오류코드를 반환하는것은, 명령/조회 분리 규칙에 어긋난다.

  • if(deletePage(page) == E_OK)
  • 명령을 표현식 같이 사용하게 된다.
  • 중첩되는 코드를 야기한다.
  • 오류 코드를 반환하면, 호출자는 오류코드를 곧바로 처리해야 한다는 문제점에 부딪힘.

< Try/Catch블록 뽑아내기 >

try/catch문은 추하다. try/catch 블록을 별도 함수로 뽑아내는 편이 좋다.

public void delete(Page page){
	try{
		deletePageAndAllReferences(page);
	}
	catch (Exception e) {
		logError(e);
	}
}

// try블록 별도 함수로 추출
private void deletePageAndAllReferences(Page page) throws Exception {
	deletePage(page);
	registry.deleteReference(page.name); // 실제동작함수
	configKeys.deleteKey(page.name.makeKey());
}
// catch블록 별도 함수로 추출
private void logError(Exception e){
	logger.log(e.getMessage());
}
  • deletePageAndAllReferences 와 logError로 정상동작과 오류처리 동작을 분리.
  • 이해와 수정이 쉬워짐

< 오류 처리도 한 가지 작업이다. >

  • try로 시작해 catch/finally문으로 끝나야함. 이후 로직이 있으면 안됨.

< Error.java 의존성 자석(magnet) >

  • 오류코드 반환 = 어딘가 오류코드가 정의됨.
  • 정의된 파일이 변경되면, 해당 클래스를 import한 모든 파일을 재컴파일/재배치 해야함.
  • 이는 기존 코드의 재사용을 야기한다.
    • 따라서, 오류코드 대신 예외처리를 사용
    • Exception 클래스에 파생된 새 예외는 재컴파일/배치 없이 새 예외 클래스 추가 가능.

반복하지 마라!

중복은 모든 악의 근원이다.
AOP, COP는 어떤면에서 중복을 제거하기 위한 전략이다.

구조적 프로그래밍

모든 함수의 입구와 출구는 하나만 존재. 즉 return문이 하나.
함수를 작게 만든다면 return, break, continue를 여러번 사용가능하나, goto문은 함수의 크기와 관계없이 지양하는 것이 좋다.

함수를 어떻게 짜죠?

여느 글짓기와 비슷하다. 생각을 기록 → 읽기 좋게 다듬는다.

  • 처음에는 길고, 복잡 + 중복 + 인수테스트 목록도 길고 + 테스트 케이스 또한 세세하다.
  • 함수를 만들고 + 이름을 바꾸고 + 중복을 제거 + 메서드를 줄이고, 순서를 바꾼다. + 전체 클래스를 쪼개기도한다.
    이 과정에서 테스트 케이스는 항상 통과 된다.

*FitNesse(오픈소스 테스트 도구)

profile
Later never comes 👩🏻‍💻

0개의 댓글