각 함수가 이야기 하나를 표현하도록
public static String renderPageWithSetupAndTeardowns (
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();
}
public static String renderPageWithSetupAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData isSuite);
return pageData.getHtml();
}
💡 if 문/else 문/while 문 등에 들어가는 블록은 한 줄이어야 좋다.
중첩구조가 생길만큼 함수가 커져서는 안 된다는 뜻이다.
들여쓰기 수준또한 2단을 넘어서지 않아야 함수를 읽고 이해하기 쉬워진다.
지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행해야 한다.
이러한 기준에서 위의 코드는 한 가지 작업만을 하고 있는가?
renderPageWithSetupAndTeardowns()
는 페이지가 테스트 페이지인지 확인한 후 테스트 페이지라면 설정 페이지와 해제 페이지를 넣는다. 테스트 페이지든 아니든 페이지를 HTML로 렌더링한다.
얼핏 보면 세 가지 일을 하고 있는 것 같다.
이 세 가지의 일을 각각의 함수로 쪼개어 만드는 것은 의미가 없다. 이러한 의미로 추상화 수준이 하나이다.
함수가 ‘한 가지’만 하는지 판단하는 방법이 하나 더 있다. 단순히 다른 표현이 아니라 의미 있는 다른 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.
함수가 확실히 ‘한 가지’ 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다.
근본 개념과 세부사항을 뒤섞기 시작하면, 계속해서 사람들이 함수에 세부사항을 점점 더 추가하게 된다.
위에서 아래로 TO문단을 읽듯이 프로그램이 읽혀야 한다. 이렇게 코드를 구현하면 추상화 수준을 일관되게 유지하기가 쉬워진다.
<각 TO 문단은 현재 추상화 수준을 설명하며 이어지는 아래 단계 TO 문단을 참고한다.>
TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지를 포함한다.
TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함한 후 일반 설정 페이지를 포함한다.
TO 설정 페이지를 포함하려면, 부모 계층에서 “SuiteSetUp” 페이지를 찾아 include 문과 페이지 경로를 추가한다.
TO 부모 계층을 검색하려면, …
본질적으로 Switch문은 N가지를 처리한다. 따라서 작게 만들기 어렵다. 이는 if/else가 여럿 이어지는 구문도 포함된다.
public abstract class Employee {
public abstract boolean isPayDay();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
----------------------------------------------
public interface EmployeeFactory {
public 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 SalariedEmployee(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
💡 위 코드는 switch 문을 추상 팩토리에 숨긴다. 팩토리는 switch문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다.
caculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출된다. 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.
길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다. 그런 다음, 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.
서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.
함수에서 이상적인 인수 개수는 0개(무항)이다. 다음은 단항, 그 다음은 이항이다.
인수는 코드를 읽는 사람이 개념을 이해하기 어렵게 만든다. includeSetupPageInfo(new PageContent)
보다 includePageInfo()
가 이해하기 쉬운 건 당연하다.
출력 인수는 입력 인수보다 이해하기 더 어렵다. 우리는 흔히 함수에다 인수로 입력을 넘기고 반환값으로 출력을 받는다는 개념에 익숙해서 대개 함수에서 인수로 결과를 받으리라 기대하지 않는다.
최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개뿐인 경우이다.
함수에 인수 1개를 넘기는 가장 흔한 두 가지
인수에 질문을 던지는 경우
ex) boolean fileExists(”MyFile”)
인수로 뭔가를 변환해 결과를 반환하는 경우
ex) InputStream fileOpen(”Myfile”)
- String형의 파일 이름을 InputStream으로 변환한다.
입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다. 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다.
StringBuffer transform(StringBuffer in)
이 void transform(StringBuffer out)
보다 좋다. 변환 함수 형식을 따르는 것이 좋다. 적어도 변환 형태는 유지하기 때문이다.
함수로 boolean값을 넘기는 것은 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 것과 마찬가지이다. 플래그가 참이면 이것을 하고 거짓이면 저걸 한다는 말이기 때문이다.
둘 다 의미는 명백하지만, 전자가 더 쉽게 읽히고 더 빨리 이해된다. 후자의 outputStream과 name은 한 값을 표현하지도, 자연적인 순서가 있지도 않기 때문이다.
또한 writeField(outputStream, name)
의 경우, 첫 인수를 무시해야 한다는 사실을 깨닫는데 시간이 필요하다. 그리고 바로 이 사실이 문제를 일으킨다. 왜냐고? 무시한 코드에 오류가 숨어들기 마련이니까.
프로그래밍을 하다보면 이항 함수가 적절한 경우도 있고, 불가피하게 사용해야 하는 경우도 있다. 하지만 이항 함수를 사용하는 것에는 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 애써야 한다.
삼항 함수를 만들 때는 신중히 고려해야 된다.
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
String.format("%s worked%.2f hours.", name, hours);
public String format(String format, Object... args)
💡 String.format의 선언부를 보면 사실상 이항 함수이다.
위의 논리는 가변 인수를 취하는 모든 함수에 적용되고, 이처럼 단항, 이항, 삼항 함수로 취급할 수 있다.
writeField(name)
처럼 동사(명사) 쌍으로 이루어진 함수 이름은 읽는 누구나 이해하기 쉽다.assertExpectedEqualsActual(expected, actual)
→ 이 함수는 인수의 순서를 기억할 필요가 없어진다.개발자의 의도와 다르게 때로는 예상치 못하게 클래스 변수를 수정하거나, 함수로 넘어온 인수나 시스템 전역 변수를 수정하거나 하는 부작용을 일으키는 경우가 있다.
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;
}
}
💡 이 코드에서 함수가 일으키는 부수 효과는 무엇일까?
Session.initialize()
호출이다.
checkPassword
함수의 이름만 봐서는 세션을 초기화한다는 것을 전혀 예상할 수가 없다.
자칫 잘못 호출하면 세션 정보가 날아가기 때문에 특정 상황에서만 호출이 가능하다.
이런 부수 효과가 시간적인 결합(Temporal Coupling)을 초래한다. 시간적인 결합은 혼란을 일으킨다.
특히 이렇게 부수 효과로 숨겨진 경우에는 더더욱.
만약 시간적인 결합이 필요하다면 함수 이름에 명시해야 한다. checkPasswordAndInitializeSession()
시간적인 결합(Temporal Coupling)이란?
→ 시스템의 컴포넌트가 특정한 시간 순서로 실행되어야만 정상적으로 작동하는 경우를 의미함.
이는 시스템의 모듈 간의 순서와 타이밍에 의존성이 있음을 나타내며, 이러한 의존성은 유지보수성과 유연성을 저하시키는 원인이 된다.
<시간적인 결합의 특징>
객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.
💡 인수 s는 입력 인수일까, 출력 인수일까?appendFooter(s);
(함수 선언부를 찾아보고 나서야 s가 출력 인수라는 사실을 정확히 알 수 있다.)
public void appendFooter(StringBuffer report)
출력 인수로 사용하라고 설계한 변수가 바로 this이다. appendFooter()는 아래와 같이 호출하는 것이 좋다.
report.appendFooter();
객체 상태를 변경하거나 객체 정보를 반환하거나 둘 중 하나만 해야 한다.
아래의 set() 함수는 이름이 attribute인 속성을 찾아 값은 value로 설정한 후 성공하면 true, 실패하면 false를 반환한다.
public boolean set(String attribute, String value);
if (set("username", "unclebob")) ...
💡 이 코드는 username이 unclebob인지 확인하는 코드인가? 설정하는 코드인가?
함수를 만든 사람은 “set”을 동사로 의도했지만, 읽는 사람은 “set”이 형용사인지, 동사인지 그 의중을 한 번에 분간하기 어렵다.
💡 이럴 땐 명령과 조회를 분리해서 혼란의 여지를 없앤다.if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}
명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
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("deletedReference from registry failed");
}
} else {
logger.log("delete failed");
return E_ERROR;
}
위 코드는 동사/형용사의 혼란을 일으키지 않지만, if문이 여러 단계로 중첩되어 복잡하다.
또한 결과로 오류 코드를 반환하면 곧바로 오류 코드를 처리해야 한다는 문제도 있다.
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
logger.log(e.getMessage());
}
이렇게 try-catch 예외를 사용해서 원래의 코드에서 오류 처리 코드를 분리하면 코드가 한결 깔끔해진다.
더 나아가 정상 동작과 오류 처리 동작을 분리하기 위해 try-catch 블록을 별도의 함수로 만들어낸다.
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());
}
deletePageAndAllReferences()
함수이다.의존성 자석이 되는 오류 코드의 사용
첫번째 코드 방식처럼 오류 코드(E_ERROR)를 반환한다는 이야기는, 클래스든 열거형 변수든, 어디선가 아래의 Error 클래스처럼 오류 코드를 정의한다는 뜻이다.
public enum Error {
OK,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WATTING_FOR_EVENT;
}
만약 이후 Error enum이 변경된다면 이 오류 코드 클래스를 import한 모든 클래스를 재컴파일/재배치 해야한다.
이러한 번거로움 때문에 프로그래머는 새 오류 코드를 추가하는 대신 기존의 오류 코드를 재사용하게 된다.
오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생되므로, 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.
구조적 프로그래밍 원칙이란?
→ 단일 입/출구 규칙. 즉, 함수는 return문이 하나여야 한다는 말이다.
루프 안에서 break나 continue를 사용해선 안 되며, goto는 절대로 안 된다.
함수가 아주 클 때에는 위의 원칙을 고수하는 것이 상당한 이점을 준다.
(그에 반해 작은 함수에서는 return, break, continue를 프로그래머의 의도에 따라 적절하게 사용해도 좋다.)
소프트웨어를 짜는 것은 여느 글짓기와 비슷하다.
모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어로 만들어진다. 프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 좀 더 표현력이 강한 언어를 만들어 이야기를 풀어가는 것이다.
함수는 그 언어에서 동사이며, 클래스는 명사이다.
시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그 언어에 속한다.
우리가 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기를 풀어가기 쉬워진다는 사실을 기억하자.