book/Clean Code

[Clean Code] 5장, 형식 맞추기

항성 2022. 8. 29. 10:53
반응형

형식을 맞추는 목적

  • 코드 형식은 의사소통의 일환. 전문 개발자의 일차적인 의무
  • 오늘 구현한 기능이 다음 버전에서 바뀔 확률은 아주 높다. 하지만 오늘 구현한 코드의 가독성은 앞으로 바뀔 코드의 품질에 지대한 영향을 미친다.

적절한 행 길이를 유지하라

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

신문 기사처럼 작성하라

  • 소스 파일의 이름은 간단하면서도 설명이 가능하게 짓는다.
  • 파일의 첫 부분은 고차원 개념과 알고리즘을 설명한다.
  • 파일의 아래로 내려갈수록 의도를 세세하게 묘사한다.
  • 파일의 마지막에는 가장 저차원 함수와 세부 내역이 나온다.

개념은 빈 행으로 분리하라

  • 코드의 각 행은 수식이나 절을 나타내고, 일련의 행 묶음은 완결된 생각 하나를 표현한다. 생각 사이에는 빈 행을 넣어 분리한다.
// 패키지 선언부, import문, 각 함수 사이에 빈 행이 들어간다. 
package com.example.springrestapidemo;

import org.springframework.beans.factory.annotation.Autowired;

@RestController
public class SampleController {

    @GetMapping("/hello")
    public String hello() throws InterruptedException {
        Thread.sleep(5000);
        return "hello";
    }

    @GetMapping("/greeting")
    public String greeting() throws InterruptedException {
        Thread.sleep(5000);
        return "greeting";
    }
}

세로 밀집도

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

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

public class ReportConfig {

    private String m_className;
    private List<Property> m_properties = new ArrayList<>();

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

수직 거리

  • 같은 파일에 속할 정도로 밀접한 두 개념은 세로 거리로 연관성을 표현한다.

    여기서 연관성이란, 한 개념을 이해하는데 다른 개념이 중요한 정도.

    연관성이 깊은 두 개념이 멀리 떨어져 있으면 코드를 읽는 사람이 소스 파일과 클래스를 여기저기 뒤지게 된다.

  • 변수 선언

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

      private static void readPreferences() {
          InputStream is = null;
          try {
              is = new FileInputStream(getPreferencesFile());
              setPreferences(new Properties(getPreferences()));
              getPreferences().load(is);
          } catch (IOException e) {
              try {
                  if (is != null)
                      is.close();
              } catch (IOException e1) {
              }
          }
      }
      public int countTestCases() { 
          int count = 0;
          for (**Test each : tests**) // 루프 제어 변수는 Test 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);
          } 
      }
      ...
  • 인스턴스 변수

    인스턴스 변수는 클래스 맨 처음에 선언. 변수 간에 세로로 거리를 두지 않는다.

    언어마다 인스턴스 변수를 어디에 선언할 것인지 의견이 다르지만, 해당 언어를 사용하는 공통적으로 알고 있어야 한다는 점이 중요하다.

      // 도중에 선언된 변수는 꽁꽁 숨겨놓은 보물 찾기와 같다.
    
      public class TestSuite implements Test {
          static public Test createTest(Class<? extends TestCase> theClass,
                                          String name) {
              ...
          }
    
          public static Constructor<? extends TestCase>
          getTestConstructor(Class<? extends TestCase> theClass)
          throws NoSuchMethodException {
              ...
          }
    
          public static Test warning(final String message) {
              ...
          }
    
          private static String exceptionToString(Throwable t) {
              ...
          }
    
          private String fName;
    
          private Vector<Test> fTests= new Vector<Test>(10);
    
          public TestSuite() { }
    
          public TestSuite(final Class<? extends TestCase> theClass) {
              ...
          }
    
          public TestSuite(Class<? extends TestCase> theClass, String name) {
              ...
          }
    
          ... ... ... ... ...
      }
  • 종속 변수

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

    호출하는 함수를 호출되는 함수보다 먼저 배치한다.

      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;
          }
      ...
  • 개념적 유사성

    개념적으로 가까운 코드일수록 가까이 배치한다.

    (한 함수가 다른 함수 호출, 변수와 그 변수를 사용, 비슷한 동작을 수행 등)

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

세로 순서

  • 일반적으로 호출되는 함수는 호출하는 함수보다 나중에 배치. (소스 코드 모듈이 고차원에서 저차원으로 자연스럽게 내려간다)
  • 중요한 개념을 가장 먼저 표현한다.
  • 가장 중요한 개념을 표현할 때는 세세한 사항을 최대한 배제한다.

가로 형식 맞추기

한 행은 가로로 짧아야 바람직하다.

120자 정도로 행 길이 제한.

가로 공백과 밀집도

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

private void measureLine(String line) {
    lineCount++;

    // 할당 연산자 좌우로 공백을 주어 왼쪽,오른쪽 요소가 확실하게 구분된다.
    int lineSize = line.length();
    totalChars += lineSize;

    // 함수이름과 괄호 사이에는 공백을 없앰으로써 함수와 인수의 밀접함을 보여준다
    // 괄호 안의 인수끼리는 쉼표 뒤의 공백을 통해 인수가 별개라는 사실을 보여준다.
    lineWidthHistogram.addLine(lineSize, lineCount);
    recordWidestLine(lineSize);
}

// 연산자 우선순위를 표현하기 위해서도 공백 사용
b*b - 4*a*c

가로 정렬

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

보기에는 깔끔해보일 수 있으나, 코드가 엉뚱한 부분을 강조해 변수 유형을 자연스레 무시하고 이름부터 읽게 됨.

정렬이 필요할 정도로 목록이 길다면 목록 길이의 문제이지 정렬이 부족해서가 아니다.

선언부가 길다는 것은 클래스를 쪼개야 한다는 것.

들여쓰기

범위(scope)로 이로우저니 계층을 표현하기 위해 우리는 코드를 들여쓴다.

들여쓰기한 파일은 구조가 한 눈에 들어온다.

  • 들여쓰기 무시하기

    간단한 if문, 짧은 while문, 짧은 함수에서 들여쓰기 규칙을 무시하고픈 유혹이 생긴다. 이겨내고 들여쓰기 해라!

가짜 범위

때로는 빈 while문이나 for문을 접한다.

이런 상황에서는 빈 블록을 올바로 들여쓰고 괄호로 감싸라. 세미콜론은 새 행에대 제대로 들여써서 써준다.

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

팀 규칙

프로그래머라면 각자 선호하는 규칙이 있다.

하지만 팀에 속한다면 자신이 선호해야 할 규칙은 바로 팀 규칙이다.

(ex : 어디에 괄호를 넣을지, 들여쓰기는 몇 자로 할지, 클래스와 변수와 메서드 이름은 어떻게 지을지 등)

밥 아저씨의 형식 규칙

모범 코드

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;
    } 
}
반응형

'book > Clean Code' 카테고리의 다른 글

[Clean Code] 7장, 오류 코드  (0) 2022.08.31
[Clean Code] 6장, 객체와 자료구조  (0) 2022.08.30
[Clean Code] 4장, 주석  (0) 2022.08.28
[Clean Code] 3장, 함수  (0) 2022.08.27
[Clean Code] 2장, 의미 있는 이름  (0) 2022.08.27