어떠한 프로그램이든 가장 기본적인 단위는 "함수"이다.
public static String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
WikiPage wikiPage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();
if (pageData.hasAttribute("Test")) {
if (includeSuiteSetup) {
WikiPage suiteSetup =
PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage
);
if (suiteSetup != null) {
WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -setup .")
.append(pagePathName).append("\n");
}
}
WikiPage setup = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
if (setup != null) {
WikiPagePath setupPath =
wikiPage.getPageCrawler().getFullPath(setup);
String setupPathName = PathParser.render(setupPath);
buffer.append("!include -setup .")
.append(setupPathName).append("\n");
}
}
buffer.append(pageData.getContent());
if (pageData.hasAttribute("Test")) {
WikiPage teardown = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
if (teardown != null) {
WikiPagePath tearDownPath =
wikiPage.getPageCrawler().getFullPath(teardown);
String tearDownPathName = PathParser.render(tearDownPath);
buffer.append("\n")
.append("!include -teardown .").append(tearDownPathName).append("\n");
}
}
if (includeSuiteSetup) {
WikiPage suiteTeardown =
PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage
);
if (suiteTeardown != null) {
WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath(suiteTeardown);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -teardown .")
.append(pagePathName).append("\n");
}
}
}
pageData.setContent(buffer.toString());
return pageData.getHtml();
}
위와 같은 코드는 한 함수에 다양한 추상화 수준을 가지고 있고, 코드 또한 너무 길다.
(SLAP, Single Level of Abstraction principle, 단일 레벨 추상화 원칙에 어긋난다)
위의 코드를 메소드는 간단하게 아래와 같이 몇개의 메소드로 분리할 수 있다.
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();
}
위의 함수보다는 아래의 함수가 읽고 이해하기 쉽다.
하지만 위의 함수 또한 뭔가 장황하다.
따라서, 결과적으로는 다음과 같이 짧은 함수로 변경되어야 한다.
public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite); return pageData.getHtml();
}
추천 영상 : youtu.be/GYNT7O3rLhU
읽기 쉬운 함수를 만드는 규칙
1. 작게 만들어라
각 함수는 하나의 이야기다.
장황한 이야기는 이해하기 어렵다.
2. 블록과 들여쓰기도 최소화해라
if/else 의 블록문들도 결국 한줄이어야 한다.
그리고 해당 블록문에서 함수를 호출하여야 한다.
블록안에서 호출되는 함수명을 통해 코드를 이해하기 훨씬 쉬워진다.
들여쓰기 또한 2단을 넘어서면 안된다. (중첩문 최소화)
3. 한가지만 하라.
함수는 한가지를 해야 하고, 그 한가지를 잘 해야 하며, 그 한가지만을 해야 한다.
함수의 이름이 나타내는 하위 추상화 단계를 수행한다면 그 함수는 한가지 작업만 하는 것이다.
함수에서 의미 있는 이름으로 2가지 이상의 함수를 추출할 수 있다면 그 함수는 여러가지 일을 하는 함수이다.
4. 함수 당 추상화 수준은 하나만 가져라.
한가지만 하기 위해서는 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
추상화의 수준이 섞이면, 필수 개념의 코드인지, 세부적인 내용의 코드인지 코드를 읽는 사람이 헷갈릴 수 밖에 없다.
고수준의 추상화와 저수준의 추상화가 섞이기 시작하면, 더 많은 저 수준의 추상화가 함수에 증가하게 된다.
5. 위에서 아래로 이야기 처럼 읽히도록 하라.
한가지만 하게 되면, 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 오게 된다.
따라서, Top-down 방식으로 이야기 하는 것 처럼 읽혀야 한다.
내려가기 규칙(Step Down Rule).
* To include the setups and teardowns, we include setups, then we include the test page con- tent, and then we include the teardowns.
- To include the setups, we include the suite setup if this is a suite, then we include the regular setup.
- To include the suite setup, we search the parent hierarchy for the “SuiteSetUp” page and add an include statement with the path of that page.
- To search the parent. . .
위와 같이 함수를 일련의 문단을 읽어 내려가듯이 코드를 구현하면 추상화 수준을 일관되게 유지하기가 쉬워진다.
6. Switch문이 포함된 함수는 다형성을 이용하라.
Switch문은 N가지의 일을 처리하기 때문에 항상 길 수 밖에 없다.
저차원 클래스에 Switch 에서 수행할 작업을 옮기고, 다형성을 통해 반복 수행되지 않도록 해야 한다.
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);
}
}
위의 Switch 예제는 다음과 같은 문제점이 있다.
- 두가지 이상의 일을 한다.
- SRP 위반. 코드가 변경되어야 할 이유가 여럿이기 때문
- OCP 위반. 새 직원 유형이 추가될때마다 코드가 수정되어야 하기 때문.
- Employee 클래스를 사용하는 다른 곳에서도 동일한 구조의 함수가 중복으로 존재할 가능성이 있음.
public abstract class Employee {
public abstract public abstract public abstract boolean isPayday();
Money calculatePay();
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 SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
따라서 위와 같이 추상 팩토리(Abstract Factory)에 Switch문을 숨긴다.
팩토리(Factory)는 Switch 문을 통해 적절히 Employee 파생 클래스의 인스턴스를 생성하여 전달하고, 각 파생 클래스의 주요 함수는 Employee 인터페이스를 통해 호출할 수 있도록 하여, 다형성을 활용하여 실제 파생 클래스의 함수가 실행되도록 한다.
7. 서술적인 이름을 사용하라.
서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로, 코드를 개선하기 쉬워진다.
모듈내에서 일관성 있는 같은 문구, 명사, 동사를 사용하여 함수 이름을 지어야 한다.
8. 최적의 인자를 사용하라.
인자는 함수의 개념을 이해하기 어렵게 만든다.
인자가 많아지면, 테스트 케이스가 복잡해지고, 이는 코드가 복잡해짐을 의미한다.
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
인자가 많아지면, 객체 인자를 고려해야 한다.
객체 인자와 객체 인자의 이름은 결국 개념을 표현할 수 있게 해주므로, 하고자 하는 일을 이해하는데 도움을 준다.
write(name)
writeField(name)
함수와 인자는 동사 / 명사 쌍을 이뤄야 한다.
void transform(StringBuffer out) 과 같이, 입력 인수를 변환하여 인수를 반환하는 것은 옳지 않다.
StringBuffer transform(StringBuffer in) 과 같이, 반환 함수 형식을 따르는 것이 좋다.
9. 부수효과를 일으키지 마라.
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()" 호출이다.
checkPasswork 라는 함수명은 패스워드를 검사한다는 의미이지 세션을 초기화 한다는 의미가 아니다.
해당 부수 효과가 필요한 경우 SRP(Single Responsibility Principle) 위반하더라도
함수명을 "checkPasswordAndInitializeSession" 과 같이 명확한 이름을 지어주는 것이 좋다.
10. 명령과 조회를 분리해라.
함수는 어떤 일을 수행하거나, 답하는 둘 중 하나만 해야 한다.
public boolean set(String attribute, String value);
if (set("username", "unclebob")) {
...
}
위의 set 함수는 "수행"하는 함수인가? "답"을 하는 함수인지 분간하기가 어렵다.
set 함수의 이름을 "setAndCheckIfExists" 와 같이 변경해도 되지만, if 문과의 가독성을 고려했을 때 어색하다.
if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}
진정한 해결책은, 위의 코드와 같이 명령(setAttribute) 함수와 조회(attributeExists) 함수를 분리해 혼란을 최소화 하는 것이다.
11. 에러 코드보다 예외를 사용하라.
if (deletePage(page) == E_OK)
명령 함수에서 오류 코드를 반환하도록 하면, 다음과 같이 if 문에서 명령을 표현식으로 사용하게 된다.
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;
}
그리고 이러한 에러 코드 반환은 위와 같이 오류 코드를 처리 하기 위해 중첩코드를 작성하게 된다.
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
logger.log(e.getMessage());
}
반면에 위와 같이, 오류 코드 대신 예외를 사용하면 에러코드와 정상코드가 분리되므로 코드가 깔끔해진다.
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
또한 위와 같이 예외가 발생하는 함수에 대해서만 try/catch 로 감싸두면, 에러처리 함수를 분리할 수 있게 된다.
단, 여기서 예외 처리 함수 또한 한가지의 작업임을 알고 있어야 한다.
따라서 오류 처리를 하는 함수는 오류만 처리하여야 하고, try 로 시작해 catch/finally 문으로 끝나야 한다.
에츠허르 데이크스트라(Edsger Dijkstra) 구조적 프로그래밍의 원칙
모든 함수와 함수의 블록들은 하나의 진입점(entry)과 출구(exit)를 가진다.
따라서 함수에는 return만 사용하고, break, continue, goto는 사용을 자제해야 한다.
함수는 어떻게 짜는가?
코딩은 글짓기와 비슷하다.
논문이나 기사를 작성할때 먼저 생각을 기록한 후, 읽기 좋게 다듬는다.
초안은 대게 서투르고 어수선 하기 때문에 원하는 결론을 위해 문장과 문단을 고치고 정리하게 된다.
함수 또한, 코드 초안을 작성하고,
다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거하고, 순서를 바꾸는 다듬는 작업을 거쳐야만 한다.
그래야만 Clean Code를 얻을 수 있다.
'프로그래밍 > 클린 코드' 카테고리의 다른 글
[클린코드] 6장. 객체와 자료구조 (0) | 2020.09.27 |
---|---|
[클린코드] 5장. 형식 맞추기 (0) | 2020.08.28 |
[클린코드] 4장. 주석 (0) | 2020.08.28 |
[클린코드] 2장. 의미 있는 이름 (0) | 2020.08.19 |
[클린코드] 1장. 깨끗한 코드 (0) | 2020.08.18 |