July 19, 2021
회사에서 ‘클린코드’ 책 스터디를 진행하고 있어 공부한 내용을 정리해보려고 한다.
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);
}
}
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 SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
추상 팩토리란?
서로 관련이 있는 객체들을 통째로 묶어서 팩토리 클래스로 만들고, 이들 팩토리를 조건에 따라 생성하도록 다시 팩토리를 만들어서 객체를 생성하는 패턴
즉, 관련성 있는 여러 종류의 객체를 일관된 방식으로 생성하는 경우에 유용하다.
추상 팩토리는 싱글톤 패턴, 팩토리 매서드 패턴을 사용함
ex) 삼성 컴퓨터 객체 공장, LG 컴퓨터 객체 공장
assertEquals(message, expected, actual)
는 첫 인수가 expected 라고 예상되어 주춤하게 되지만, assertEquals(1.0, amount, .001)
은 부동소수점 비교가 상대적이라는 사실을 인지할 수 있어 그리 음험하지 않은 삼항함수Circle makecircle(double x, double y, double radius)
와 Circle makeCircle(Point center, double radius)
와 같이 x, y를 묶어 넘기려면 결국 이름을 붙여야 하므로 개념을 표현해줄 수 있다.String.format
메서드가 좋은 예시String.format
선언부를 살펴보면 public String format(String format, object... args)
로 되어 있다.ex)write(name)
writeField(name)
처럼 사용하면 이름(name)이 필드(field) 라는 사실이 분명히 드러날 수 있다.assertEquals
보다 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
}
}
}
}
appendFooter(s)
는 무언가에 s를 바닥글로 첨부할까? 아니면 s에 바닥글을 첨부할까? 이는 함수의 선언부를 찾아봐야 분명해진다.public void appendFooter(StringBuffer report)
report.appendFooter()
와 같이 호출하는 방식이 좋다.public boolean set(String attribute, String value)
이 함수는 attribute 인 속성을 찾아 값을 value로 설정한 후 성공하면 true를 반환하고 실패하면 false를 반환하는 함수이다.if (set("username", "unclebob")) ...
와 같이 괴상한 코드가 나온다.해결책은 명령과 조회를 분리해 혼란을 애초에 뿌리뽑는 방법이다.
if (attributeExists("username")) {
setAttribute("username", "unclebob");
.......
}
if (deletePage(page) == E_OK)
이 코드는 동사/형용사 혼란을 일으키지 않는 대신 여러 단계로 중첩되는 코드를 야기시킴오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힘
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());
}
로 작성시 더 깔끔해진다.
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()); }
- 정상 동작과 어류 처리 동작을 위처럼 분리하면 코드를 이해하고 수정하기 더 쉬워진다.
#### 오류 처리도 한 가지 작업이다.
- 앞에서 말했듯이 함수는 '한 가지' 작업만 해야 하는데 오류 처리도 한 가지 작업에 속한다. 그러므로 오류 처리 하는 함수는 오류만을 처리해야 마땅하다.
#### Error.java 의존성 자석
- 오류 코드를 반환한다는 이야기는, 클래스든 열거형 변수든, 어디선가 오류 코드를 정의 한다는 뜻(ex. constants_pb의 Error)
```java
public enum Error {
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}
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();
}
앞에 3-1 예제 코드에서 SetUp, SuiteSetUp, TearDown, SuiteTearDown
이 반복된다.
public class SetupTeardownIncluder {
private PageData pageData;
private boolean isSuite;
private WikiPage testPage;
private StringBuffer newPageContent;
private PageCrawler pageCrawler;
public static String render(PageData pageData) throws Exception {
return render(pageData, false);
}
public static String render(PageData pageData, boolean isSuite) throws Exception {
return new SetupTeardownIncluder(pageData).render(isSuite);
}
private SetupTeardownIncluder(PageData pageData) {
this.pageData = pageData;
testPage = pageData.getWikiPage();
pageCrawler = testPage.getPageCrawler();
newPageContent = new StringBuffer();
}
private String render(boolean isSuite) throws Exception {
this.isSuite = isSuite;
if (isTestPage())
includeSetupAndTeardownPages();
return pageData.getHtml();
}
private boolean isTestPage() throws Exception {
return pageData.hasAttribute("Test");
}
private void includeSetupAndTeardownPages() throws Exception {
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
private void includeSuiteSetupPage() throws Exception {
include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}
private void includeSetupPage() throws Exception {
include("Setup", "-setup");
}
private void includePageContent() throws Exception {
newPageContent.append(pageData.getContent());
}
private void includeTeardownPages() throws Excepton {
includeTeardownPage();
if (isSuite)
includeSuiteTeardownPage();
}
private void includeTeardownPage() throws Exception {
include("TearDown", "-teardown");
}
private void includeSuiteTeardownPage() throws Exception {
include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
}
private void updatePageContent() throws Exception {
pageData.setContent(newPageContent.toString());
}
private void include(String pageName, String arg) throws Exception {
WikiPage inheritedPage = findInheritedPage(pageName);
if ( inheritedPage != null ) {
String pagePathName = getPathNameForPage(inheritedPage);
buildIncludeDirective(pagePathName, arg);
}
}
private WikiPage findInheritedPage(String pageName) throws Exception {
return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}
private String getPathNameForPage(WikiPage page) throws Exception {
WikiPagePath pagePath = pageCrawler.getFullPath(page);
return PathParser.render(pagePath);
}
private void buildIncludeDirective(String pagePathName, String arg) {
newPageContent
.append("\n!include ")
.append(arg)
.append(" .")
.append(pagePathName)
.append("\n");
}
}
- 3-7 코드에서 include 방법으로 중복을 없앤다.
- 코드를 중복하면 코드 길이가 늘어날 뿐 아니라 알고리즘/로직이 변하면 중복 되어 있는곳 모두 손을 봐야 한다. 게다가 어느 한곳이라도 빠뜨리면 오류가 발생할 확률도 높아진다.
- 객체 지향 프로그래밍에서는 코드를 부모 클래스로 몰아 중복을 없앤다.
### 구조적 프로그래밍
- 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다고 한다. 즉, return문이 하나여야 한다.
- 루프 안에서 break, continue를 사용해선 안되며 goto는 절대로 절대로 안된다.
- 함수가 작다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다. 오히려 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
### 함수를 어떻게 짜죠?
- 소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다. 논문이나 기사를 작성할 때 서투르고 중구난방의 어수선한 초안을 작성한다.
- 함수를 짤 대도 마찬가지. 처음에는 길고 복잡하고 들여쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 아주 길다. 이름은 즉흥적이도 코드는 중복된다. 하지만 그 서투른 코드를 빠짐없이 테스트 하는 단위 테스트 케이스도 만든다.
- 그런 후 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 이 와중에도 단위 테스트는 항상 통과한다. (테스트 코드의 중요성...ㅠㅠ)
- 그래서 최정적으로 이 장에서 설명한 규칙을 따르는 함수가 얻어진다.
### 결론
- 함수는 그 언어에서 동사이며, 클래스는 명사이다. 프로그래밍의 기술은 언제나 언어 설계의 기술이다.
- 프로그래밍을 잘하는 프로그래머들은 시스템을 구현할 프로그램이 아니라 풀어갈 이야기로 여긴다.
- 프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 좀 더 표현력이 강한 언어를 만들어 이야기를 풀어가는 것
- 여기서 시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그 언어에 속한다.
- 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기를 풀어가기가 쉬워진다는 사실을 기억하기 바란다.