프로그래머는 형식을 깔끔하게 맞춰 코드를 작성해야 한다.
코드 형식을 맞추기 위해 간단한 규칙을 정하고 모두가 그 규칙을 따라야 한다.
코드 형식은 중요하다.
코드 형식은 의사소통의 일환이며, 의사소통은 전문 개발자의 일차적인 의무다.
맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 계속 영향을 미친다.
자바의 파일 크기 : 클래스 크기와 밀접
독자는 위 → 아래로 기사를 읽는다.
소스파일 역시 신문 기사와 비슷하게 작성한다.
💡 신문은 다양한 기사로 이뤄지며, 대다수 기사가 아주 짧다.
어떤 기사는 조금 길지만, 한 면을 채우는 기사는 거의 없다.
신문이 읽을 만한 이유는 여기에 있다.
대부분의 코드 : 왼쪽
→ 오른쪽
, 위
→ 아래
방향으로 읽힌다.
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
// pass
};
public BoldWidget(Parent parent, String text) throws Exception {
// pass
};
public String render() throws Exception {
// pass
}
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
// pass
};
public BoldWidget(Parent parent, String text) throws Exception {
// pass
};
public String render() throws Exception {
// pass
}
빈 행을 뺄 경우 코드의 가독성이 떨어져 암호처럼 보인다.
빈 행의 삽입 여부에 따라 행 묶음의 분리효과를 얻을 수 있다.
빈 행을 삽입하지 않을 경우, 코드 전체가 한 덩어리로 보인다.
// 예시 5-3
class ReporterConfig {
/**
* 리포터 리스너의 클래스 이름
*/
constructor(m_className, m_properties) {
this.m_className = m_className;
this.m_properties = [];
}
/**
* 리포터 리스너의 속성
*/
addProperty(property) {
m_properties.append(property);
}
}
// 예시 5-3 리팩토링
class ReporterConfig {
constructor(m_className, m_properties) {
this.m_className = m_className;
this.m_properties = [];
}
addProperty(property) {
m_properties.append(property);
}
}
시스템이 무엇을 하는지 이해하고 싶은데,
이 조각 저 조각이 어디에 있는지 찾고 기억하는데 시간과 노력이 소모된다.
서로 밀접한 개념은 세로로 가까이 둬야 한다.
→ 두 개념이 서로 다른 파일에 속할 경우 규칙이 통하지 않는다.
→ 타당한 근거가 없다면 서로 밀접한 개념은 한 파일에 속해야 마땅하다.
→ 위 이유가 java의 protected
변수를 피해야 하는 이유 중 하나
같은 파일에 속할 정도로 밀접한 두 개념은 세로 거리의 연관성을 표현
연관성이 깊은 두 개념이 멀리 떨어져 있을 경우
변수 선언
function readPreferences() {
let is = null;
try {
//pass
}
catch (e) {
try {
if (is != null) is.close();
}
catch(e1) {
}
}
}
function countTestCases() {
let count = 0;
forEach(each => count += each.countTestCases());
return count;
}
for (XmlTest test : m_suite.getTests()) {
TestRunner tr = m_runnerFactory.newTestRunner(this.test);
tr.addListener(m_textReporter);
m_testRunners.add(tr);
invoker = tr.getInvoker();
for (ITestNGMethod m : tr.getBeforeSuiteMethods()) {
beforeSuiteMethods.put(m.getMethod(), m);
}
for (ITestNGMethod m : tr.getAfterSuiteMethods()) {
afterSuiteMethods.put(m.getMethod(), m);
}
}
// ...
인스턴스 변수
클래스 맨 처음에 선언
변수간 세로로 거리를 두지 않음
잘 설계한 클래스는 클래스의 많은 메서드가 인스턴스 변수를 사용하기 떄문
인스턴스 변수를 선언하는 위치에 대한 논쟁 ↑
C++
: 모든 인스턴스 변수를 클래스 마지막에 선언하는 가위 규칙 적용
Java
: 보통 클래스 맨 처음에 인스턴스 선언
→ 중요점 : 잘 알려진 위치에 인스턴스 변수를 모은다는 사실
→ 변수 선언을 어디서 찾을지 모두가 알고 있어야 함
코드를 읽다가 우연히 변수를 발견하게 되는 상황
class TestSuite implements Test {
constructor(theClass, name) {
this.theClass = theClass;
this.name = name;
}
createTest(theClass, name) {
// ...
}
warning(message) {
// ...
}
exceptionToString(t) {
// ...
}
const fName;
const fTests;
TestSuite() {
}
TestSuite(theClass) {
// ...
}
TestSuite(theclass, name) {
// ...
}
}
종속 함수
아래 예제
class WikiPageResponder implements SecureResponder {
constructor(page, pageData, pageTitle, request, crawler) {
this.page = page;
this.pageData = pageData;
this.pageTitle = pageTitle;
this.request = request;
this.crawler = crawler;
}
makeResponse(context, request) {
const pageName = getPageNameOrDefault(request, "FrontPage");
loadPage(pageName, context);
if (page == null)
return notFountResponse(context, request);
else
return makePageResponse(context);
}
getPageNameOrDefault(request, defaultPageName) {
const pageName = request.getResource();
if (pageName == null)
pageName = defaultPageName;
return pageName;
}
loadPage(resuorce, context) {
const path = PathParser.parser(resource);
this.crawler = context.root.getPageCrawler();
this.page = crawler.getPage(context.root, path);
if (page != null)
this.pageData = this.page.getData();
}
notFoundResponse(context, request) {
return new NotFoundResponder().makeResponse(context, request);
}
makePageResponse(context) {
this.pageTitle = PathParser.render(this.crawler.getFullPath(this.page));
const html = makeHtml(context);
const response = new SimpleResponse();
response.setMaxAge(0);
response.setContent(html);
return response;
}
}
위 코드는 상수를 적절한 수준에 두는 좋은 예제이다.
getPageNameOrDefault()
내부에서 "FrontPage"
상수를 사용하는 방법도 가능
개념적 유사성
어떤 코드는 서로 끌어당김 → 개념적 친화도가 높기 떄문
친화도가 높을수록 코드를 가까이에 배치할 것
친화도가 높은 요인
개념적 유사성의 좋은 예시 코드
class Assert {
assertTrue(message, condition) {
if (!condition) fail(message);
}
assertTrue(condition) {
assertTrue(null, condition);
}
asertFalse(message, condition) {
assertTrue(message, !condition);
}
assetFalse(condition) {
assertFalse(null, condition);
}
}
고차원
→ 저차원
으로 자연스럽게 내려감프로젝트 7개에서 조사한 행 길이 분포 그래프
→ 프로그래머는 명백하게 짧은 행을 선호함
따라서 짧은 행이 바람직함
예전 : 오른쪽으로 스크롤할 필요가 없도록 프로그래밍
최근 : 매우 커진 모니터
→ 글꼴 크기를 줄여 200자까지도 한 화면에 들어감
→ 추천하지 않는 방식임
120자 정도의 행 길이 제한
가로로는 공백을 사용해 밀접한 개념과 느슨한 개념을 표현함
function measureLine(line) {
lineCount++;
const lineSize = line.length();
const totalChars += lineSize;
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}
class Quadatic {
root1(a, b, c) {
let determinant = determinant(a, b, c);
return (-b + Math.sqrt(determinant)) / (2*a);
}
root2(a, b, c) {
let determinant = determinant(a, b, c);
return (-b + Math.sqrt(determinant)) / (2*a);
}
determinant(a, b, c) {
return b*b - 4*a*c;
}
}
public class FitNesseExpediter implements ResponseSender {
private Socket socket;
private InputStream input;
private OutputStream output;
private Request request;
private Response response;
private FitNesseContext context;
protected long requestParsingTimeLimit;
private long requestProgress;
private long requestParsingDeadline;
private boolean hasError;
public FitNesseExpeditor(Socket s,
FitNesseContext context) throws Exception
{
this.context = context;
socket = s;
input = s.getInputStream();
output = s.getOutputStream();
requestParsingTileLimit = 10000;
}
}
public class FitNesseExpediter implements ResponseSender {
private Socket socket;
private InputStream input;
private OutputStream output;
private Request request;
private Response response;
private FitNesseContext context;
protected long requestParsingTimeLimit;
private long requestProgress;
private long requestParsingDeadline;
private boolean hasError;
public FitNesseExpeditor(Socket s, FitNesseContext context) throws Exception
{
this.context = context;
socket = s;
input = s.getInputStream();
output = s.getOutputStream();
requestParsingTileLimit = 10000;
}
}
소스파일은 윤곽도와 계층적으로 유사
파일 전체에 적용되는 정보 & 파일 내 개별 클래스에 적용되는 정보
클래스 내 각 메서드에 적용되는 정보 & 블록 내 블록에 재귀적으로 적용되는 정보
계층에서의 각 수준 : 이름을 선언하는 범위이자 선언문과 실행문을 해석하는 범위
💡 범위로 이뤄진 계층의 표현을 위해 코드 들여쓰기를 사용
들여쓰는 정도 : 계층에서 코드가 자리잡은 수준에 비례
클래스 정의와 같은 파일 수준인 문장은 들여쓰기 적용 X
클래스 내 메서드는 클래스보다 한 수준 들여쓰기
메서드 코드는 메서드 선언보다 한 수준 들여쓰기
블록 코드는 블록을 포함하는 코드보다 한 수준 들여쓰기
프로그래머는 이런 들여쓰기 체계에 크게 의존함
왼쪽으로 코드를 맞춰 코드가 속하는 범위를 시각적으로 표현
→ 이 범위에서 저 범위로 재빨리 이동하기 쉬워짐
→ 현재 상황과 무관한 if문
/while문
코드를 일일이 살펴볼 필요가 없음
소스파일 왼쪽을 훑으며 새 메서드
, 새 변수
, 새 클래스
를 찾음
class FitNesseServer implements SocketServer { constructor(context) { this.context = context; } serve(s) { serve(s, 10000); } serve(s, requestTimeout) { try { const sender = new FitNesseExpediter(s, context); sender.setRequestParsingTimeLimit(requestTimeout); sender.start();} catch(e) {e.printStackTrace(); } } }
vs
class FitNesseServer implements SocketServer {
constructor(context) {
this.context = context;
}
serve(s) {
serve(s, 10000);
}
serve(s, requestTimeout) {
try {
const sender = new FitNesseExpediter(s, context);
sender.setRequestParsingTimeLimit(requestTimeout);
sender.start();
}
catch(e) { e.printStackTrace(); }
}
}
들여쓰기를 한 파일의 구조는 한 눈에 들어옴
변수, 생성자 함수, 접근자 합수, 메서드가 금방 보임
들여쓰기를 한 코드 : 쉽게 분석 및 이해 가능
들여쓰기를 하지 않은 코드 : 열심히 분석하지 않는 한 빠른 이해가 어려움
들여쓰기 무시하기
if문
, 짧은 while문
, 짧은 함수
에서의 들여쓰기 규칙을 무시하고자 하는 유혹이 생김class CommentWidget extends TextWidget {
constructor(REGEXP) { this.REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?"; }
CommentWidget(parent, text) { super(parent, text); }
render() { throw ""; }
}
class CommentWidget extends TextWidget {
constructor(REGEXP) {
this.REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?";
}
CommentWidget(parent, text) {
super(parent, text);
}
render() {
throw "";
}
}
빈 블록을 올바로 들여쓰고 괄호로 감쌀 것
// 좋지 않은 코드
while(dis.read(buf, 0, readBufferSize) != -1)
;
팀은 한가지 규칙에 합의해야 한다.
모든 팀원은 그 규칙을 따라야 한다.
좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이루어짐
스타일은 일관적으로 매끄려워야 함
💡 한 소스 파일에서 봤던 형식이 다른 소스 파일에도 쓰이리라는 신뢰감을 독자에게 줘야 함
온갖 스타일을 뒤섞어 소스코드를 필요 이상으로 복잡하게 만드는 실수는 피할 것
class CodeAnalyzer implements JavaFileAnalysis {
constructor(lineCount, maxLineWidth, widestLineNumber, lineWidthHistogram, totalChars) {
this.lineCount = lineCount;
this.maxLineWidth = maxLineWidth;
this.widestLineNumber = widestLineNumber;
this.lineWidthHistogram = lineWidthHistogram;
this.totalChars = totalChars;
}
CodeAnalyzer() {
this.lineWidthHistogram = new LineWidthHistogram();
}
findJavaFiles(parentDirectory) {
const files = [];
findJavailes(parentDirectory, files);
return files;
}
findJavaFiles(parentDirectory, files) {
for (file in parentDirectory.listFiles()) {
if (file.getName().endsWith(".java"))
files.add(file);
else if (file.isDirectory())
findJavaFiles(file, files);
}
}
analyzeFile(javaFile) {
const br = new BufferReader(new FileReader(javaFiles));
const line;
while((line = br.readLine()) != null)
neasureLine(line);
}
measureLine(line) {
this.lineCount++;
const lineSize = line.length();
this.totalChars += lineSize;
this.lineWidthHistogram.addLine(lineSize, this.lineCount);
recordWidestLine(lineSize);
}
recordWidestLine(lineSize) {
if (lineSize > maxLineWidth) {
this.maxLineWidth = lineSize;
this.widestLineNumber = this.lineCount;
}
}
getLineCount() {
return this.lineCount;
}
getMatLineWidth() {
return this.maxLineWidth;
}
getWidestLineNumber() {
return this.getWidestLineNumber;
}
getLineWidthHistogram() {
return this.lineWidthHistogram;
}
getMeanLineWidth() {
return this.totalChars / this.lineCount;
}
getMedianLineWidth() {
const sortedWidths = getSortedWidths();
const cumulativeLineCount = 0;
for (let width in sortedWidths) {
cumulativeLineCount += lineCountForWidth(width);
if (cumulatuveLineCount > this.lineCount/2)
return width;
}
throw new Error("Cannot get here");
}
lineCountForWidth(width) {
return this.lineWidthHistogram.getLineforWidth(width).size();
}
getSortedWidths() {
const widths = this.lineWidthHistogram.getWidths();
const sortedWidths = Array.from(width);
sortedWidths.sort();
return sortedWidths;
}
}