[Clean Code] 3장 함수

ASk·2021년 12월 30일
0

Clean Code

목록 보기
3/17
post-thumbnail

Intro

  • 의도를 분명히 표현하는 함수를 어떻게 구현할 수 있을까?
  • 함수에 어떤 속성을 부여해야 처음 읽는 사람이 프로그램을 직관적으로 파악 할 수 있을까?

작게 만들어라!

  • 함수를 만들 때 첫째 규칙은 '작게!', 둘째 규칙은 '더 작게!' 이다.
  • 함수가 작을수록 좋다는 증거나 자료를 제시하신 어렵지만, 짧을 수록 각 함수가 명백하고 하나의 동작을 한다.
  • 예시 1
// Before
public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception {
  boolean isTestPage = pageData.hasAttribute("Test"); 
  if (isTestPage) {
    WikiPage testPage = pageData.getWikiPage(); 
    StringBuffer newPageContent = new StringBuffer(); 
    includeSetupPages(testPage, newPageContent, isSuite); 
    newPageContent.append(pageData.getContent()); 
    includeTeardownPages(testPage, newPageContent, isSuite); 
    pageData.setContent(newPageContent.toString());
  }
  return pageData.getHtml(); 
}

// After
public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { 
  if (isTestPage(pageData)) {
    includeSetupAndTeardownPages(pageData, isSuite);
  }
  return pageData.getHtml();
}

블록과 들여쓰기

  • 중첩구조(if/else 문, while 문 등)에 들어가는 블록은 한 줄이어야 한다.
  • 각 함수 별 들여쓰기 수준이 2단을 넘어서면 안된다.
  • 그래야 함수는 더욱 읽고 이해하기 쉬워진다.

한가지만 해라!

  • 함수는 한가지를 해야 한다. 그 한가지를 잘 해야 한다. 그 한가지만을 해야 한다.
  • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 하는 것이다.
  • 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출 할 수 있다면 해당 함수는 여러작업을 하는것이다.

    추상화 : 복잡한 자료, 모듈, 시스템등으로 부터 핵심적인 개념 또는 기능을 간추려 내는 것

함수 내 섹션

  • 함수를 여러 섹션으로 나눌 수 있다면 그 함수는 여러작업을 한다는 증거다.

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

  • 함수가 '한가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 된다.
  • 만약 한 함수 내에 추상화 수준이 섞이게 된다면 읽는 사람이 헷갈린다.(근본 개념인지 세부사항인지 헷갈리므로)
  • 근본 개념과 세부사항을 뒤섞기 시작하면, 깨진 창문처럼 함수에 세부사항을 점점 추가하게 된다.

위에서 아래로 코드 읽기:내려가기 규칙

  • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
  • 위에서 아래로 읽으면 함수 추상화 부분이 한번에 한단계씩 낮아진다. (내려가기 규칙)

Switch 문

  • switch 문은 작게 만들기 어렵다. (if/else 의 연속도 포함)
  • 다형성을 이용하여 switch 문을 abstract factory에 숨겨 다형적 객체를 생성하는 코드 안에서만 switch를 사용하도록 한다.
  • 다만 불가피하게 switch문을 써야될 상황도 생긴다.
  • 예제 1
    • 함수가 길다
    • 한가지 작업만 수행하지 않는다.
    • SRP(Single Responsibility Principle) 위반
    • OCP(Open Closed Principle) 위반
// Bad
public Money calculatePay(Employee e) throws InvalidEmployeeType {
  switch (e.type) {
    case COMMISSIONED:
      return calculateCommissionedPay(e); 
  	case HOURLY:
      return calculateHourlyPay(e); 
  	case SALARIED:
      return calculateSalariedPay(e); 
  	default:
      throw new InvalidEmployeeType(e.type); 
  }
}

// Good
public abstract class Employee {
  public abstract boolean isPayday();
  public abstract Money calculatePay();
  public abstract void deliverPay(Money pay);
}
-----------------
public interface EmployeeFactory {
 Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
-----------------
public class EmployeeFactoryImpl implements EmployeeFactory {
  public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
    switch (r.type) {
      case COMMISSIONED:
        return new CommissionedEmployee(r) ;
      case HOURLY:
        return new HourlyEmployee(r);
      case SALARIED:
        return new SalariedEmploye(r);
      default:
        throw new InvalidEmployeeType(r.type);
    }
  }
}

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

