CleanCode 05 형식 맞추기

1. 형식을 맞추는 목적

코드의 형식은 중요하다 너무 중요해서 무시하기 어렵다. 너무나도 중요하므로 융통성 없이 맹목적으로 따르면 안된다. 코드 형식은 의사소통의 일환이다.
코드의 가독성은 앞으로 바꾸리 코드의 품질에 지대한 영향을 미친다. 오랜시간이 지나 원래 코드의 흔적을 더 이상 찾아보기 어려울 정도로 코드가 바뀌어도 맨 처음 잡아 놓은 구현스타일과 가독성 수준은 유지보수 용이성확장성에 계속 영향을 미친다.

2. 적절한 행 길이를 유지하라

소스코드는 얼마나 길어야할까요?

JUnit 자바 전체 파일중 대략 1/3이 40줄에서 100줄 조금 넘는 정도이다. 가장 긴 파일은 500줄을 넘지 않으면 평균 200줄 정도를 기록한다. 충분히 이정도로 거대한 시스템을 구축할 수 있다는 사실이다. 일반적으로 큰 파일보다는 작은파일이 이해하기 쉽다.

2.1. 신문 기사처럼 작성하라

신문기사처럼 이름은 간단하고 설명이 가능하게 짓는다. 그리고 이름만 보고도 올바른 모듈을 살펴보고 있는지 아닌지를 판단할 정도로 신경 써서 짓는다. 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명한다. 아래로 내려갈수록 의도를 세세하게 묘사한다. 마지막에는 가장 저차원적인 함수와 세부 내역이 나온다.

2.2. 개념은 빈 행으로 분리하라

빈 행을 넣지 않을 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
public static final String REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
Pattern.MULTILINE + Pattern.DOTALL);
public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = pattern.matcher(text); match.find();
addChildWidgets(match.group(1));}
public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>");
html.append(childHtml()).append("</b>");
return html.toString();
}
}

빈행만으로 훨씬 가독성 좋은 코드를 만들 수 있다.

빈 행을 넣을 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package fitnesse.wikitext.widgets;

import java.util.regex.*;

public class BoldWidget extends ParentWidget {
public static final String REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
Pattern.MULTILINE + Pattern.DOTALL
);

public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = pattern.matcher(text);
match.find();
addChildWidgets(match.group(1));
}

public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>");
html.append(childHtml()).append("</b>");
return html.toString();
}
}

2.3. 세로 밀집도

줄바꿈이 개념을 분리한다면 세로 밀집도는 연관성을 의미한다.

1
2
3
4
5
6
7
8
public class ReporterConfig {
private String m_className;
private List<Property> m_properties = new ArrayList<Property>();

public void addProperty(Property property) {
m_properties.add(property);
}
}

2.4. 수직 거리

함수 연관관계와 동작방식을 이해하려고 이 함수에서 저 함수를 오가며 소스 파일을 위아래로 뒤지는 실수를 한 경험이 있는가?
서로 밀접한 개념들은 세로로 가까이 둬야한다. 타당한 근거가 없다면 서로 밀접한 개념은 한 파일에 속해야 마땅하다. 이게 바로 protected 변수를 피해야하는 이유중 하나이다.

변수선언은 사용하는 위치에서 최대한 가까이 선언한다. 인스턴스변수는 클래스 맨 처음(혹은 끝)에 선언한다.

