[Clean Code] 클린코드 5장 : 형식 맞추기

    클린코드 5장(로버트C. 마틴)을 읽고...

     

    형식을 맞추는 목적

     

    코드 형식은 의사소통의 일환이다.

    오랜 시간이 지나 원래 코드의 흔적을 더 이상 찾아보기 어려울 정도로 코드가 바뀌어도

    맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 계속 영향을 미친다.

     

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

     

    일반적으로 큰 파일보다 작은 파일이 이해하기 쉽다.

    커다란 시스템도 200줄 정도면 구축 가능하다.

     

    신문 기사처럼 작성하라.

     

    - 이름은 간단하면서도 설명이 가능하게 짓는다. (이름만 보고 모듈을 파악할 수 있도록..)

    - 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명한다.

    - 아래로 내려갈수록 의도를 세세하게 묘사한다.

    - 마지막에는 가장 저차원의 함수와 세부내역이 나온다.

     

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

     

    거의 모든 코드는 왼쪽에서 오른쪽, 위에서 아래로 읽힌다.

    각 행은 수식이나 절을 나타내고, 일련의 행 묶음은 완결된 생각 하나를 표현한다.

    생각 사이는 빈행을 넣어 분리해야 마땅하다.

     

     

    package fitnesse.wikitext.widgets;
    
    import java.util.regex.*;
    
    public class BoldWidget extends ParentWidget {
        /* ... */
    }
    
    public BoldWidget(ParentWidget parent, String text) throws Exception {
    	/* ... */
    }
    
    public String render() throws Exception {
    	/* ... */
    }

     

    위 코드가 아래 코드보다 더 가독성이 좋다!

    그러므로 줄을 바꾸어 개념을 분리하자.

     

     

    package fitnesse.wikitext.widgets;
    import java.util.regex.*;
    public class BoldWidget extends ParentWidget {
        /* ... */
    }
    public BoldWidget(ParentWidget parent, String text) throws Exception {
    	/* ... */
    }
    public String render() throws Exception {
    	/* ... */
    }

     

    세로 밀집도

     

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

    즉, 서로 밀접한 코드 행은 세로로 가까이 놓여야 한다.

     

     

    수직 거리

     

    - 변수선언

     

      변수는 사용하는 위치에 최대한 가까이 선언한다

      지역 변수는 각 함수 맨 처음에 선언한다.

      루프문을 제어하는 변수는 루프 문 내부에 선언한다.

     

     

    - 인스턴스 변수

     

      인스턴스 변수는  클래스 맨 처음에 선언한다.

     

     

    - 종속 함수

     

      한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다.

      또한, 호출하는 함수가 호출되는 함수 보다 먼저 배치되는 것이 좋다.

     

     

    - 개념적 유사성

     

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

      한 함수가 다른 함수를 호출해 생기는 직접적인 종속성이 한 예이다.

      하지만 그외에도 비슷한 동작을 수행하는 일군의 함수가 좋은 예이다.

     

     

    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);
    } 
    ...

     

    가로 형식 맞추기

     

    프로그래머는 명백히 짧은 행을 선호한다.

    가급적이면 120자 정도로 행 길이를 제한한다.

     

     

    가로 공백과 밀집도

     

    가로로는 공백을 사용해 밀접한 개념과 느슨한 개념을 표현한다.

    다음과 같은 코드를 보자.

     

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

     

    할당 연산자를 강조하기 위해 앞뒤에 공백을 중 왼쪽 요소와 오른쪽 요소를 분명히 나누었다.

    반면 함수 이름과 이어지는 괄호에는 공백을 넣지 않는다 (함수와 인수는 서로 밀접하므로)

    공백을 넣으면 한 개념이 아니라 별개로 보인다.

     

    연산자 우선순위를 강조하기 위해서도 공백을 사용한다.

    승수 사이에는 공백이 없고, 항 사이에는 공백을 넣어준다.

     

     

    가로 정렬

     

    public class FitNesseExpediter implements ResponseSender {
        private     Socket         socket;
        private     InputStream    input;
        private     OutputStream   output;
        private     Reques         request;      
        private     Response       response; 
        private     FitNesseContex context; 
        protected   long           requestParsingTimeLimit;
        private     long           requestProgress;
        private     long           requestParsingDeadline;
        private     boolean        hasError;
    
    	public FitNesseExpediter(Socket s, FitNesseContext context) throws Exception
    	{
    		this.context =            context;
    		socket =                  s;
    		input =                   s.getInputStream();
    		output =                  s.getOutputStream();
    		requestParsingTimeLimit = 10000;
    	}

     

    위와 같은 가로정렬은 유용하지 않다

    - 코드가 엉뚱한 부분을 강조해 진짜 의도가 가려진다.

    - 위 선언부를 읽다 보면 변수 유형은 무시하고 이름만 읽게 된다.

    - 할당 연산자는 보이지 않고 오른쪽 피연산자에 눈이 간다.

    - 코드 정렬을 자동으로 해주는 도구가 위와 같은 정렬을 무시한다.

     

     

     

    들여 쓰기

     

    들여쓰는 정도는 계층에서 코드가 자리잡은 수준에 비례한다.

    들여쓰기한 파일은 구조가 한눈에 들어온다. 변수, 생성자 함수, 접근자 함수, 메서드가 금방보인다.

     

    - 클래스 정의처럼 파일 수준인 문장은 들여쓰지 않는다.

    - 클래스 내 메서드는 클래스보다 한 수준 들여쓴다.

    - 메서드 코드는 메서드 선언보다 한수준 들여쓴다.

    - 블록 코드는 블록을 포함하는 코드보다 한 수준 들여쓴다.

     

     

    들여쓰기 무시하기

     

    때로는 간단한 if문이나 짧은 while문, 짧은 함수에서 들여쓰기 규칙을 무시하지 말고 들여쓰기를 넣어주자.

     

     

    가짜 범

     

    때로는 빈 while문이나  for문을 접하게 되는데, 이러한 구조는 최대한 피하는 것이 좋다.만약 쓰게 된다면 세미코론은 새 행에다 제대로 들여써서 넣어준다.

     

    while (dis.read(buf, 0, readBufferSize) != -1)
    ;

     

     

    팀 규칙

     

    프로그래머는 팀에 속한다면 팀 규칙을 따라야 한다.

    팀은 한가지 규칙에 합의하여야 하고, 모든 팀원은 그 규칙을 따라야 한다.

     

    밥 아저씨의 형식 규칙

     

    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;
        } 
    }

     

     

    댓글