“코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다” - 워드 커닝햄(Ward Cunningham)

  • 함수가 작고 단순할수록 서술적인 이름을 고르기 쉬워진다.
  • 겁먹을 필요없다, 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
  • 이름을 정하느라 시간을 들여도 괜찮다. 이런저런 이름을 넣어 코드를 읽어보면 더 좋다.
  • 서술적인 이름을 사용하면 머릿속에서 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
  • 일관성 있게 이름을 붙여야 한다.

함수 인수

  • 함수에서 이상적인 인수 개수는 0개 (무항).
  • 인수는 코드 이해에 방해가 되는 요소이므로 최선은 0개이고, 차선은 1개뿐인 경우다.
  • 테스트 관점에서도 인수가 많아지면 인수조합을 검증하는 테스트 케이스 작성도 어려워진다.
  • 출력인수(함수의 반환 값이 아닌 입력 인수로 결과를 받는 경우)는 코드를 재차 확인하게 만드므로 쓰지 않는것이 좋다.

많이 쓰는 단항 형식

  • 인수에 질문을 던지는 경우
    • boolean fileExists("MyFile");
  • 인수를 뭔가로 변환해 결과를 변환하는 경우
    • InputStream fileOpen("MyFile");
  • 이벤트 함수일 경우 (이 경우에는 이벤트라는 사실이 코드에 명확하게 드러나야 한다.)
  • 위의 예가 아니라면 단항함수는 가급적 피한다.
  • 예시 1, 입력 인수를 변환하는 함수라면 변환결과는 반환값으로 돌려준다. 출력인수 사용 X.
// Bad
void transform(StringBuffer out);

// Good
StringBuffer transform(StringBuffer in);

플래그 인수

  • 플래그 인수는 추하다. 쓰지마라, boolean 값을 넘기는 관례는 정말로 끔찍하다.
  • bool 값을 넘기는 것 자체가 그 함수는 한꺼번에 여러가지 일을 처리한다고 공표하는 것과 마찬가지다.

이항 함수

  • 단항 함수보다 이해하기가 어렵다.
  • Point 클래스의 경우에는 이항 함수가 적절하다. 2개의 인수간의 자연적인 순서가 있어야함 Point p = new Point(x,y);
  • 당연하게 여겨지는 assertEquals(expected, actual) 에서도 인수를 바꿔 넣는 경우가 많은 것처럼 인위적으로 순서를 기억해야한다.
  • 무조건 나쁜 것은 아니지만, 인수가 2개이니 만큼 이해가 어렵고 위험이 따르므로 가능하면 단항으로 바꾸도록 애써야 한다.

삼항 함수

  • 이항 함수보다 이해하기가 훨씬 어렵다.
  • 위험도 2배 이상 늘어난다. 삼항 함수를 만들 때는 신중히 고려하라.
  • 예시 1, 인수가 많아질 수록 안좋다.
assertEquals(expected, actual);
assertEquals(message, expected, actual)

인수 객체

  • 인수가 많이 필요할 경우, 일부 인수를 독자적인 클래스 변수로 선언할 가능성을 살펴보자.
  • 객체를 생성해 인수를 줄이는 방법이 눈속임이라 여결지 모르지만 그렇지 않다. 개념을 표현할 수 있게 해준다.
  • 예시 1
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

인수 목록

  • 때로는 인수 개수가 가변적인 함수도 필요하다. String.format 메서드가 좋은 예.
  • 가변 인수 전보를 동등하게 취급하면 List 형 인수 하나로 취급할 수 있다.
  • 예시 1, String.format 은 사실상 이항 함수다.
public String format(String format, Object... args);

동사와 키워드

  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야한다.
    • write(name) 보다는 writeField(name);
  • 함수이름에 키워드(인수 이름)을 추가하면 인수 순서를 기억할 필요가 없어진다.
    • assertEquals 보다는 assertExpectedEqualsActual(expected, actual);

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

  • 부수효과는 거짓말이다.
  • 함수에서 한가지를 하겠다고 약속하고는 남몰래 다른 짓을 하는 것이므로, 한 함수에서는 딱 한가지만 수행할 것!
  • 예시 1
    • Session.initialize(); 호출은 부수 효과다.
    • 이러한 부수효과는 시간적 결합을 초래 한다. 세션을 지울수있는 특정 상황에서만 호출 가능하므로
    • 하단 메서드는 checkPasswordAndInitializeSession 이라는 이름이 맞지만 이는 함수가 '한 가지' 만한다는 규칙을 위반한다.
public class UserValidator {
  private Cryptographer cryptographer;
  public boolean checkPassword(String userName, String password) {
    User user = UserGateway.findByName(userName);
    if (user != User.NULL) {
      String codedPhrase = user.getPhraseEncodedByPassword();
      String phrase = cryptographer.decrypt(codedPhrase, password);
      if ("Valid Password".equals(phrase)) {
        Session.initialize();
        return true;
      }
    }
    return false;
  }
}

