|
| 1 | + |
| 2 | +# 형식 맞추기 |
| 3 | +프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야 한다. 코드 형식을 맞추기 위한 간단한 규칙을 정하고 그 규칙을 착실히 따라야 한다. |
| 4 | + |
| 5 | +## 1. 형식을 맞추는 목적 |
| 6 | + |
| 7 | +코드 형식은 의사소통의 일환이다. 맨처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 계속 영향을 미친다. |
| 8 | + |
| 9 | + |
| 10 | +## 2. 적절한 행 길이를 유지하라 |
| 11 | + |
| 12 | +적은 줄의 파일로도 커다란 시스템을 구축할수 있다. 반드시 지킬 엄격한 규칙은 아니지만 바람직한 규칙으로 삼아라. |
| 13 | +일반적으로 큰 파일보다 작은 파일이 이해하기 쉽다. |
| 14 | + |
| 15 | +### 2-1. 신문 기사처럼 작성하라 |
| 16 | + |
| 17 | +이름은 간단하면서도 설명이 가능하게 짓는다. 이름만 보고도 올바른 모듈을 살펴보고 있는지 아닌지를 판단할 정도로 신경써서 짓고, |
| 18 | +소스파일 첫 부분은 고차원 개념과 알고리즘을 설명한다. 아래로 내려갈수록 의도를 세세하게 묘사한다. |
| 19 | +마지막에는 가장 저차원 함수와 세부내역이 나온다. |
| 20 | + |
| 21 | +### 2-2. 개념은 빈 행으로 분리하라 |
| 22 | + |
| 23 | +빈 행은 새로운 개념을 시작한다는 시각적 단서다. 코드를 읽어 내려가다 보면 빈 행 바로 다음 줄에 눈길이 멈춘다. |
| 24 | +``` |
| 25 | +//빈 행으로 분리시 가독성이 좋다 |
| 26 | +package fitnesse.wikitext.widgets; |
| 27 | +
|
| 28 | +import java.util.regex.*; |
| 29 | +
|
| 30 | +public class BoldWidget extends ParentWidget{ |
| 31 | + public static final String REGEXP = "'''.+?'''" |
| 32 | + |
| 33 | + pravate static final Pattern pattern = Pattern.compile("'''(.+?)'''", Pattern.MULTILINE + Pattern.DOTALL); |
| 34 | + |
| 35 | + public BoldWidget(ParentWidget parent, String text) throws Exception { |
| 36 | + super(parent); |
| 37 | + Matcher match = pattern.matcher(text); |
| 38 | + match.find(); |
| 39 | + addChildWidgets(match.group(1)); |
| 40 | + } |
| 41 | + |
| 42 | + public String render() throws Exception { |
| 43 | + StringBuffer html = new StringBuffer("<b>"); |
| 44 | + html.append(childHtml()).append("</b>"); |
| 45 | + return html.toString(); |
| 46 | + } |
| 47 | +} |
| 48 | +
|
| 49 | +
|
| 50 | +// 빈 행이 없는 경우, 가독성이 현저히 떨어진다 |
| 51 | +package fitnesse.wikitext.widgets; |
| 52 | +import java.util.regex.Matcher; |
| 53 | +import java.util.regex.Pattern; |
| 54 | +public class BoldWidget extends ParentWidget { |
| 55 | + public static final String REGEXP = "'''.+?'''"; |
| 56 | + private static final Pattern pattern = Pattern.compile("'''(.+?)'''", |
| 57 | + Pattern.MULTILINE + Pattern.DOTALL |
| 58 | + ); |
| 59 | + public BoldWidget(ParentWidget parent, String text) throws Exception { |
| 60 | + super(parent); |
| 61 | + Matcher match = pattern.matcher(text); |
| 62 | + match.find(); |
| 63 | + addChildWidgets(match.group(1)); |
| 64 | + } |
| 65 | + public String render() throws Exception { |
| 66 | + StringBuffer html = new StringBuffer("<b>"); |
| 67 | + html.append(childHtml()).append("</b>"); |
| 68 | + return html.toString(); |
| 69 | + } |
| 70 | +} |
| 71 | +
|
| 72 | +``` |
| 73 | + |
| 74 | + |
| 75 | +### 2-3. 세로 밀집도 |
| 76 | + |
| 77 | +세로 밀집도는 연관성을 의미한다. 서로 밀접한 코드행은 세로로 가까이 놓여야 한다. |
| 78 | +``` |
| 79 | +//주석으로 두 인스턴스 변수를 떨어뜨려 놓음 |
| 80 | +public class ReporterConfig { |
| 81 | + /** |
| 82 | + * 리포터 리스너의 클래스 이름 |
| 83 | + */ |
| 84 | + private String m_className; //인스턴스 변수 1 |
| 85 | +
|
| 86 | + /** |
| 87 | + * 리포터 리스너의 속성 |
| 88 | + */ |
| 89 | + private List<Property> m_properties = new ArrayList<Property>(); //인스턴스 변수 2 |
| 90 | + public void addProperty(Property property) { |
| 91 | + m_properties.add(property); |
| 92 | +} |
| 93 | +
|
| 94 | +//두 인스턴스 변수를 밀접하게 배치 |
| 95 | +public class ReporterConfig { |
| 96 | + private String m_className; //인스턴스 변수 1 |
| 97 | + private List<Property> m_properties = new ArrayList<Property>(); //인스턴스 변수 2 |
| 98 | +
|
| 99 | + public void addProperty(Property property) { |
| 100 | + m_properties.add(property); |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | + |
| 105 | + |
| 106 | +### 2-4. 수직 거리 |
| 107 | + |
| 108 | +같은 파일에 속할 정도로 밀접한 개념은 세로거리로 연관성을 표현한다. 여기서 연관성이란 한 개념을 이해하는데 다른 개념이 중요한 정도다. |
| 109 | +연관성이 깊은 두 개념이 멀리 떨어져 있으면 코드를 읽는 사람이 소스 파일과 클래스를 여기저기 뒤지게 된다. |
| 110 | + |
| 111 | +#### 2-4-1. 변수선언 |
| 112 | + |
| 113 | +변수는 사용하는 위치에 최대한 가까이 선언한다. 지역 변수같은 경우 각 함수 맨 처음에 선언한다. |
| 114 | +``` |
| 115 | +private static void readPreferences() { |
| 116 | + InputStream is = null; //지역변수는 각 함수 맨 처음에 선언한다. |
| 117 | + |
| 118 | + try { |
| 119 | + is = new FileInputStream(getPreferencesFile()); |
| 120 | + setPreferences(new Properties(getPreferences())); |
| 121 | + getPreferences().load(is); |
| 122 | + } catch (IOException e) { |
| 123 | + try { |
| 124 | + if (is != null) |
| 125 | + is.close(); |
| 126 | + } catch (IOException e1) { |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | +``` |
| 131 | + |
| 132 | + |
| 133 | +#### 2-4-2. 인스턴스 변수 |
| 134 | + |
| 135 | +자바에서는 인스턴스 변수는 클래스 맨 처음에 선언한다. 인스턴스 변수 간에 세로로 거리를 두지 않는다 |
| 136 | +일반적으로 C++에서는 모든 인스턴스 변수를 클래스 마지막에 선언하는 가위 규칙(scissors rule)을 적용하지만, |
| 137 | +중요한 건 잘 알려진 위치에 인스턴스 변수를 모아놓아야 하며 변수 선언을 어디서 찾을지 모두가 알고 있어야 한다. |
| 138 | + |
| 139 | +#### 2-4-3. 종속 함수 |
| 140 | +한 함수가 다른 함수를 호출할 경우, |
| 141 | +두 함수는 세로로 가까이 배치하며 호출하는 함수를 호출되는 함수보다 먼저 배치한다. |
| 142 | +규칙적으로 함수를 배치하면 호출되는 함수를 찾기 쉬워지며 그만큼 모듈 전체의 가독성도 높아진다. |
| 143 | +``` |
| 144 | +public class WikiPageResponder implements SecureResponder { |
| 145 | + protected WikiPage page; |
| 146 | + protected PageData pageData; |
| 147 | + protected String pageTitle; |
| 148 | + protected Request request; |
| 149 | + protected PageCrawler crawler; |
| 150 | +
|
| 151 | + public Response makeResponse(FitNesseContext context, Request request) |
| 152 | + throws Exception { |
| 153 | + String pageName = getPageNameOrDefault(request, "FrontPage"); //getPageNameOrdefault 함수를 첫번쨰로 호출 |
| 154 | + loadPage(pageName, context); //loadPage 함수를 두번쨰로 호출 |
| 155 | + if (page == null) //결과에 따라 notFoundResponse 혹은 makePageResponse 함수를 순차적으로 호출 |
| 156 | + return notFoundResponse(context, request); |
| 157 | + else |
| 158 | + return makePageResponse(context); |
| 159 | + } |
| 160 | +
|
| 161 | + private String getPageNameOrDefault(Request request, String defaultPageName) //첫번째로 getPageNameOrdefault 함수 정의 |
| 162 | + { |
| 163 | + String pageName = request.getResource(); |
| 164 | + if (StringUtil.isBlank(pageName)) |
| 165 | + pageName = defaultPageName; |
| 166 | +
|
| 167 | + return pageName; |
| 168 | + } |
| 169 | +
|
| 170 | + protected void loadPage(String resource, FitNesseContext context) //두번째로 loadPage 함수 정의 |
| 171 | + throws Exception { |
| 172 | + WikiPagePath path = PathParser.parse(resource); |
| 173 | + crawler = context.root.getPageCrawler(); |
| 174 | + crawler.setDeadEndStrategy(new VirtualEnabledPageCrawler()); |
| 175 | + page = crawler.getPage(context.root, path); |
| 176 | + if (page != null) |
| 177 | + pageData = page.getData(); |
| 178 | + } |
| 179 | +
|
| 180 | + private Response notFoundResponse(FitNesseContext context, Request request) //세번째로 notFoundResponse 함수 정의 |
| 181 | + throws Exception { |
| 182 | + return new NotFoundResponder().makeResponse(context, request); |
| 183 | + } |
| 184 | +
|
| 185 | + private SimpleResponse makePageResponse(FitNesseContext context) //네번째로 makePageResponse 함수 정의 |
| 186 | + throws Exception { |
| 187 | + pageTitle = PathParser.render(crawler.getFullPath(page)); |
| 188 | + String html = makeHtml(context); |
| 189 | +
|
| 190 | + SimpleResponse response = new SimpleResponse(); |
| 191 | + response.setMaxAge(0); |
| 192 | + response.setContent(html); |
| 193 | + return response; |
| 194 | + } |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +#### 2-4-4. 개념적 유사성 |
| 199 | + |
| 200 | +개념적인 친화도가 높을수록 코드를 가까이 배치한다. 친화도가 높은 요인은 여러가지인데, |
| 201 | +한 함수가 다른 함수를 호출해 생기는 직접적인 종속성이 한 예다. |
| 202 | +변수와 그 변수를 사용하는 함수도 한 예다. 비슷한 동작을 수행하는 일군의 함수또한 좋은 예이다. |
| 203 | +``` |
| 204 | +public class Assert { |
| 205 | + static public void assertTrue(String message, boolean condition) { |
| 206 | + if (!condition) |
| 207 | + fail(message); |
| 208 | + } |
| 209 | + |
| 210 | + static public void assertTrue(boolean condition) { |
| 211 | + assertTrue(null, condition); |
| 212 | + } |
| 213 | + static public void assertFalse(String message, boolean condition) { |
| 214 | + assertTrue(message, !condition); |
| 215 | + } |
| 216 | + static public void assertFalse(boolean condition) { |
| 217 | + assertFalse(null, condition); |
| 218 | + } |
| 219 | +} |
| 220 | +``` |
| 221 | + |
| 222 | +## 3. 가로 형식 맞추기 |
| 223 | + |
| 224 | +요즘 모니터가 커져 보여지는 길이가 길어졌지만, 프로그래머는 평균적으로 짧은 행을 선호한다. |
| 225 | +길게 작성한 코드여도 최대 120자 정도의 행 길이로 제한하는것을 지향한다. |
| 226 | + |
| 227 | +### 3-1. 가로 공백과 밀집도 |
| 228 | + |
| 229 | +가로 공백을 사용해 밀접한 개념과 느슨한 개념을 표현한다. 함수 이름과 이어지는 괄호 사이에는 공백 넣지 않는다. |
| 230 | +함수를 호출하는 코드에서 괄호 안 인수는 공백으로 분리한다. |
| 231 | +``` |
| 232 | +private void measureLine(String line) { |
| 233 | + lineCount++; |
| 234 | + int lineSize = line.length(); |
| 235 | + totalChars += lineSize; //할당 연산자를 강조하기 위해 앞뒤로 공백을 준다 |
| 236 | + lineWidthHistogram.addLine(lineSize, lineCount); //함수 호출하는 코드에서 괄호 안 인수는 공백으로 분리 |
| 237 | + recordWidestLine(lineSize); //일반적으로 함수이름과 이어지는 괄호사이에는 공백을 주지 않는다 |
| 238 | +} |
| 239 | +``` |
| 240 | + |
| 241 | +``` |
| 242 | +//연산자 우선순위를 강조하기 위한 공백 사용 example |
| 243 | +public class Quadratic { |
| 244 | + public static double root1(double a, double b, double c) { |
| 245 | + double determinant = determinant(a, b, c); |
| 246 | + return (-b + Math.sqrt(determinant)) / (2*a); //승수사이에는 공백이 없지만 항 사이에는 공백이 들어간다. |
| 247 | + } |
| 248 | + |
| 249 | + public static double root2(int a, int b, int c) { |
| 250 | + double determinant = determinant(a, b, c); |
| 251 | + return (-b - Math.sqrt(determinant)) / (2*a); |
| 252 | + } |
| 253 | + |
| 254 | + private static double determinant(double a, double b, double c) { |
| 255 | + return b*b - 4*a*c; |
| 256 | + } |
| 257 | +} |
| 258 | +``` |
| 259 | + |
| 260 | + |
| 261 | +### 3-2. 가로 정렬 |
| 262 | +#### 3-2 Example1 |
| 263 | +``` |
| 264 | +//가로정렬을 시킨 example |
| 265 | +public class FitNesseExpediter implements ResponseSender { |
| 266 | + private Socket socket; |
| 267 | + private InputStream input; |
| 268 | + private OutputStream output; |
| 269 | + private Request request; |
| 270 | + private Response response; |
| 271 | + private FitNesseContext context; |
| 272 | + private ExecutorService executorService; |
| 273 | + private long requestParsingTimeLimit; |
| 274 | + private long requestProgress; |
| 275 | + private long requestParsingDeadline; |
| 276 | + private boolean hasError; |
| 277 | + |
| 278 | + public FitNesseExpediter(Socket s, |
| 279 | + FitNesseContext context) throws IOException { |
| 280 | + this.context = context; |
| 281 | + this.socket = s; |
| 282 | + input = s.getInputStream(); |
| 283 | + output = s.getOutputStream(); |
| 284 | + this.requestParsingTimeLimit = 10000; |
| 285 | + } |
| 286 | +} |
| 287 | +``` |
| 288 | +위 예제와 같이 정렬을 할 때 여러 단점이 있다. |
| 289 | ++ 코드가 엉뚱한 부분을 강조해 진짜 의도가 가려진다. |
| 290 | ++ 변수 유형보다 변수 이름부터 읽게 된다. |
| 291 | ++ 할당 연산자는 안 보이고 오른쪽 피연산자에 집중되게 된다 |
| 292 | ++ 코드 형식을 자동으로 맞춰주는 도구는 위의 정렬을 무시한다 |
| 293 | ++ 위와 같이 코드 선언부가 길면 클래스를 쪼개야 한다 |
| 294 | + |
| 295 | +위와 같은 이유로 아래와 같이 정렬을 하지않음으로 오히려 중대한 결함을 찾기 쉬워진다. |
| 296 | +#### 3-2 Example2 |
| 297 | +``` |
| 298 | +//가로정렬을 하지않은 example |
| 299 | +public class FitNesseExpediter implements ResponseSender { |
| 300 | + private Socket socket; |
| 301 | + private InputStream input; |
| 302 | + private OutputStream output; |
| 303 | + private Request request; |
| 304 | + private Response response; |
| 305 | + private FitNesseContext context; |
| 306 | + private ExecutorService executorService; |
| 307 | + private long requestParsingTimeLimit; |
| 308 | + private long requestProgress; |
| 309 | + private long requestParsingDeadline; |
| 310 | + private boolean hasError; |
| 311 | + |
| 312 | + public FitNesseExpediter(Socket s, FitNesseContext context) throws IOException { |
| 313 | + this.context = context; |
| 314 | + this.socket = s; |
| 315 | + input = s.getInputStream(); |
| 316 | + output = s.getOutputStream(); |
| 317 | + this.requestParsingTimeLimit = 10000; |
| 318 | + } |
| 319 | +} |
| 320 | +``` |
| 321 | + |
| 322 | +### 3-3. 들여쓰기 |
| 323 | + |
| 324 | +범위로 이뤄진 계층을 표현하기 위해 코드를 들여쓴다. 들여쓰기한 코드의 경우 구조가 한눈에 들어온다. |
| 325 | +주의할점은 |
| 326 | ++ 파일 수준인 문장은 들여쓰지 않는다(클래스) |
| 327 | ++ 메서드는 클래스보다 한 수준 들여쓴다 |
| 328 | ++ 메서드 코드는 메서드 선언보다 한 수준 들여쓴다 |
| 329 | ++ 블록 코드는 블록을 포함하는 코드보다 한 수준 들여쓴다 |
| 330 | + |
| 331 | +때로는 간단한 if문, 짧은 while문, 짧은 함수에서 들여쓰기를 무시하고 싶기도 하다. |
| 332 | +하지만 이런 경우에도 들여쓰기를 하는것을 지향하자. |
| 333 | +``` |
| 334 | +//들여쓰기를 무시한 경우 |
| 335 | +public class CommentWidget extends TextWidget { |
| 336 | + public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?"; |
| 337 | + |
| 338 | + public CommentWidget(ParentWidget parent, String text) {super(parent, text);} |
| 339 | + public String render() throws Exception {return "";} |
| 340 | +} |
| 341 | +
|
| 342 | +//들여쓰기를 적용한 경우 |
| 343 | +public class CommentWidget extends TextWidget { |
| 344 | + public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?"; |
| 345 | + |
| 346 | + public CommentWidget(ParentWidget parent, String text) { |
| 347 | + super(parent, text); |
| 348 | + } |
| 349 | + |
| 350 | + public String render() throws Exception { |
| 351 | + return ""; |
| 352 | + } |
| 353 | +} |
| 354 | +``` |
| 355 | +#### 3-3-1. 가짜범위 |
| 356 | + |
| 357 | +때로는 빈 while문이나 빈 for문을 접한다. body가 없는 while문은 세미콜론을 새 행에다 들여쓰자. |
| 358 | +``` |
| 359 | +while(dis.read(buf, 0, readBufferSize) != -1) |
| 360 | +; |
| 361 | +//이부분은 정확히 어떤 의미인지 모르겠다... |
| 362 | +``` |
| 363 | + |
| 364 | +## 4. 팀 규칙 |
| 365 | + |
| 366 | +프로그래머라면 각자 선호하는 규칙이 있다. 하지만 팀에 속한다면 자신이 선호해야 할 규칙은 바로 팀 규칙이다. |
| 367 | +팀은 한가지 규칙에 합의하고, 모든 팀원은 그 규칙을 따르는것을 지향하자. |
| 368 | +개개인이 마음대로 짜대는 코드는 피해야한다. 좋은 시스템은 읽기 쉬운 문서로, 스타일은 일관적이고 매끄러워야 한다. |
0 commit comments