종속 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class WikiPageResponder implements SecureResponder { 
protected WikiPage page;
protected PageData pageData;
protected String pageTitle;
protected Request request;
protected PageCrawler crawler;

public Response makeResponse(FitNesseContext context, Request request) throws Exception {
String pageName = getPageNameOrDefault(request, "FrontPage");
loadPage(pageName, context);
if (page == null)
return notFoundResponse(context, request);
else
return makePageResponse(context);
}

private String getPageNameOrDefault(Request request, String defaultPageName) {
String pageName = request.getResource();
if (StringUtil.isBlank(pageName))
pageName = defaultPageName;

return pageName;
}

protected void loadPage(String resource, FitNesseContext context)
throws Exception {
WikiPagePath path = PathParser.parse(resource);
crawler = context.root.getPageCrawler();
crawler.setDeadEndStrategy(new VirtualEnabledPageCrawler());
page = crawler.getPage(context.root, path);
if (page != null)
pageData = page.getData();
}

private Response notFoundResponse(FitNesseContext context, Request request)
throws Exception {
return new NotFoundResponder().makeResponse(context, request);
}

private SimpleResponse makePageResponse(FitNesseContext context)
throws Exception {
pageTitle = PathParser.render(crawler.getFullPath(page));
String html = makeHtml(context);
SimpleResponse response = new SimpleResponse();
response.setMaxAge(0);
response.setContent(html);
return response;
}

종속 함수 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 또한 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다. 다음으로 호출하는 함수는 그 아래에 정의한다. 호출되는 함수를 찾기가 쉬워지며 전체 가독성도 높아진다

개념의 유사성

개념적인 친화도가 높을 수록 코드를 가까이 배치한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Assert {
static public void assertTrue(String message, boolean condition) {
if (!condition)
fail(message);
}

static public void assertTrue(boolean condition) {
assertTrue(null, condition);
}

static public void assertFalse(String message, boolean condition) {
assertTrue(message, !condition);
}

static public void assertFalse(boolean condition) {
assertFalse(null, condition);
}

친화도가 높은 요인은 여러가지이다. 그 중 하나가 직접적인 종속성이다. 이런 경우에는 종속성은 부차적 요인이므로 종속적인 관게가 없더라도 가까이 배치하도록한다.

3. 가로 형식 맞추기

한 행은 가로로 얼마나 길어야 적당할까? 프로그래머는 짧은 행을 선호하고 보통 40자 정도가 된다. 약 120자 정도로 제한할 것을 권고한다.

가로 공백과 밀집도

가로로는 공백을 사용해 밀접한 개념과 느슨한 개념을 표현한다. 공백을 넣으면 두가지 주요 요소가 확실히 나뉜다는 사실이다.
반면, 함수 이름과 이어지는 괄호 사이에는 공백을 넣지 않았다. 함수와 인수는 서로 밀접하기 때문이다.

1
2
addLine(lineSize, lineCount);
return b*b - 4*a*c;

수식을 읽기가 매우 편하다. 곱셈은 우선순위가 가장 높다. 항 사이에는 공백이 들어간다. 덧셈과 뺄셈은 우선순위가 곱셈보다 낮기때문이다.

1
return (-b - Math.sqrt(determinant)) / (2*a);

하지만, 도구에서 없애는 경우가 흔하다

가로 정렬

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FitNesseExpediter implements ResponseSender {
private Socket socket;
private InputStream input;
private OutputStream output;
protected long requestParsingTimeLimit;
private long requestProgress;
private long requestParsingDeadline;
private boolean hasError;

public FitNesseExpediter(Socket s,
FitNessContext context) throws Exception {
this.context = context;
socket = s;
requestParsingTimeLimit = 10000;
}

깔끔해보일지 모르지만, 엉뚱한 부분을 강조해 진짜 의도가 가려지기때문이다. 예를 들어, 위 선언부를 읽다보면 변수유형은 무시하고 변수이름부터 읽게 된다. 코드 형식을 자동으로 맞춰주는 도구는 대다수가 위와 같은 정렬을 무시한다.

3.1. 들여쓰기

소스파일은 윤곽도와 계층이 비슷하다. 파일 전체에 적용되는 정보가 있고, 파일 내 개별 클래스에 적용되는 정보가 있고, 클래스 내 각 메서드에 적용되는 정보가 있고, 블록 내 블록에 재귀적으로 적용되는 정보가 있다.
들여쓰기 한 파일은 한눈에 구조가 들어온다. 때로는 간단한 if문, while문, 짧은 함수에서 들여쓰기 규칙을 무시하고픈 유혹이 생긴다. 한 행에 뭉뚱그려놓은 코드를 지양해라!

3.2. 가짜 범위

이상한 세미콜론을 적용하지 말자.

1
2
while()
;

4. 팀 규칙

팀은 한가지 규칙을 정하고 팀원들은 규칙을 따라야한다. IDE코드 형식기를 설정하거나 규칙을 정해라!

5. 밥 아저씨의 형식 규칙

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class CodeAnalyzer implements JavaFileAnalysis { 
private int lineCount;
private int maxLineWidth;
private int widestLineNumber;
private LineWidthHistogram lineWidthHistogram;
private int totalChars;

public CodeAnalyzer() {
lineWidthHistogram = new LineWidthHistogram();
}

public static List<File> findJavaFiles(File parentDirectory) {
List<File> files = new ArrayList<File>();
findJavaFiles(parentDirectory, files);
return files;
}

private static void findJavaFiles(File parentDirectory, List<File> files) {
for (File file : parentDirectory.listFiles()) {
if (file.getName().endsWith(".java"))
files.add(file);
else if (file.isDirectory())
findJavaFiles(file, files);
}
}

public void analyzeFile(File javaFile) throws Exception {
BufferedReader br = new BufferedReader(new FileReader(javaFile));
String line;
while ((line = br.readLine()) != null)
measureLine(line);
}

private void measureLine(String line) {
lineCount++;
int lineSize = line.length();
totalChars += lineSize;
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}

private void recordWidestLine(int lineSize) {
if (lineSize > maxLineWidth) {
maxLineWidth = lineSize;
widestLineNumber = lineCount;
}
}

public int getLineCount() {
return lineCount;
}

public int getMaxLineWidth() {
return maxLineWidth;
}

public int getWidestLineNumber() {
return widestLineNumber;
}

public LineWidthHistogram getLineWidthHistogram() {
return lineWidthHistogram;
}

public double getMeanLineWidth() {
return (double)totalChars/lineCount;
}

public int getMedianLineWidth() {
Integer[] sortedWidths = getSortedWidths();
int cumulativeLineCount = 0;
for (int width : sortedWidths) {
cumulativeLineCount += lineCountForWidth(width);
if (cumulativeLineCount > lineCount/2)
return width;
}
throw new Error("Cannot get here");
}

private int lineCountForWidth(int width) {
return lineWidthHistogram.getLinesforWidth(width).size();
}

private Integer[] getSortedWidths() {
Set<Integer> widths = lineWidthHistogram.getWidths();
Integer[] sortedWidths = (widths.toArray(new Integer[0]));
Arrays.sort(sortedWidths);
return sortedWidths;
}
}

Bob 아저씨의 코드규칙을 참고해보도록 하자!

  1. 함수 () { } 에서 괄호이후 한칸을 띄운다.
  2. if문이 한줄일 경우 다음칸으로 띄우고 들여쓰기를 하자
  3. 코드의 의미를 명확하게 한다.