출력인수

  • 일반적으로 출력 인수는 피해야 한다.
  • 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택하라.
  • 예시 1
// Bad
appendFooter(report);

// Good
report.appendFooter();

명령과 조회를 분리하라!

  • 함수는 뭔가 객체 상태를 변경하거나, 객체 정보를 반환하거나 둘 중 하나다. 둘 다 하면 안된다.
  • public boolean set(String attribute, String value); 같은 경우에는 속성 값 설정 성공 시 true 를 반환하므로 괴상한 코드다.
  • 예시 1
// Bad
if (set("username", "unclebob"))...

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

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

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 위반한다.
  • 예시1, if (deletePage(page) == E_OK)
  • try/catch 를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해 진다.

Try/Catch 블록 뽑아내기

  • try/catch 블록은 원래 추하다.
  • 정상 동작과 오류 처리 동작을 섞는다. 그러므로 try/catch 블록을 별도 함수로 뽑아내는것이 좋다.
  • 예시 1
// Bad
if (deletePage(page) == E_OK) {
  if (registry.deleteReference(page.name) == E_OK) {
  	if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
      logger.log("page deleted");
  	} else {
      logger.log("configKey not deleted");
  	}
  } else {
    logger.log("deleteReference from registry failed"); 
  } 
} else {
  logger.log("delete failed"); return E_ERROR;
}

// Good
try {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
  logError(e);
}

// Better
public void delete(Page page) {
  try {
    deletePageAndAllReferences(page);
  } catch (Exception e) {
    logError(e);
  }
}
private void deletePageAndAllReferences(Page page) throws Exception {
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
  logger.log(e.getMessage());
}

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

  • 함수는 '한 가지' 작업만 해야하며, 오류 처리도 '한 가지' 작업에 속한다.
  • 그러므로 오류 처리 함수는 오류만 처리해야 마땅하다.
  • 함수에 try 키워드가 있다면 try 문으로 시작해 catch/finally 문으로 끝나야 한다.

Error.java 의존성 자석

  • 오류를 처리하는 곳곳에서 오류코드를 사용한다면 enum class를 쓰게 되는데 이런 클래스는 의존성 자석이다.
  • 새 오류코드를 추가하거나 변경할 때 관련 클래스 전부를 다시 컴파일하고 다시 배치해야한다.
  • 그러므로 예외를 사용하는 것이 더 안전하다.
public enum Error { 
  OK,
  INVALID,
  NO_SUCH,
  LOCKED,
  OUT_OF_RESOURCES, 	
  WAITING_FOR_EVENT;
}

반복하지 마라!

  • 중복은 모든 소프트웨어에서 모든 악의 근원이므로 늘 중복을 없애도록 노력해야한다.
  • 구조적 프로그래밍, AOP, COP 모두 어떤면에서 중복 제거 전략이다.

구조적 프로그래밍

  • 다익스트라의 구조적 프로그래밍의 원칙을 따르자면 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나여야 된다.
  • 즉, 함수는 return문이 하나여야 되며, 루프 안에서 break나 continue를 사용해선 안된며 goto는 절대로, 절대로 안된다.
  • 그런데 구조적 프로그래밍의 목표와 규율은 공감하지만 함수가 작다면 위 규칙은 별 이익을 제공하지 못한다.
  • 함수가 아주 클 때만 상당한 이익을 제공한다. 그러므로 함수를 작게 만든다면 간혹 return, break, continue를 사용해도 괜찮다.
  • 오히려 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.

함수를 어떻게 짜죠?

  • 소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다.
  • 처음에는 길고 복잡하고, 들여쓰기 단계나 중복된 루프도 많다. 인수목록도 길다.
  • 하지만, 이 코드들을 빠짐없이 테스트하는 단위 테스트 케이스도 만들고, 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다.
  • 처음부터 탁 짜내지 않는다. 그건 아무도 안된다.

결론

  • 대가 프로그래머는 시스템을 (구현할) 프로그램이 아니라 (풀어갈) 이야기로 여긴다.
  • 여기서 설명한 규칙을 따른다면 길이가 짧고, 이름이 좋고, 체계가 잡힌 함수가 나온다.
  • 하지만 진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다는 사실을 명심해야한다.
  • 작성한 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아 떨어져야 이야기를 풀어가기 쉬워진다.
profile
Backend Developer

0개의 댓글