Clean Code(클린 코드) | 로버트 C. 마틴 | 인사이트- 교보ebook

애자일 소프트웨어 장인 정신, 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

ebook-product.kyobobook.co.kr

코드

https://github.com/rkwhr0010/clean_code/tree/main/src

변경 사항은 git history 참고

 

오래된 주석은 거짓 정보를 퍼트린다.

거짓된 주석은 없는 것보다 나쁘다.

 

주석은 순수하게 선하지 않다. 필요악이다.

코드 자체가 표현력이 풍부하다면, 필요하지 않다.

 

따라서 주석을 단다는 것은 자신의 표현력이 부족하는 것이다.

 

좋은 주석이라도 시간이 지나면서, 관리되지 않아 나쁜 주석이 된다. 현실적으로 주석은 관리 되지 않는다.

 

코드만이 거짓을 말하지 않는다. 최대한 주석이 없는 방향으로 개발해야 한다.

 

주석은 나쁜 코드를 보완하지 못한다

주석은 코드 품질이 나쁠 단다. 따라서 주석이 보이면, 코드를 정리해야 한다

 

코드로 의도를 표현하라!

  //어느것이 더 좋은지 비교
  void example() {
    //직원에게 복지 혜택을 받을 자격이 있는지 검사
    if((employee.flags & HOURLY_FLAG) && employee.age > 65) {
      /* ~~~~ */
    }
    if(employee.isEligibleForFullBenefits()) {
      /* ~~~~ */
    }

 

좋은 주석

필수 정보는 주석을 밖에 없다.

 

법적인 주석

코드 상단에 카피라이트에 해당된다.

 

정보를 제공하는 주석

코드로 표현이 도저히 힘든 정규식 같은게 좋은 예다.

    //kk:mm:ss EEE, MMM dd, yyyy 형식
    Pattern timeMatcher = Pattern.compile(
      "\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");

마저도 시간과 날짜를 변환해주는 클래스를 별도로 만들어 옮기면, 주석이 필요하지 않다.

 

의도를 설명하는 주석

  public int compareTo(Object o) {
    if(o instanceof WikiPagePath) {
      WikiPagePath p = (WikiPagePath) o;
      String compressedName = StringUtil.join(names, "");
      String compressedArgumentName = StringUtil.join(p.names, "");
      return compressedName.compareTo(compressedArgumentName);
    }
    return 1; //오른쪽 유형이므로 정렬 순위가 더 높다.
  }
  void testConcurrentAddWidgets() throws Exception {
    WidgetBuilder widgetBuilder = new WidgetBuilder(new Class[]{BoldWidget.class});
    String text = "'''bold text'''";
    ParentWiget parent = new BoldWidget(new MockWidgetRoot(), "'''bold test'''");
    AtomicBoolean failFalg = new AtomicBoolean();
    failFalg.set(false);
    //스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
    for (int i = 0; i < 25000; i++) {
      WidgetBuilderThread widgetBuilderThread =
          new WidgetBuilderThread(widgetBuilder, text, parent, failFalg);
      Thread thread = new Thread(widgetBuilderThread);
      thread.start();
    }
    assertEquals(false, failFalg.get());
  }

 

의미를 명료하게 밝히는 주석

  void testCompareTo() throws Exception {
    WikiPagePath a = PathParser.parse("PageA");
    WikiPagePath ab = PathParser.parse("PageA.PageB");
    WikiPagePath b = PathParser.parse("PageB");
    WikiPagePath aa = PathParser.parse("PageA.PageA");
    WikiPagePath bb = PathParser.parse("PageB.PageB");
    WikiPagePath ba = PathParser.parse("PageB.PageA");
    //assertTrue 가 라이브러리를 사용한 것이라고 가정
    assertTrue(a.compareTo(a) == 0); // a == a
    assertTrue(a.compareTo(b) != 0); // a != b
    assertTrue(a.compareTo(b) == -1);// a < b
  }

모호한 인수나 반환값을 의미를 좋게 표현하려고 바꾸고 싶지만,

라이브러리를 사용하는 경우 변경이 불가하다. 경우는 주석이 유용하다.

 

물론 주석을 완전히 신뢰할 없다는 문제가 남아있다.

처럼 주석은 다른 방법으로 표현할 방법이 없는 고민 달아야 한다.

 

결과를 경고하는 주석

특정 테스트 케이스가 수행 시간이 오래걸린다면, 이를 위한 경고는 괜찮다.

요즘은 @Ignore 어노테이션 같은 것으로 안에 사유를 적어 주석보다 명시적으로 사용한다.

 

또는 해당 코드가 스레드 세이프하지 않을 경고를 남기기도 한다

 

TODO 주석

요즘 IDE TODO 인식 해서 보여주는 기능이 있다.

 

보통은 현재 구현이 어려운 코드를 TODO 주석으로 남길 것이다.

따라서 이를 주기적으로 체크해 줄여가야 한다.

 

중요성을 강조하는 주석

얼핏 봤을 대수롭지 않게 넘길 만한 코드에 중요한 사실을 경고할

    //trim() 중요하다. 시작에 공백이 들어가 있으면, 다른 문자열로 인식된다.
    String listItemContent = match.group(3).trim();
    new ListItemWidget(this, listItemContent, this.level + 1);
    return buildList(text.substring(match.end()));

 

공개 API에서 Javadocs

Javadocs 만들기로 했으면, 정말 만들어야 한다.

만든 공개 API Javadocs 훌륭하다.

 

 

나쁜 주석

대부분의 주석이 해당된다.

 

주절거리는 주석

무언가를 설명하려고 주석을 생각이면, 최대한 의미있게 자세히 서술해야 한다.

설명이 미흡하면, 결국 주석의 의미는 희미해지고, 코드를 뒤지며 로직을 확인해야 한다.

 

같은 이야기를 중복하는 주석

변수나 함수 이름만으로도 충분한데, 의미 없이 중복된 내용을 주석으로 작성한 것을 말한다.

  /*
   * this.closed true 일 때 반환되는 메서드
   * 타임아웃에 도달하면 예외를 던진다.
   */
  synchronized void waitForClose(final long timeoutMillis) throws Exception {
    if(!closed) {
      wait(timeoutMillis);
      if(!closed) {
        throw new Exception("MockResponseSender Could not be closed");
      }
    }
  }
  /* 로그를 찍기 위한 로거 */
  Logger logger;

 

오해할 여지가 있는 주석

 

의무적으로 다는 주석

자바독 예시

  /**
   * @param title CD 제목
   * @param author CD 저자
   * @param tracks CD 트랙수
   * @param durationInMinutes CD 분 길이
   */
  public void addCd(String title, String author, int tracks, int durationInMinutes) {
  }

오히려 가독성만 저해한다.

 

이력을 기록하는 주석

Git 같은 형상관리 프로그램이 이력을 저장하고 있다.

이제 필요없다.

 

있으나 마나 주석

이런 주석을 자주 접하면, 개발자는 주석을 무시하게 된다.

그러면 정말 중요한 주석인 경우 읽게 된다.

 

위치는 표시하는 주석

// 여기 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

이런 주석이다. 시적으로 혼자만 가끔 사용할 있어도 커밋 시점엔 제거돼야 한다.

 

닫는 괄호에 다는 주석

예전에 IDE 없던 시절에는 함수가 길어지면, 찾기 힘들 었다

현재는 IDE 닫은 괄호를 눈에 띄게 표시해주며, 특히 여는 괄호에서 닫는 괄호로 이동 기능도 제공한다.

 

 

Vscode Ctrl + Shift + P 괄호 이동 단축키다.

 

공로를 돌리거나 저자를 표시하는 주석

형상 관리 프로그램이 관리한다.

 

주석으로 처리한 코드

언젠가 필요할 까봐 주석으로 남기는 코드가 있다.

역시 형상 관리 프로그램으로 과거 코드를 언제든지 접근할 있으므로 제거한다.

 

HTML주석

Javadocs 작성할 주석에 HTML 태그를 사용한다. 이렇게 되면 javadocs 만들지 않는 이상 주석만 보고 파악하기 힘들다.

 

현재 IDE 팝업 같은 형식으로 보기 편하게 지원해서 의미는 없는 듯하다.

 

전역 정보

주석을 단다면 주변 코드 정보만 달아야 한다. 전역 시스템 정보를 달면 안된다.

조금이라도 코드에 변경이 생기면, 전역 주석을 항상 최신화 있는가

 

 

너무 많은 정보

예를 들어 JWT 라이브러리를 사용하는데 라이브러리 주석에 JWT 동작방식이 있다고 생각하면 이해가 쉽다.

 

모호한 관계

코드와 이를 설명하는 주석의 관계가 명확하지 않는 경우를 말한다.

 

비공개 코드에서 Javadocs

공개 API에선 의미가 있지만, 비공개 코드에선 의미가 없다. 오히려 코드를 읽기 힘들게 한다.

 

예제

리팩터링

package chap04.ex07;
/**
 * 이 클래스는 사용자가 지정한 최대 값까지 소수를 생성한다.
 * 사용된 알고리즘은 에라스토테네스의 체다.
 *
 * 에라스토테네스 소개  [의미없는 주석]
 * ~~~~~~~~~~~~~~~~~~
 *
 * 알고리즘 설명
 * ~~~~~~~~~~~~~~~~~~
 *
 * @author 누군가
 * @version 2023.09.01
 */
public class GeneratePrimes {
  /** [의미없는 주석]
   * @param maxValue소수를 찾아낼 최대 값
   */
  //지나지게 거대한 메서드
  public static int[] generatePrimes(int maxValue) {
    if (maxValue > 2) { //유일하게 유효한 경우 [의미없는 주석]
      //선언
      int s = maxValue + 1;
      boolean[] f = new boolean[s];
      int i;
      //배열을 참으로 초기화 [의미없는 주석] 메서드로 뺄 것(메서드 이름을 명시적으로)
      for(i = 0; i < s; i++) {
        f[i] = true;
      }
      //소수가 아닌 알려진 숫자를 제거 [의미없는 주석]
      f[0] = f[1] = false;
      // [의미없는 주석] [의미없는 주석] 메서드로 뺄 것(메서드 이름을 명시적으로)
      int j;
      for (i = 2; i < Math.sqrt(s) + 1; i++) {
        if (f[i]) {
          for (j = 2 * i; j < s; j += i) {
            f[j] = false; //배수는 소수가 아니다.
          }
        }
      }
      //소수 개수는? [의미없는 주석] 메서드로 뺄 것(메서드 이름을 명시적으로)
      int count = 0;
      for (i = 0; i < s; i++) {
        if (f[i]) {
          count++; // 카운트 증가
        }
      }
     
      int[] primes = new int[count];
      // 소수를 결과 배열로 이동한다. [의미없는 주석] 메서드로 뺄 것(메서드 이름을 명시적으로)
      for (i = 0, j = 0; i < s; i++) {
        if (f[i]) {
          primes[j++] = i;
        }
      }
      return primes;
    } else { // maxValue < 2 [의미없는 주석]
      return new int[0]; //입력이 잘못되면 비어 있는 배열을 반환하다. [의미없는 주석]
    }
  }
}

 

리팩터링

package chap04.ex07;
/**
 * 이 클래스는 사용자가 지정한 최대 값까지 소수를 구한다.
 * 알고리즘은 에라스토테네스의 체다.
 * 2에서 시작하는 정수 배열을 대상으로 작업한다.
 * 처음으로 남아 있는 정수를 찾아 배수를 모두 제거한다.
 * 배열에 더 이상 배수가 없을 때까지 반복한다.
 */
public class PrimeGenerator {
  private static boolean[] crossedOut;
  private static int[] result;
  public static int[] generatePrimes(int maxValue) {
    if (maxValue < 2) {
      return new int[0];
    } else {
      uncrossIntegersUpTo(maxValue);
      crossOutMultiples();
      putUncrossedIntegersIntoResult();
      return result;
    }
  }


  private static void uncrossIntegersUpTo(int maxValue) {
    crossedOut = new boolean[maxValue + 1];
    for (int i = 2; i < crossedOut.length; i++) {
      crossedOut[i] = false;
    }
  }
  private static void crossOutMultiples() {
    int limit = determineIterationLimit();
    for (int i = 2; i <= limit; i++) {
      if (notCrossed(i)) {
        crossOutMultiplesOf(i);
      }
    }
  }
  private static int determineIterationLimit() {
    /*
     * 배열에 있는 모든 배수는 배열의 제곱근보다 작은 소수의 인수다.
     * 따라서 이 제곱근보다 더 큰 숫자의 배수는 제거할 필요가 없다.
     */
    double iterationLimit = Math.sqrt(crossedOut.length);
    return (int) iterationLimit;
  }
  private static void crossOutMultiplesOf(int i) {
    for (int multiple = 2 * i;
        multiple < crossedOut.length;
        multiple += i) {
      crossedOut[multiple] = true;
    }
  }
  private static boolean notCrossed(int i) {
    return crossedOut[i] == false;
  }
  private static void putUncrossedIntegersIntoResult() {
    result = new int[numberOfUncrossedIntegers()];
    for(int j = 0, i = 2; i < crossedOut.length; i++) {
      if (notCrossed(i)) {
        result[j++] = i;
      }
    }
  }
  private static int numberOfUncrossedIntegers() {
    int count = 0;
    for (int i =2; i < crossedOut.length; i++) {
      if (notCrossed(i)) {
        count++;
      }
    }
    return count;
  }
}

 

 

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

6장 객체와 자료 구조  (1) 2023.12.18
5장 형식 맞추기  (0) 2023.12.11
3장 함수  (1) 2023.11.27
2장 의미 있는 이름  (1) 2023.11.20
1장 깨끗한 코드  (0) 2023.11.13

 

 

Clean Code(클린 코드) | 로버트 C. 마틴 | 인사이트- 교보ebook

애자일 소프트웨어 장인 정신, 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

ebook-product.kyobobook.co.kr

코드

https://github.com/rkwhr0010/clean_code/tree/main/src

변경 사항은 git history 참고

 

프로그램의 가장 기본 단위가 함수다. (자바 기준 메서드)

 

원본 코드

public class HtmlUtil {
    public static String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
        WikiPage wikiPage = pageData.getWikiPage();
        StringBuffer buffer = new StringBuffer();
        if (pageData.hasAttribute("Test")) {
            if(includeSuiteSetup) {
                WikiPage suiteSetup =
                    PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage);
                if(suiteSetup != null) {
                    WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup);
                    String pagePathName = PathParser.render(pagePath);
                    buffer.append("!include -setup .")
                        .append(pagePathName)
                        .append("\n");
                }
            }
        }
        WikiPage setUp = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
        if(setUp != null) {
            WikiPagePath setUpPath = wikiPage.getPageCrawler().getFullPath(setUp);
            String setupPathName = PathParser.render(setUpPath);
            buffer.append("!include -setup .")
                .append(setupPathName)
                .append("\n");
        }
        buffer.append(pageData.getContent());
        if(pageData.hasAttribute("Test")) {
            WikiPage teardown =
                PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
            if(teardown != null) {
                WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath(teardown);
                String tearDownPathName = PathParser.render(tearDownPath);
                buffer.append("\n")
                    .append("!include -teardown .")
                    .append(tearDownPathName)
                    .append("\n");
            }
            if(includeSuiteSetup) {
                WikiPage suiteTearDown =
                    PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage);
                if(suiteTearDown != null) {
                    WikiPagePath pagePath = wikiPage.getPageCrawler().getFullPath(teardown);
                    String pagePathName = PathParser.render(pagePath);
                    buffer.append("\n")
                        .append("!include -teardown .")
                        .append(pagePathName)
                        .append("\n");
                }
            }
        }
        pageData.setContent(buffer.toString());
        return pageData.getHtml();
    }
}

 

리팩터링 코드

    public static String renderPageWithSetupsAndTeardowns(
            PageData pageData,
            boolean includeSuiteSetup) throws Exception {
               
        boolean isTestPage = pageData.hasAttribute("Test");
        if (isTestPage) {
            WikiPage wikiPage = pageData.getWikiPage();
            StringBuffer newPageContent = new StringBuffer();
            includeSetupPages(includeSuiteSetup, wikiPage, newPageContent);
            newPageContent.append(pageData.getContent());
            includeTeardownPages(includeSuiteSetup, wikiPage, newPageContent);
            pageData.setContent(newPageContent.toString());
        }
        return pageData.getHtml();
    }

 

 

작게 만들어라!

함수를 만드는 규칙은 하나도 작게, 둘도 작게다.

명확한 근거는 없지만, 저자 경험에서 나온 규칙이다.

 

작은 함수 기준

가로 0 ~ 150

세로 2 ~ 4

들여쓰기 수준 1 ~ 2

 

    public static String renderPageWithSetupsAndTeardowns(
            PageData pageData,
            boolean includeSuiteSetup) throws Exception {
        if (isTestPage(pageData)) {
            includeSetupAndTeardownPages(pageData, includeSuiteSetup);
        }
        return pageData.getHtml();
    }

 

블록과 들여쓰기

제어문과 반복문 속에 들어가는 블록은 원래 줄이어야 한다는 뜻이다.

내부 코드를 함수로 추출하여 이름을 짓는다면, 코드를 이해하기 쉬워ㅣ진다.

 

가지만 해라!

함수는 가지를 해야 한다. 가지를 해야 한다. 가지만을 해야 한다.

 

함수는 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 함수는 가지 작업만 한다.

함수를 만드는 이유는 개념을 다음 추상화 수준에서 여러 단계로 나누기 위해서다.

 

기존 함수에서 다른 의미를 가지는 함수를 추출할 있다면 함수는 여러 작업을 하는 것이다.

 

함수 섹션

함수에서 섹션이 나눠진다면, 함수는 여러가지 일을 한다는 뜻이다.

가지만 하는 함수는 섹션을 나누기 힘들다.

 

 

함수 추상화 수준은 하나로!

함수가 가지만 잘하려면 모든 문장의 추상화 수준이 동일해야 한다.

 

함수 내에 추상화 수준이 섞이면, 읽는 사람이 헷갈린다.

특히 근본 개념과 세부사항이 섞여있다면, 깨진 창문 처럼 함수는 사람들이 세부사항을 추가하기 시작한다.

 

위에서 아래로 코드 읽기: 내려가기 규칙

좋은 코드는 좋은 책을 읽는 것과 같아야 한다.

높은 추상화 수준 코드에서 점점 낮아지는 추상화 수준으로 코드는 구성돼야 한다.

 

Switch (if-else 포함)

스위치문은 근본적으로 N 가지 일을 처리한다. 그리고 구조적으로 작게 만들기 어렵다.

 

개발 제어문은 사용할 밖에 없다.

 

스위치문은 다형성을 이용해 분리할 있다.

 

예시

  public Money calculatePay(Employee e) throws InvalidEmployeeType{
    switch (e.type) {
      case COMMISSIONED:
        return calculateCommissionedPay(e);
      case HOURLY:
        return calculateHourlyPay(e);
      case SALARIED:
        return calculateSalariedPay(e);
      default:
        throw new InvalidEmployeeType(e.type);
    }
  }

문제점,

함수가 길다.

신규 유형을 추가하기 힘들다.

SRP 위반한다. 코드를 변경할 이유가 하나가 아니다

OCP 위반한다. 신규 유형을 추가 마다 기존 코드가 영향을 받는다.

 

가장 문제

똑같은 유해한 구조가 무한정 반복될 수 있다.

  /*
  * 페이지 48, 가장 큰 문제 예시
  * 똑같은 유해한 구조가 무한정 반복될 수 있다.
  */
  void isPayDay(Employee e, Date date) throws InvalidEmployeeType {
    switch (e.type) {
      case COMMISSIONED:
        /* 뭔가 하는 코드 */
        return ;
      case HOURLY:
        /* 뭔가 하는 코드 */
        return ;
      case SALARIED:
        /* 뭔가 하는 코드 */
        return ;
      default:
        throw new InvalidEmployeeType(e.type);
    }
  }
  void deliverPay(Employee e, Money money) throws InvalidEmployeeType{
    switch (e.type) {
      case COMMISSIONED:
        /* 뭔가 하는 코드 */
        return ;
      case HOURLY:
        /* 뭔가 하는 코드 */
        return ;
      case SALARIED:
        /* 뭔가 하는 코드 */
        return ;
      default:
        throw new InvalidEmployeeType(e.type);
    }
  }

 

개선

public interface EmployeeFactory {
  Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements EmployeeFactory {
  @Override
  public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
    switch (r.type) {
      case COMMISSIONED:
        return new CommissionedEmployee(r);
      case HOURLY:
        return new HourlyEmployee(r);
      case SALARIED:
        return new SalariedEmployee(r);
      default:
        throw new InvalidEmployeeType(r.type);
    }
  }
}
public abstract class Employee {
  EmployeeRecord r;
  Employee(EmployeeRecord r) {
    this.r = r;
  }
  public abstract boolean isPayDay() ;
  public abstract Money calculatePay() ;
  public abstract void deliverPay(Money pay) ;
}

추상 팩터리로 객체를 생성하는 switch 문을 외부로 부터 숨긴다.

팩터리 구현체는 switch 문을 구현해 Employee 파생 클래스의 인스턴스를 생성한다.

 

호출자는 Employee라는 인터페이스를 통해 구현부는 모른체 호출하게 된다.

언어 차원에서 다형성으로 인해 실제 파생 클래스 메서드가 호출된다.

 

코드 안에서 다형성을 이용한 switch 문은 번만 사용한다.

 

서술적인 이름을 사용하라!

단편적인 이름은, 함수가 무엇을 하는지 알기 힘들다.

renderHtml()  함수는 무엇을 하는지 정확히 없다.

 

서술적인 이름을 지어 함수가 하는 일을 표현해야 한다.

 

함수 이름이 너무 길거나, 짓기 어렵다면, 함수가 가지 일을 하는지 확인해야 한다.

가지만 하는 함수는 이름을 짓기 쉽다.

 

요즘 IDE 이름 변경이 매우 쉬우니, 좋은 이름이 떠올랐으면 즉시 바꾸자

 

함수 인수

가장 좋은 인수는 0

다음 1

다음 2 항이다. 2 부터는 인수 순서에 의존성이 생긴다.

3 부터는 피하는 좋고,

4 항은 특별한 이유 없이는 사용하면 안된다. (특별한 이유 예시 _ 배열 복사 같은 것은 인수가 많을 밖에 없다)

 

많은 인수는 테스트 코드 작성도 어렵게 한다. 경우의 수가 늘어나기 때문

되도록 인수는 0 ~ 1 유지해야 한다.

 

많이 쓰는 단항 형식

같이 유념하고 개념, 명령과 질의 분리

 

질의와 찾기가 많다

질의

Boolean    isExists("123")

찾기

User         findById("123")

 

자주 사용하지 않지만, 이벤트 형식도 있다.

이벤트 함수는 입력 인수만 있다. 출력 인수는 없다. (명령, command)

함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다.

passwordAttemptFailedNtimes(int attempts)

 

이벤트 함수는 시스템 상태를 바꾸기에 이름에서 이벤트 함수임을 명확히 해야 한다.

 

경우는 가급적 단항 함수를 피해야 한다.

 

좋은 예시

Void includeSetupPageInto(StringBuffer pageText)

 

StringBuffer includeSetupPageInto(StringBuffer pageText)

항등 함수처럼 자기 자신을 리턴하는 났다.

 

플래그 인수

함수에 Boolean 값을 넣는 것은 좋다.

함수는 여러 가지 일을 하는 함수라는 것을 드러내는 꼴이다.

 

이항 함수, 삼항 함수

이함 함수부터는 인수 순서를 신경써야 한다.

, 고려 사항이 기하급수적으로 증가한다.

 

인수 객체

인수가 2 ~ 3 이상 필요 독자적인 클래스 변수를 고려한다.

  //이것 보다.
  Circle makeCircle(double x, double y, double radius){
    return new Circle(x, y, radius);
  }
  //이것이 더 명료하다. 연관있는 인수를 하나로 묶었기 때문이다.
  Circle makeCircle(Point center, double radius){
    return new Circle(center, radius);
  }

단순한 눈속임처럼 보이지만, 아니다. 서로 연관있는 인수를 하나로 묶었다는 것에서 가치가 있다.

 

인수 목록

가변 인수가 필요한 경우가 있다.

경우는 사실상 이항 함수로 봐야한다.

 

동사와 키워드

함수 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.

 

함수 동사, 인수 명사 이렇게 쌍을 이뤄야 한다.

 

writeField(String name)

경우 누가봐도 이름을 필드에 쓴다는 것을 있다.

 

인수의 순서를 함수 이름에 적시하는 것은 좋은 방법이다.

assertExpectedEqualsActual(expected, actual)

 

부수 효과를 일으키지 마라!

부수 효과는 함수에서 가지를 하겠다고 해놓고 몰래(부수효과) 다른 짓도 하는 행위다.

 

public class UserValidator {
  private Crytograher crytograher;
  public boolean checkPassword(String username, String password) {
    User user = UserGateway.findByName(username);
   
    if(user != User.NULL) {
      String codePhrase = user.getPhraseEncodeByPassword();
      String phrase = crytograher.decrpt(codePhrase, password);
      if("Valid Password".equals(phrase)) {
        Session.initialize();
        return true;
      }
    }
    return false;
  }
}

패스워드가 일치하면 true, 아니면 false 리턴하는 함수다.

 

예시에서 부수효과는 Session.initialize(); 이다

 

함수 이름만 봐선 로그인 성공 세션을 초기화 한다는 것을 없다.

, 사용자가 의도치 않게 인증 세션을 지울 가능성이 있다.

 

또한 시간적 결합이 존재한다.

checkPassword() 함수는 세션을 초기화해도 되는 상황(시간적 결합)에서만 호출 가능하다.

세션을 지울 수도 있기 때문이다.

 

경우 함수 이름에서 이를 명시해 줘야 한다.

checkPasswordAndInitializeSession

 

참고로 함수는 가지 일을 한다

 

명령과 조회를 분리하라!

함수는 뭔가를 수행하거나 뭔가에 답하는 가지 행동만 해야한다.

같이 하면 안된다.

 

  public boolean set(String attrbute, String value) {
    //저장에 성공하면 true를 실패하면 false를 반환한다고 가정
    return false;
  }
  public void doSomething() {
    /* 이상한 코드 형식 */
    if(set("하나", "")) {
    }
  }

명령과 질의를 같이 수행하면 위와 같은 괴상한 코드가 나올 있다.

 

추가로 함수는 이름이 불명확하다.

set() 무슨 동작을 하는지 예상이 안된다.

  //명령
  public void set(String attribute, String value) {
  }
  //질의
  public boolean attributeExists(String string) {
    return false;
  }
  public void doSomething() {
    if(attributeExists("하나")) {
      set("하나", "");
    }
  }

명령과 질의를 분리하고, 이름을 명확히해 주석 같은 정보가 없어도 하는지 있다.

 

오류 코드보다 예외를 사용하라!

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리에 위반한다.

 

오류 코드는 중첩 코드를 야기한다.

반환되는 오류코드를 처리하기 위해 제어문이 필요하기 때문이다.

 

  String something(Page page) {
    if(deletePage(page) == E_OK) {
      if(registry.deleteReference(page.name) == E_OK) {
        if(configKeys.deleteKey(page.name.makeKey()) == E_OK){
          logger.info("page deleted");
        } else {
          logger.info("configKey not deleted");
        }
      } else {
        logger.info("deleteReference from registry failed");
      }
    } else {
      logger.info("delete failed");
      return E_ERROR;
    }
    return E_OK;
  }

오류 코드 대신 예외 사용

  void something(Page page) {
    try {
      deletePage(page);
      registry.deleteReference(page.name);
      configKeys.deleteKey(page.name.makeKey());
    } catch (Exception e) {
      logger.info(e.getMessage());
    }
  }

 

try/catch 블록 뽑기

Try/catch 블록은 구조가 추하다.

정상 처리 코드와 오류 처리 코드를 뒤섞기 때문이다.

여기서 뒤섞인 다는 것은 하나의 함수는 하나만 잘해야 하는데,

오류 처리 코드와 정상 처리 코드가 함께 있다는 것을 의미하는 같다.

 

별도 함수를 뽑아 문제를 제거하는 편이 옳다.

  void delete(Page page) {
    try {
      deletePageAndAllReferences(page);
    } catch (Exception e) {
      logError(e);
    }
  }
  private void logError(Exception e) {
    logger.info(e.getMessage());
  }
  private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
  }

 

이렇게 오류 처리 코드와 정상 동작 코드를 분리하면 이해하고 수정하기 쉬워 진다.

 

오류 처리도 가지 작업이다.

오류 처리도 가지 작업이기 때문에 이전 코드에서 별도 함수로 추출해서 정상 코드와 분리한 것이다.

 

Error.java 의존성 자석

에러 코드를 반환한다는 것은 어딘 가에 오류 코드를 반환하기 위한 코드를 정의한 다는 뜻이다.

public enum Error {
    OK,
    INVALID,
    NO_SUCH,
    LOCKED,
    OUT_OF_RESOURCES,
    WATTING_FOR_EVENT;
}
interface OtherError {
  String OK = "OK";
  String INVALID = "INVALID";
  String NO_SUCH = "NO_SUCH";
  String LOCKED = "LOCKED";
  String OUT_OF_RESOURCES = "OUT_OF_RESOURCES";
  String WATTING_FOR_EVENT = "WATTING_FOR_EVENT";
}

 

위와 같은 클래스가 의존성 자석이다.

 

열거형 Error 경우, 새로운 오류를 정의해야 하는 경우 Error 사용하는 모든 클래스를 컴파일 해야 한다.

 

문제로 Error 열거형은 변경이 쉽지 않다.

재컴파일을 피하려고 기존 에러 코드를 재사용하게 된다.

 

오류 코드 대신 예외를 사용하면, 새로운 예외는 Exception 클래스에서 파생되어 기존 코드는 전혀 재컴파일이 필요 없다.

 

반복하지 마라!(Don’t Repeat Yourself : DRY 원칙)

중복의 문제는 변경에 취약하다.

중복이 10 라면 하나의 변경에도 10 수정 해야 한다.

10 번의 수정 과정에서 개발자가 실수를 하지 않는 다는 보장도 없다.

 

현대 소프트웨어에서 가장 문제되는 코드는 중복이다.

그래서 이를 극복하기 위한 다양한 방법들이 존재한다.

 

데이터 베이스도 이런 중복 문제로 정규화를 수행한다.

AOP 중복 제거를 위함이다.

 

구조적 프로그래밍

에츠허르 데이크스트라 구조적 프로그래밍 원칙에 따르면, 모든 함수는 입구와 출구가 하나만 존재해야 한다.

단일입구/단일출구 원칙

 

Break, continue, goto(절대) 사용하면 안된다.

 

규율은 함수가 때만 이익을 제공한다.

작은 함수에선 효과가 없다.

 

작은 함수에선 여러 출구가 오히려 함수 표현에 도움될 때도 있다.

 

함수를 어떻게 짜죠?

글쓰기라 생각하면 된다.

일단 생각한 바를 기록한 다듬고, 다듬는다.

 

이를 함수에 대입하면,

일단 동작을 하는 로직을 만든다. 이땐 들여쓰기가 아무리 많아도 괜찮다.

다만, 초안 작성에도 테스트 코드는 만들면서 진행해야 한다.

 

함수를 쪼개거나 이름을 짓는 , 이후 리팩터링을 한다.

 

결론

대가 프로그래머는 시스템을 구현할 프로그램이 아닌 풀어갈 이야기로 여긴다.

프로그래밍 언어라는 수단을 사용해 표현려기 강한 언어를 만들어 이야기를 풀어간다.

함수가 이를 표현하는 언어에 속한다.

 

좋은 함수는 길이가 짧고, 이름이 좋고, 체계가 잡힌 함수다.

좋은 함수의 궁극적 목표는 시스템 이야기를 풀어가는 것이다.

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

5장 형식 맞추기  (0) 2023.12.11
4장 주석  (1) 2023.12.04
2장 의미 있는 이름  (1) 2023.11.20
1장 깨끗한 코드  (0) 2023.11.13
다시 시작  (0) 2023.11.10

 

 

Clean Code(클린 코드) | 로버트 C. 마틴 | 인사이트- 교보ebook

애자일 소프트웨어 장인 정신, 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

ebook-product.kyobobook.co.kr

코드

https://github.com/rkwhr0010/clean_code/tree/main/src

변경 사항은 git history 참고

 

의도를 분명히 밝혀라

이름을 짓는데 시간을 투자해야 한다. 좋은 이름으로 절약하는 시간이 크기 때문이다.

 

이름 붙일 대상은 전부다. 패키지, 클래스, 변수

 

주석이 필요한 것은 의도가 명확하지 않은 것이다.

class Ex001 {
    void 의도를분명히밝혀라(){
        /**
         * 주석이 필요하면 의도가 명확하지 않은 것
         */
        Date d; // 도착시간
       
        /**
         * 의도는 명확히
         */
        Date arrivalTime;
    }
}

 

class Ex002 {
    List<int[]> theList = new ArrayList<>();
    public List<int[]> getThem(){
        List<int[]> list1 = new ArrayList<>();
        // theList 내용물이 뭐지?
        theList.stream()
            .filter(intArr -> intArr[0] == 4) // intArr[0] == 4 는 뭐지?
            .forEach(intArr -> list1.add(intArr));
        //반환되는 리스트는 어떻게 사용하지?
        return list1;
    }
}

코드 로직은 쉬우나, 무엇을 하는 코드인지 판단이 안된다.

 

class Ex002_2 {
    //지뢰찾기 게임이라는 것을 알아냈다.
    private static final int FLAGGED = 4;
    private static final int STATUS_VALUE = 0;
    List<int[]> gameBoard = new ArrayList<>();
    public List<int[]> getFlaggedCells(){
        List<int[]> flaggedCells = new ArrayList<>();
        gameBoard.stream()
            .filter(cell -> cell[STATUS_VALUE] == FLAGGED)
            .forEach(flaggedCells::add);
           
        return flaggedCells;
    }
}

 

코드 로직은 변경된게 하나 없이, 좋은 이름만으로 의도가 명확해 졌다.

 

class Ex002_3 {
    List<Cell> gameBoard = new ArrayList<>();
    public List<Cell> getFlaggedCells(){
        List<Cell> flaggedCells = new ArrayList<>();
        gameBoard.stream()
            .filter(Cell::isFlagged)
            .forEach(flaggedCells::add);
           
        return flaggedCells;
    }
    class Cell {
        private int[] status;
        private static final int FLAGGED = 4;
        private static final int STATUS_VALUE = 0;
        public boolean isFlagged() {
            return FLAGGED == status[STATUS_VALUE];
        }
    }
}

나아가 int[] 배열 대신, 별로 클래스를 만들어 로직을 캡슐화 했다.

 

그릇된 정보를 피하라

그릇된 정보는 코드 의미를 흐린다.

축약어 같은 경우 보는 사람 관점에서 서로 다르게 해석될 여지가 있다.

hp 같은 경우 게임을 좋아하는 사람은 health point 노트북을 좋아하는 사람은 hp 회사를 떠올린다.

 

자료구조와 연관된 단어는 신중히 사용한다.

Account 여러 개를 그룹화하는 컨테이너를 만든다고 가정하면 AccountList 같은 이름은 List 구현한 처럼 보이기에 사용하면 안된다.

실제로 컨테이너가 List라도 List이름을 붙이는 것을 지양해야 한다.



class Ex003 {
    class Account {}
    class AccountList{} // 안 좋은 이름 마치 List를 구현한 것 처럼 느껴진다.
    //GOOD
    class AccountGroup{}
    class Accouts{}
}

 

되도록 비슷한 이름을 사용하는 것을 피해야 한다.

인지적으로 찾기 너무 힘들다.

클래스 이름이 만약 앞부분 제외하고, 같아면 크게 상관 없겠지만,

이름 길이도 비슷한데, 중간 부분 철저만 조금 다르면, 다른 점을 찾기가 힘들다.

 



class Ex003_2 {
    void exam(){
        //아무 의미없는 임시 변수명 지양
        int a = 0;
        int b = 1;
        if(a == b) {
            a = 10;
        }
    }
    void exam2(){
        //아무 의미없는 임시 변수명에, 비슷한 철자까지 더 하면 더 끔찍하다.
        int i = 0;
        int l = 1;
        int I = 2;
    }
    void exam3(){
        //보통 기업들 코딩 컨벤션을 보면, 임시 변수는 아래와 같이
        //반복문에 같은 곳에만 허용한다.
        for(int i = 0; i < 10; i++){
        }
    }
}

 

 

의미 있게 구분하라

 



class Ex004 {
    //나쁜 이름
    public static void copyChars(char[] a1, char[] a2){
        for(int i = 0; i < a1.length; i++){
            a2[i] = a1[i];
        }
    }
}
class Ex004_2 {
    //의미가 분명한 좋은 이름
    public static void copyChars(char[] source, char[] destination){
        for(int i = 0; i < source.length; i++){
            destination[i] = source[i];
        }
    }
}

a1, a2 같은 불용어를 쓰면, 어느 배열에서 어느 배열로 복사하는 로직을 봐야지 있다.

, 정보를 제공하지 않는 의미없는 불용어 때문에 메서드 내부 구조를 봐야한다.

 

불용어

문자 의미를 파악하는 기여하는 거의 없는 문자를 불용어라고 한다.

주로 접두나 접미에 많이 위치한다.

My, Info, Data, a, The 같은 것이 불용어다.

 

과거 좋은 IDE 없던 시절엔 유의미한 경우가 있었지만, 지금은 의미가 없다.

예를 들어, strName  String 타입 name 이라는 뜻이지만,

현재는 IDE 명확히 타입을 알려주기에 str 의미가 없는 불용어다.

 

만약, 계좌에 대한 정보를 담은 클래스를 찾아야하는

Account AccontInfo 클래스를 발견했다.

어떤 클래스를 봐야하는 알수가 없다.

 

남이 봤을 명확히 구분되는 이름을 사용해야 한다.

 

발음하기 쉬운 이름을 사용하라

인간의 두뇌는 단어라는 철자 묶음을 처리한다.

대부분의 단어는 발음하기도 쉽다.

따라서 발음하기 쉬운 이름은 단어를 사용하는 것이다.

 

이는 지나친 축약어에서 발견된다.

//발음하기 쉬운 이름을 사용하라
class Ex005 {
    //안 좋은 예
    private Date modymdhms; //y년, m월, d일, h시, m분, s초;
    //좋은 예
    private Date modificationTimestamp;
}

 

검색하기 쉬운 이름을 사용하라

문자 하나를 사용하는 상수는 찾기가 힘들다.

예시 ) 0, 1 , a, i

 

이런 이유로 이름이 검색이 쉬운 것은 당연하다.

 

이름의 길이는 자신이 속한 스코프 범위에 비례해야 한다.

 

예를 들어, 반복문 i 같은 변수는 괜찮다. 그런데 멀리 외부 범위 여기 저기서 읽어오는 변수이름이 i 라면 정말 찾기 힘들다.

class Ex006 {
    void bad() {
        //예시 용 변수
        int s = 0;
        int[] t = null;
        for (int j = 0; j < 34; j++ ) {
            s += (t[j] * 4 ) / 5;
        }
    }
}

s 그나마 누산용 변수인 것을 있다.

34까지 반복하는지 파악이 안된다.

t[] 배열은 무슨 배열이고, 계산식도 어떤 의미인지 파악이 안된다.

 

class Ex006_2 {
    void good() {
        //예시 용 변수
        int[] taskEstimate = null;
        int realDaysPerIdealDay = 4;
        final int WORK_DAYS_PER_WEEK = 5;
        int sum = 0;
        for (int j = 0; j < WORK_DAYS_PER_WEEK; j++ ) {
            int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
            int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK);
            sum += realTaskWeeks;
        }
    }
}

의미 있는 이름을 지으면, 메서드가 길어진다.

그래도 무엇을 하는지 있으며, 검색하기 쉽다.

 

인코딩을 피하라

이름에 불필요한 정보를 더하지 말아야한다.

 

헝가리식 표기법

프로그래밍에서 변수 함수 인자 이름 앞에 데이터 타입을 명시하는 표기법

String strName;

 

헝가리식 표기법이 대표적

 

과거에는 자원 문제로 이름 길이가 제한되거나, 컴파일러 기능이 미약해 타입 감지를

해줬다.

 

현재는 상황이 다르다. 필요없다.

 

자신의 기억력을 자랑하지 마라

i, s ,r 같이 한글자로 지어놓고, 기억해놨으니 괜찮다는 식은 안좋다.

아무리 자신의 기억력이 좋아도, 한참 보면 무슨 역할 변수인지 없다.

 

클래스 이름

명사나 명사구가 적합하다. 동사는 사용하지 않는다.

JSONParser, Product

불용어가 들어가게 조심한다.

ProductInfo, ProductData

 

메서드 이름

동사나 동사구가 적합하다.

 

 

기발한 이름은 피하라

코드를 개발한 자신만 있다. 심지어 나중에 자신이 봐도 기억 못할 있다.

무조건 명확한 이름이 좋다.

 

개념에 단어를 사용하라

메서드들을 예시로 들면

찾는 것은 find, 저장은 save, 삭제는 delete 것을 있다.

찾기라는 개념을 fetch, retrieve, search 혼용해서 사용하면 안된다.

말장난을 하지 마라



class Ex007 {
    //다른 곳도, add 어휘 메서드는 무언 가를 더한 값을 리턴한다.
    int add(int left, int right) {
        return left + right;
    }
    //무언가 컨테이너에 값을 더하는 메서드가 필요해서
    //구현하고 이름을 add로 지으면, 기존 add 어휘가 가지는 의미가 두 개가 된다.
    List<Object> list;
    void add(Object obj){
        list.add(obj);
    }
    //add 어휘에 일관성을 지키기 위해 insert라는 이름으로 바꾼다.
    void insert(Object obj){
        list.add(obj);
    }
}

 

해법 영역에서 가져온 이름을 사용하라

디자인 패턴이나, 알고리즘 같은 용어를 사용하는 것이 좋다.

적절한 해법 영역에 용어가 없다면, 문제 영역에서 이름을 가져온다.

 

코드 성격이 문제 영역인지, 해법 영역인지 어느 쪽에 가까운 판단 이름을 지어야 한다.

 

의미 있는 맥락을 추가하라

변수 하나의 이름이 스스로 의미있는 맥락을 가지는 경우는 거의 없다.



class Ex008_2 {
    //addr 라는 접두를 추가해 맥략을 분명히 했다.
    void eaxm() {
        String addrFirstName, addrLastName, addrStreet
            , addRhouseNumber, addrCity, addrState, addrZipcode;
        /*
         * 무언가 하는 코드...
         */
    }
}
class Ex008_3 {
    void eaxm() {
        Address address = new Address();
        /*
         * 무언가 하는 코드...
         */
    }
    //더 좋은 방법은 야에 새로운 클래스로 같은 맥락 변수를 묶는 것이다.
    class Address {
        String firstName, lastName, street, houseNumber, city, state, zipcode;
    }
}

불필요한 맥락을 없애라

미래 백화점 어플리케이션을 만든다고 가정하고,

어플리케이션에 클래스를 만드는데 굳이 불필요하게

Future Department Store 약어로 FDS 접두로 클래스마다 붙이는 것은 의미가 없다.

 

마치면서

우리는 클래스, 메서드 등에 붙은 모든 이름을 외우지 못한다.

좋은 이름을 짓는 것은 매우 중요하다.

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

4장 주석  (1) 2023.12.04
3장 함수  (1) 2023.11.27
1장 깨끗한 코드  (0) 2023.11.13
다시 시작  (0) 2023.11.10
마무리  (0) 2022.11.15

 

 

Clean Code(클린 코드) | 로버트 C. 마틴 | 인사이트- 교보ebook

애자일 소프트웨어 장인 정신, 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

ebook-product.kyobobook.co.kr

https://github.com/rkwhr0010/clean_code/tree/main/src

 

 

코드란 고객의 요구사항을 표현하는 언어다.

 

나쁜코드

당장의 일정 때문에 빠르게 "구동만 되는" 코드를 만들다보면, 시간이 지남에 따라 결국 코드 때문에 유지보수가 힘들어져 생산성 저하로 이어진다.

 

결과적으로 클린코드는 유지보수 용이성이 상승하여, 생산성 증대를 가져온다.

 

르블랑의 법칙

나중은 결국 오지 않는다. 지금해야 한다.

 

태도

코드 하나를 수정하려고 했는데, 다른 코드까지 수정해야 하는 경험을 해본 있을 것이다.

 

이는 개발자의 핵심이 크다.

 

요구사항이 자주 변경되거나, 일정이 촉박하다고 변명하면 안된다.

의사 선생님에게 간단한 외과 진료를 받는데, 빨리 치료받게 손씻지 말라고, 요구해서 의사가 손을 씻는 것을 본적 있는가?

 

나쁜 코드의 위험을 이해하지 못하는 관리자 말을 그대로 따는 것은 프로 답지 못한 행동이다.

 

난제

나쁜 코드는 업무 속도를 늦추게 되어있다.

하지만, 당장 눈앞에 시간을 맞추려고, 나쁜 코드를 양산하는 유혹에 빠진다.

 

이게 나쁜 것임을 알고 있지만, 앞의 일정을 지키려고, 나쁜 코드를 양산한다.

 

결과적으로 보자, 종국엔 나쁜 코드 때문에 어차피 기한을 맞추지 못할 날이 것이다.

결국, 빨리 가는 유일한 방법은 클린 코드다.

 

깨끗한 코드

클린 코드와 나쁜 코드를 구분할 아는 것과 직접 클린 코드를 작성하는 것은 다르다.

우리 목표는 클린 코드를 작성하는 방법을 채득하는 것이다.

 

비야네 스트롭스트룹
"나는 우아하고 효율적인 코드를 좋아한다. 논리가 간단해야 버그가 숨어들지 못한다. 의존성을 최대한 줄여야 유지보수가 쉬워진다. 오류는 명백한 전략에 의거해 철저히 처리한다. 성능을 최적으로 유지해야 사람들이 원칙 없는 최적화로 코드를 망치려는 유혹에 빠지지 않는다. 깨끗한 코드는 가지를 제대로 한다."

 

깨끗한 코드는 "보기에 즐거운" 코드다.

나쁜 코드는 코드를 고치면서 나쁜 코드를 만들게 한다.

 

깨진 창문 효과

깨진 창문은 관리되지 않는 느낌을 준다. 누구도 깨진다고 신경쓰지 않는다.

나중엔 자신도 창문을 깬다.

, 일단 창문이 깨지면 쇠퇴하게 된다.

 

깨끗한 코드는 세세한 사항까지 처리하는 코드다.(명명법, 메모리 누수 )

 

깨끗한 코드는 가지를 잘한다. 나쁜 코드는 여러 일을 하려다 목적과 의도가 모호해 진다.

 

그래디 부치
"깨끗한 코드는 단순하고 직접적이다. 깨끗한 코드는 문장처럼 읽힌다. 깨끗한 코드는 결코 설계자의 의도를 숨기지 않는다. 오히려 명쾌한 추상화와 단순한 제어문으로 가득하다."

 

깨끗한 코드는 소설과 같이 읽혀야 한다.

기승전결이 명확한 것처럼 클린 코드를 읽을 코드의 목적과 개발자가 의도한 바를 있어야 한다.

 

데이브 토마스
"깨끗한 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다. 단위 테스트 케이스와 인수 테스트 케이스가 존재한다. 깨끗한 코드에는 의미 있는 이름이 붙는다. 특정 목적을 달성하는 방법은 여러 가지가 아니라 하나만 제공한다. 의존성은 최소이며 의존성을 명확히 정의한다. API 명확하며 최소로 줄였다. 언어에 따라 필요한 모든 정보를 코드만으로 명확히 표현할 없기에 코드는 문학적으로 표현해야 마땅하다."

 

읽기 쉬운 코드와 고치기 쉬운 코드는 별개다.

 

테스트 케이스가 없는 코드는 아무리 우하한 코드라도 깨끗한 코드가 아니다.

 

코드 보다 작은 코드가 좋다.

 

마이클 페더스
"깨끗한 코드의 특징은 많지만 중에서도 모두를 아우르는 특징이 하나 있다. 깨끗한 코드는 언제나 누군가 주의 깊게 짰다는 느낌을 준다. 고치려고 살펴봐도 딱히 곳이 없다. 작성자가 이미 모든 사항을 고려했으므로, 고칠 궁리는 하다보면 언제나 제자리로 돌아온다. 그리고는 누군가 남겨준 코드, 누군가 주의 깊게 짜놓은 작품에 감사를 느낀다."

 

깨끗한 코드는 누군가 시간을 들여 깔끔하고 단정하게 정리한 코드다.

 

제프리스(요약)
중요도 좋은코드
  • 모든 테스트를 통과한다.
  • 중복이 없다.
  • 시스템 모든 설계 아이디어를 표현한다.
  • 클래스, 메서드, 함수 등을 최대한 줄인다.
표현력이 좋은 코드
  • 이름이 중요하다. 요즘 IDE 이름 변경이 매우 쉬워 좋은 이름으로 변경하기 좋다.
  • 책임이 많은 객체나 메서드를 찾아 여러 개로 나눠야 한다.
 
표현력이 좋은 코드
  • 프로그램에서 유사한 요소 집합이 보이면, 추상화한다.
    다시말해, 추상 클래스나 인터페이스로 실제 구현을 감싼다.
    이렇게 되면, 호출하는 쪽은 인터페이스를 바라보기 때문에 변경에 강한 코드가 된다

작게 추상화, 중복 제거, 기능만

 

 

워드 커닝햄
"코드를 읽으면서 짐작했던 기능을 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다. 코드가 문제를 풀기 위한 언어처럼 보인다면 아름다운 코드라 불러도 되겠다."

 

깨끗한 코드는 독해를 필요가 없어야 한다. 명백하고 단순하기 때문이다.

 

접근법

의심하지 말고, 빠져서 익혀볼 , 그렇다고 다른 방법론이 거짓이라는 것이 아니다.

충분히 학습 다른 방법론도 익히면서, 견문을 넓일

 

우리는 저자다

실제로 개발을 시작하면, 코드를 읽는 시간이 코드를 짜는 시간보다 훨씬 많다.

따라서 읽기 쉬운 코드를 짜는 것은 중요하다. 그래야 코드를 빠르게 있다.

 

보이스카우트 규칙
"캠프장은 처음 왔을 때보다 깨끗하게 해놓고 떠나라."

체크인 때보다 체크아웃 보다 깨끗한 코드를 만들면 된다.

노력을 기울일 필요 없다.

중요한 것은 지속성이다.

 

 

원칙

SOLID

  • SRP (Single Responsibility Principle)
    클래스는 변경할 , 가지 이유만 존재해야 한다.
  • OCP(Open Closed Principle)
    클래스는 확장엔 열리고, 변경에 닫혀 있어야 한다.
  • LSP(Liskov Substitution Principle)
    상속받은 클래스는 기초 클래스로 대체할 있어야 한다.
  • DIP(Dependency Inversion Principle)
    추상화에만 의존해야 한다. 구체화에 의존하면 안된다.
  • ISP(Interface Segregation Principle)
    클라이언트가 필요한 만큼 인터페이스로 기능을 최소한으로 분리해 제공해야 한다.

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

3장 함수  (1) 2023.11.27
2장 의미 있는 이름  (1) 2023.11.20
다시 시작  (0) 2023.11.10
마무리  (0) 2022.11.15
13장 동시성 - 2  (1) 2022.11.12

이 책은 처음 읽었을 당시는 머리에 남는 것은 좋은 이름 밖에 없었는데

지금 개발을 시작한지 2년이 약간 안되는 시점에 다시 읽어보니 훨씬 더 좋은 책이라는 것을 깨닫고 있다.

 

처음 읽었을 당시 개발을 접한지 1년도 안된 시점이라 잘 이해도 안갔다.

현재는 비교적 수월하게 읽히도 글귀에 숨겨진 의미도 보인다. 당시엔 객체지향 원칙, 디자인 패턴 등을 전혀 몰라서 이 책의 정수를 내것으로 취하지 못했다.

 

이렇게 좋은 책을 다시 읽지 않으면 나만 손해인 것 같아 다시 정리를 시작

 

https://ebook-product.kyobobook.co.kr/dig/epd/ebook/E000003160816

 

Clean Code(클린 코드) | 로버트 C. 마틴 | 인사이트- 교보ebook

애자일 소프트웨어 장인 정신, 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

ebook-product.kyobobook.co.kr

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

2장 의미 있는 이름  (1) 2023.11.20
1장 깨끗한 코드  (0) 2023.11.13
마무리  (0) 2022.11.15
13장 동시성 - 2  (1) 2022.11.12
13장 동시성 - 1  (0) 2022.11.08

개요

상속은 코드 재활용의 손쉬운 수단

 

상속 잘못 적용 예로 자바 스택 클래스가 대표적

스택은 리스트를 상속하고 있는데, 코드를 재사용하겠다는 취지는 좋았으나 불필요한 모든 기능이 모두 상속됐다.

 

구성 관계로 리스트를 사용해 필요한 기능만 위임했더라면 스택 API 이렇게 복잡하지 않았을 것이다.

 

이렇듯 슈퍼클래스의 기능이 서브클래스에 어울리지 않는다면 상속을 하면 안된다는 신호다.

 

제대로된 상속은 서브클래스가 슈퍼클래스의 모든 기능을 사용함과 동시에 서브클래스의 인스턴스를 슈퍼클래스의 인스턴스로도 취급할 있어야 한다.(리스코프 치환 원칙)

 

상속은 슈퍼클래스와 서브클래스가 강결합 상태이다. 위임은 일부 기능만 빌려올 별개이다.

슈퍼 클래스를 수정하면 모든 서브클래스가 영향을 받는다. 반면에 위임은 영향을 안받지만, 단점으로 위임 함수 모두를 전달 함수로 만들어야하는 수고로움이 생긴다.

예시

고서 보관하는 오래된 도서관



class CatalogItem{
    constructor(id, title, tags){
        this._id = id;
        this._title = title;
        this._tags = tags;
    }
    get id(){return this._id;}
    get title(){return this._title;}
    hasTag(arg){return this._tags.includes(arg);}
}
//오래된 스크롤에 정기 세척 이력 필요해 만듦
class Scroll extends CatalogItem{
    constructor(id, title, tags, dateLastCleaned){
        super(id, title, tags);
        this._lastCleaned = dateLastCleaned;
    }
    needsCleaning(targetDate){
        const threshold = this.hasTag("revered") ? 700 : 1500;
        return this.daySinceLastCleaning(targetDate) > threshold;
    }
    daySinceLastCleaning(targetDate){
        return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
    }
}


class Scroll extends CatalogItem{
    constructor(id, title, tags, dateLastCleaned){
        super(id, title, tags);
        //슈퍼 클래스 참조를 하나 만들고 슈퍼클래스를 생성한다.
        this._catalogItem = new CatalogItem(id, title, tags);
        this._lastCleaned = dateLastCleaned;
    }
    needsCleaning(targetDate){
        const threshold = this.hasTag("revered") ? 700 : 1500;
        return this.daySinceLastCleaning(targetDate) > threshold;
    }
    daySinceLastCleaning(targetDate){
        return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
    }
}


class Scroll extends CatalogItem{
    constructor(id, title, tags, dateLastCleaned){
        super(id, title, tags);
        //슈퍼 클래스 참조를 하나 만들고 슈퍼클래스를 생성한다.
        this._catalogItem = new CatalogItem(id, title, tags);
        this._lastCleaned = dateLastCleaned;
    }
    //슈퍼 클래스에 대응하는 메서드를 만들고, 전달 메서드화 시킨다.
    get id(){return this._catalogItem._id;}
    get title(){return this._catalogItem._title;}
    hasTag(arg){return this._catalogItem._tags.includes(arg);}
    needsCleaning(targetDate){
        const threshold = this.hasTag("revered") ? 700 : 1500;
        return this.daySinceLastCleaning(targetDate) > threshold;
    }
    daySinceLastCleaning(targetDate){
        return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
    }
}


class CatalogItem{
    constructor(id, title, tags){
        this._id = id;
        this._title = title;
        this._tags = tags;
    }
    get id(){return this._id;}
    get title(){return this._title;}
    hasTag(arg){return this._tags.includes(arg);}
}
class Scroll{//상속관계를 끊는다.
    constructor(id, title, tags, dateLastCleaned){
        //슈퍼 클래스 참조를 하나 만들고 슈퍼클래스를 생성한다.
        this._catalogItem = new CatalogItem(id, title, tags);
        this._lastCleaned = dateLastCleaned;
    }
    //슈퍼 클래스에 대응하는 메서드를 만들고, 전달 메서드화 시킨다.
    get id(){return this._catalogItem._id;}
    get title(){return this._catalogItem._title;}
    hasTag(arg){return this._catalogItem._tags.includes(arg);}
    needsCleaning(targetDate){
        const threshold = this.hasTag("revered") ? 700 : 1500;
        return this.daySinceLastCleaning(targetDate) > threshold;
    }
    daySinceLastCleaning(targetDate){
        return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
    }
}

 

가다듬기

슈퍼클래스를 위임으로 바꾸는 것은

다만 모든 카탈로그아이템 마다 스크롤 속성을 하나씩 가지게 됐는데, 이를 하나의 참조만 같도록 값을 참조로 바꾸기 리팩터링을 수행한다.



class Scroll{
    constructor(id, title, tags, dateLastCleaned){
        this._id = id;//먼저 id를 내 속성으로 만들기
        this._catalogItem = new CatalogItem(null, title, tags);
        this._lastCleaned = dateLastCleaned;
    }
    get id(){return this._id;}
    get title(){return this._catalogItem._title;}
    hasTag(arg){return this._catalogItem._tags.includes(arg);}
    needsCleaning(targetDate){
        const threshold = this.hasTag("revered") ? 700 : 1500;
        return this.daySinceLastCleaning(targetDate) > threshold;
    }
    daySinceLastCleaning(targetDate){
        return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
    }
}


//스크롤 데이터 읽는 부분 코드
const scrolls = aDocument
    .map(record => new Scroll(record.id,
                                record.catalogData.title,
                                record.catalogData.tags,
                                LocalDate.parse(record.lastCleaned)));



class Scroll{//먼저 적절한 저장소 찾기 또는 만들기, 여기선 적절한 저장소가 있다고 가정(catalog)
    constructor(id, title, tags, dateLastCleaned,catalogID, catalog){
        this._id = id;
        this._catalogItem = catalog.get(catalogID);
        this._lastCleaned = dateLastCleaned;
    }
    get id(){return this._id;}
    get title(){return this._catalogItem._title;}
    hasTag(arg){return this._catalogItem._tags.includes(arg);}
    needsCleaning(targetDate){
        const threshold = this.hasTag("revered") ? 700 : 1500;
        return this.daySinceLastCleaning(targetDate) > threshold;
    }
    daySinceLastCleaning(targetDate){
        return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
    }
}


class CatalogItem{
    constructor(id, title, tags){
        this._id = id;
        this._title = title;
        this._tags = tags;
    }
    get id(){return this._id;}
    get title(){return this._title;}
    hasTag(arg){return this._tags.includes(arg);}
}
class Scroll{//먼저 적절한 저장소 찾기 또는 만들기, 여기선 적절한 저장소가 있다고 가정(catalog)
    constructor(id, dateLastCleaned,catalogID, catalog){
        this._id = id;
        this._catalogItem = catalog.get(catalogID);
        this._lastCleaned = dateLastCleaned;
    }
    get id(){return this._id;}
    get title(){return this._catalogItem._title;}
    hasTag(arg){return this._catalogItem._tags.includes(arg);}
    needsCleaning(targetDate){
        const threshold = this.hasTag("revered") ? 700 : 1500;
        return this.daySinceLastCleaning(targetDate) > threshold;
    }
    daySinceLastCleaning(targetDate){
        return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
    }
}
//스크롤 데이터 읽는 부분 코드
const scrolls = aDocument
    .map(record => new Scroll(record.id,
                                LocalDate.parse(record.lastCleaned),
                                record.catalogData.id,
                                catalog ));

 

 

 

개요

다중 상속 언어가 아니면 일반적으로 상속은 단일 상속만 된다. 바꿔말하면 하나의 기준으로 분류할 밖에 없다. 상속은 부모클래스와 자식클래스가 강하게 묶이게된다. 부모를 수정하면 예외없이 모든 자식이 영향을 받는다.(단점이라는 소리는 아님)

 

위임은 이런 문제가 없다. 상속보다 결합도가 훨씬 낮기 때문이다.

 

디자인 원칙으로 "상속보다 구성을 사용하라"라는 말이 있다.

상속은 컴파일 시점에 행동이 고정된다. 반면에 구성은 런타임 시점에 동적으로 행동을 변화시킬 있다.

 

서브클래스를 위임으로 바꾸기 리팩터링은 디자인 패턴으로 따지면, 서브클래스를 상태나 전략 패턴으로 대체하기로 있다.

예시

class Booking{
    constructor(show, date){
        this._show = show;
        this._date = date;
    }
    get hasTalkback(){
        return this._show.hasOwnProperty('talkback') && !this.isPeakDay;
    }
    get basePrice(){
        let result = this._show.price;
        if(this.isPeakDay) result += Math.round(result * 0.15);
        return result;
    }
}
class PremiumBooking extends Booking{
    constructor(show, date, extras){
        super(show, date);
        this._extras = extras;
    }
    get hasTalkback(){
        return this._show.hasOwnProperty('talkback');
    }
    get basePrice(){
        return Math.round(super.basePrice + this._extras.premiumFee);
    }
    get hasDinner(){
        return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
    }
}
// 클라이언트 (일반 예약)
aBooking = new Booking(show, date);
// 클라이언트 (프리미엄 예약)
aBooking = new PremiumBooking(show, date, extras);

서브 클래스를 위임으로 바꾸는 이유, 상속은 번만 가능하다. 다른 상속을 사용해야할 이유가 지금 상속 구조보다 크다면, 지금 상속은 구성으로 바꿔야한다.


//생성자를 팩터리 함수로 변경해 캡슐화
function createBooking(show, date){
    return new Booking(show, date);
}
function createPremiumBooking(show, date, extras){
    return new PremiumBooking(show, date, extras);
}




// 클라이언트 (일반 예약)
aBooking = createBooking(show, date);
// 클라이언트 (프리미엄 예약)
aBooking = createPremiumBooking(show, date, extras);


//위임 클래스 만들기
class PremiumBookingDelegate{
    constructor(hostBooking, extras){
        //서브클래스는 super 키워드로 부모 클래스 멤버에 쉽게 접근할 수 있지만,
        //위임에선 역참조가 필수다.
        this._host = hostBooking;
        this._extras =extras;
    }
}


//프리미엄 예약을 대체할 새로운 위임을 연결
function createPremiumBooking(show, date, extras){
    const result = new PremiumBooking(show, date, extras);
    result._bePremium(extras);
    return result;
}


class Booking{
    constructor(show, date){
        this._show = show;
        this._date = date;
    }
    get hasTalkback(){
        return this._show.hasOwnProperty('talkback') && !this.isPeakDay;
    }
    get basePrice(){
        let result = this._show.price;
        if(this.isPeakDay) result += Math.round(result * 0.15);
        return result;
    }
    //이메서드가 private이라는 의미로 _ 붙임
    _bePremium(extras){
        this._premiumDelegate = new PremiumBookingDelegate(this, extras);
    }
}


class PremiumBooking extends Booking{
    constructor(show, date, extras){
        super(show, date);
        this._extras = extras;
    }
    get hasTalkback(){
        return this._premiumDelegate.hasTalkback;
    }
    get basePrice(){
        return Math.round(super.basePrice + this._extras.premiumFee);
    }
    get hasDinner(){
        return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
    }
}
//위임 클래스
class PremiumBookingDelegate{
    constructor(hostBooking, extras){
        this._host = hostBooking;
        this._extras =extras;
    }
    get hasTalkback(){
        return this._host._show.hasOwnProperty('talkback');
    }
}


class PremiumBooking extends Booking{
    constructor(show, date, extras){
        super(show, date);
        this._extras = extras;
    }
    /* 위임 메서드가 잘 동작하면, 이제 제거
    get hasTalkback(){
        return this._premiumDelegate.hasTalkback;
    }*/
    get basePrice(){
        return Math.round(super.basePrice + this._extras.premiumFee);
    }
    get hasDinner(){
        return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
    }
}


class Booking{
    constructor(show, date){
        this._show = show;
        this._date = date;
    }
    //위임 사용 로직 반영
    get hasTalkback(){
        return (this._premiumDelegate)
            ? this._premiumDelegate.hasTalkback
            : this._show.hasOwnProperty('talkback') && !this.isPeakDay;
    }
    get basePrice(){
        let result = this._show.price;
        if(this.isPeakDay) result += Math.round(result * 0.15);
        return result;
    }
    _bePremium(extras){
        this._premiumDelegate = new PremiumBookingDelegate(this, extras);
    }
}
//문제 발생
class PremiumBooking extends Booking{
    constructor(show, date, extras){
        super(show, date);
        this._extras = extras;
    }
    get basePrice(){
        return this._premiumDelegate.basePrice;
    }
    get hasDinner(){
        return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
    }
}
//위임 클래스
class PremiumBookingDelegate{
    constructor(hostBooking, extras){
        this._host = hostBooking;
        this._extras =extras;
    }
    get hasTalkback(){
        return this._host._show.hasOwnProperty('talkback');
    }
    //super로 부모 메서드 호출하는 부분을 방금과 같은 방식으로 리팩터링하면 무한 재귀에 빠진다., 상속을 구성으로 변경할 흔히 발생하는 문제
    get basePrice(){
        return Math.round(this._host.basePrice + this._extras.premiumFee);
    }
}


class Booking{
    constructor(show, date){
        this._show = show;
        this._date = date;
    }
    get hasTalkback(){
        return (this._premiumDelegate)
            ? this._premiumDelegate.hasTalkback
            : this._show.hasOwnProperty('talkback') && !this.isPeakDay;
    }
    //해결방안 1
    //슈퍼 클래스의 계산 로직을 함수로 추출, 가격 계산과 분배 로직 분리
    get basePrice(){
        return (this._premiumDelegate)
            ? this._premiumDelegate.basePrice
            : this._privateBasePrice;
    }
    get _privateBasePrice(){
        let result = this._show.price;
        if(this.isPeakDay) result += Math.round(result * 0.15);
        return result;
    }
    _bePremium(extras){
        this._premiumDelegate = new PremiumBookingDelegate(this, extras);
    }
}


//위임 클래스
class PremiumBookingDelegate{
    constructor(hostBooking, extras){
        this._host = hostBooking;
        this._extras =extras;
    }
    get hasTalkback(){
        return this._host._show.hasOwnProperty('talkback');
    }
    get basePrice(){
        return Math.round(this._host._privateBasePrice + this._extras.premiumFee);
    }
}



class Booking{
    constructor(show, date){
        this._show = show;
        this._date = date;
    }
    get hasTalkback(){
        return (this._premiumDelegate)
            ? this._premiumDelegate.hasTalkback
            : this._show.hasOwnProperty('talkback') && !this.isPeakDay;
    }
    //해결방안 2
    // 위임 메서드를 기반 메서드의 확장 형태로 재호출
    get basePrice(){
        let result = this._show.price;
        if(this.isPeakDay) result += Math.round(result * 0.15);
        return (this._premiumDelegate)
            ? this._premiumDelegate.extendBasePrice(result)
            : result;
    }
   
    _bePremium(extras){
        this._premiumDelegate = new PremiumBookingDelegate(this, extras);
    }
}


//위임 클래스
class PremiumBookingDelegate{
    constructor(hostBooking, extras){
        this._host = hostBooking;
        this._extras =extras;
    }
    get hasTalkback(){
        return this._host._show.hasOwnProperty('talkback');
    }
    extendBasePrice(base){
        return Math.round(base + this._extras.premiumFee);
    }
}


class Booking{
    constructor(show, date){
        this._show = show;
        this._date = date;
    }
    get hasTalkback(){
        return (this._premiumDelegate)
            ? this._premiumDelegate.hasTalkback
            : this._show.hasOwnProperty('talkback') && !this.isPeakDay;
    }
    get basePrice(){
        let result = this._show.price;
        if(this.isPeakDay) result += Math.round(result * 0.15);
        return (this._premiumDelegate)
            ? this._premiumDelegate.extendBasePrice(result)
            : result;
    }
   
    _bePremium(extras){
        this._premiumDelegate = new PremiumBookingDelegate(this, extras);
    }
    get hasDinner(){
        return (this._premiumDelegate)
            ? this._premiumDelegate.hasDinner
            : undefined;
    }
}
class PremiumBooking extends Booking{
    constructor(show, date, extras){
        super(show, date);
        this._extras = extras;
    }
}
//위임 클래스
class PremiumBookingDelegate{
    constructor(hostBooking, extras){
        this._host = hostBooking;
        this._extras =extras;
    }
    get hasTalkback(){
        return this._host._show.hasOwnProperty('talkback');
    }
    extendBasePrice(base){
        return Math.round(base + this._extras.premiumFee);
    }


    get hasDinner(){
        return (this._premiumDelegate)
            ? this._premiumDelegate.hasDinner
            : undefined; //자바스크립트는 에러를 유연하게 처리하는 방식이 주류
            //다른 객체지향동적 언어라면, 예외를 던지는게 더 나은 선택일 수 있다.
    }
}


function createBooking(show, date){
    return new Booking(show, date);
}
function createPremiumBooking(show, date, extras){
    const result = new Booking(show, date, extras);
    result._bePremium(extras);
    return result;
}
class Booking{
    constructor(show, date){
        this._show = show;
        this._date = date;
    }
    get hasTalkback(){
        return (this._premiumDelegate)
            ? this._premiumDelegate.hasTalkback
            : this._show.hasOwnProperty('talkback') && !this.isPeakDay;
    }
    get basePrice(){
        let result = this._show.price;
        if(this.isPeakDay) result += Math.round(result * 0.15);
        return (this._premiumDelegate)
            ? this._premiumDelegate.extendBasePrice(result)
            : result;
    }
   
    _bePremium(extras){
        this._premiumDelegate = new PremiumBookingDelegate(this, extras);
    }
    get hasDinner(){
        return (this._premiumDelegate)
            ? this._premiumDelegate.hasDinner
            : undefined;
    }
}
//위임 클래스
class PremiumBookingDelegate{
    constructor(hostBooking, extras){
        this._host = hostBooking;
        this._extras =extras;
    }
    get hasTalkback(){
        return this._host._show.hasOwnProperty('talkback');
    }
    extendBasePrice(base){
        return Math.round(base + this._extras.premiumFee);
    }
    get hasDinner(){
        return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
    }
}
// 클라이언트 (일반 예약)
aBooking = createBooking(show, date);
// 클라이언트 (프리미엄 예약)
aBooking = createPremiumBooking(show, date, extras);

상속을 구성으로 바꾸면서, 분배 로직과 양방향 참조 추가 복잡도가 높아졌다. 그만큼 유연성을 증가해 런타임 동적으로 프리미엄 예약으로 바꿀 있다는 이점이 생겼다. 추가로 필요한 곳에 상속을 사용할 있게 됐다.

 

 

예시 : 서브클래스가 여러 개일



function createBird(data){
    switch(data.type){
        case '유럽 제비' :
            return new EuropeanSwallow(data);
        case '아프리카 제비' :
            return new AfricanSwallow(data);
        case '노르웨이 제비' :
            return new NorwegianBlueParrot(data);
        default : return new Bird(data);
    }
}
class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    get airSpeedVelocity(){return null;}
}
class EuropeanSwallow extends Bird{
    get airSpeedVelocity(){return 35;}
}
class AfricanSwallow extends Bird{
    constructor(data){
        super(data);
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}
class NorwegianBlueParrot extends Bird{
    constructor(data){
        super(data);
        this._voltage = data.voltage;
        this._isNailed = data.isNailed;
    }
    get plumage(){
        if(this._voltage>100) return "그을렸다";
        else return this._plumage || "예쁘다";
    }
    get airSpeedVelocity(){
        return( this._isNailed) ? 0 : 10 + this._voltage / 10;
    }
}
야생 조류와 사육 조류를 구분 짓기
이렇게 구분 지을 경우, 기존 종에 따른 분류를 포기해야 한다.


//EuropeanSwallow부터 시작, 빈 위임 클래스 생성
class EuropeanSwallowDelegate{
}


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    //위임을 처리할 메서드 생성
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    get airSpeedVelocity(){return null;}
}


//위임 메서드로 옮김
class EuropeanSwallowDelegate{
    get airSpeedVelocity(){return 35;}
}
class EuropeanSwallow extends Bird{
    get airSpeedVelocity(){return this._speciesDelegate.airSpeedVelocity;}
}


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    //분배 메서드 처리
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}


//유럽 제비 클래스 제거, 관련 코드 정리
function createBird(data){
    switch(data.type){
        case '아프리카 제비' :
            return new AfricanSwallow(data);
        case '노르웨이 제비' :
            return new NorwegianBlueParrot(data);
        default : return new Bird(data);
    }
}


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data);
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}


//아프리카 제비 클래스 위임 처리
class AfricanSwallowDelegate{
    constructor(data){
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
}
class AfricanSwallow extends Bird{
    constructor(data){
        super(data);
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}


//메서드 옮기기
class AfricanSwallowDelegate{
    constructor(data){
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}
class AfricanSwallow extends Bird{
    constructor(data){
        super(data);
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return this._speciesDelegate.airSpeedVelocity;
    }
}


//아프리카 제비 클래스 제거, 관련 코드 정리
function createBird(data){
    switch(data.type){
        case '노르웨이 제비' :
            return new NorwegianBlueParrot(data);
        default : return new Bird(data);
    }
}
class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data);
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
class EuropeanSwallowDelegate{
    get airSpeedVelocity(){return 35;}
}
class AfricanSwallowDelegate{
    constructor(data){
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}
class NorwegianBlueParrot extends Bird{
    constructor(data){
        super(data);
        this._voltage = data.voltage;
        this._isNailed = data.isNailed;
    }
    get plumage(){
        if(this._voltage>100) return "그을렸다";
        else return this._plumage || "예쁘다";
    }
    get airSpeedVelocity(){
        return( this._isNailed) ? 0 : 10 + this._voltage / 10;
    }
}


function createBird(data){
    switch(data.type){
        case '노르웨이 제비' :
            return new NorwegianBlueParrot(data);
        default : return new Bird(data);
    }
}
//노르웨이 제비도 같은 작업 수행
class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data);
            case '노르웨이 제비' :
                return new NorwegianBlueParrotDelegate(data);
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
class EuropeanSwallowDelegate{
    get airSpeedVelocity(){return 35;}
}
class AfricanSwallowDelegate{
    constructor(data){
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}
class NorwegianBlueParrotDelegate{
    constructor(data){
        this._voltage = data.voltage;
        this._isNailed = data.isNailed;
    }
    get airSpeedVelocity(){
        return( this._isNailed) ? 0 : 10 + this._voltage / 10;
    }
}
class NorwegianBlueParrot extends Bird{
    constructor(data){
        super(data);
        this._voltage = data.voltage;
        this._isNailed = data.isNailed;
    }
    get plumage(){
        if(this._voltage>100) return "그을렸다";
        else return this._plumage || "예쁘다";
    }
    get airSpeedVelocity(){
        return this._speciesDelegate.airSpeedVelocity;
    }
}


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data);
            case '노르웨이 제비' :
                return new NorwegianBlueParrotDelegate(data,this);
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
class NorwegianBlueParrotDelegate{
    //plumage() 옮기기, 오버라이드 메서드로 역참조 필요
    constructor(data, bird){
        this._bird = bird;//역참조
        this._voltage = data.voltage;
        this._isNailed = data.isNailed;
    }
    get airSpeedVelocity(){
        return( this._isNailed) ? 0 : 10 + this._voltage / 10;
    }
    get plumage(){
        if(this._voltage>100) return "그을렸다";
        else return this._bird._plumage || "예쁘다";
    }
}
class NorwegianBlueParrot extends Bird{
    constructor(data){
        super(data);
        this._voltage = data.voltage;
        this._isNailed = data.isNailed;
    }
    get plumage(){
        return this._speciesDelegate.plumage;
    }
    get airSpeedVelocity(){
        return this._speciesDelegate.airSpeedVelocity;
    }
}
    //plumage() 분배 처리 문제 1
    get plumage(){
        if(this._speciesDelegate)
            return this._speciesDelegate.plumage;
        else
            return this._plumage || "보통이다";
    }
    //plumage() 분배 처리 문제 2
    //instanceof 연산자를 사용해 특정 클래스만 검사하는 것은 악취나는 코드다.
    get plumage(){
        if(this._speciesDelegate instanceof NorwegianBlueParrotDelegate)
            return this._speciesDelegate.plumage;
        else
            return this._plumage || "보통이다";
    }


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate(this);
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data,this);
            case '노르웨이 제비' :
                return new NorwegianBlueParrotDelegate(data,this);
            default: return null;
        }
    }
    get name(){return this._name;}
    //NorwegianBlueParrotDelegate만 특별 취급해보기
    get plumage(){
        if(this._speciesDelegate)
            return this._speciesDelegate.plumage;
        else
            return this._plumage || "보통이다";
    }
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
//plumage() 기본 메서드가 중복된다. 그리고 역참조를 추가하는 코드도 중복된다.
class EuropeanSwallowDelegate{
    constructor(bird){
        this._bird = bird;
    }
    get airSpeedVelocity(){return 35;}
    get plumage(){
        return this._bird._plumage || "예쁘다";
    }
}
class AfricanSwallowDelegate{
    constructor(data,bird){
        this._bird = bird;
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
    get plumage(){
        return this._bird._plumage || "예쁘다";
    }
}
class NorwegianBlueParrotDelegate{
    constructor(data, bird){
        this._bird = bird;
        this._voltage = data.voltage;
        this._isNailed = data.isNailed;
    }
    get airSpeedVelocity(){
        return( this._isNailed) ? 0 : 10 + this._voltage / 10;
    }
    get plumage(){
        if(this._voltage>100) return "그을렸다";
        else return this._bird._plumage || "예쁘다";
    }
}


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate(data, this);
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data,this);
            case '노르웨이 제비' :
                return new NorwegianBlueParrotDelegate(data,this);
            default: return new SpeciesDelegate(data, this);
        }
    }
    get name(){return this._name;}
    get plumage(){
        return this._speciesDelegate.plumage;
    }
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
//이 중복을 제거하기 위해 위임 클래스들에서 공통부분을 뽑아 슈퍼클래스로 만든다.
class SpeciesDelegate{
    constructor(data, bird){
        this._bird = bird;
    }
    get plumage(){
        return this._bird._plumage || "예쁘다";
    }
    airSpeedVelocity(){return null;}
}
class EuropeanSwallowDelegate extends SpeciesDelegate{
    constructor(data,bird){
        super(data, bird);
    }
    get airSpeedVelocity(){return 35;}
}
class AfricanSwallowDelegate extends SpeciesDelegate{
    constructor(data,bird){
        super(data, bird);
        this._bird = bird;
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}
class NorwegianBlueParrotDelegate extends SpeciesDelegate{
    constructor(data, bird){
        super(data,bird);
        this._voltage = data.voltage;
        this._isNailed = data.isNailed;
    }
    get airSpeedVelocity(){
        return( this._isNailed) ? 0 : 10 + this._voltage / 10;
    }
    get plumage(){
        if(this._voltage>100) return "그을렸다";
        else return this._bird._plumage || "예쁘다";
    }
}
//완료
function createBird(data){
    return new Bird(data);
}
class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate(data, this);
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data,this);
            case '노르웨이 제비' :
                return new NorwegianBlueParrotDelegate(data,this);
            default: return new SpeciesDelegate(data, this);
        }
    }
    get name(){return this._name;}
    get plumage(){return this._speciesDelegate.plumage; }
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
class SpeciesDelegate{
    constructor(data, bird){
        this._bird = bird;
    }
    get plumage(){
        return this._bird._plumage || "예쁘다";
    }
    airSpeedVelocity(){return null;}
}
class EuropeanSwallowDelegate extends SpeciesDelegate{
    constructor(data,bird){
        super(data, bird);
    }
    get airSpeedVelocity(){return 35;}
}
class AfricanSwallowDelegate extends SpeciesDelegate{
    constructor(data,bird){
        super(data, bird);
        this._bird = bird;
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}
class NorwegianBlueParrotDelegate extends SpeciesDelegate{
    constructor(data, bird){
        super(data,bird);
        this._voltage = data.voltage;
        this._isNailed = data.isNailed;
    }
    get airSpeedVelocity(){
        return( this._isNailed) ? 0 : 10 + this._voltage / 10;
    }
    get plumage(){
        if(this._voltage>100) return "그을렸다";
        else return this._bird._plumage || "예쁘다";
    }
}

 

개요

시간이 지남에 따라 상속 계층 구조가 이상 필요없어지는 경우 합친다.

예시

class Super{
    common(){/**슈퍼클래스 작업... */    }
}
class Sub extends Super{
    specific(){/**서브클래스 작업... */}
}


class Super{
    common(){/**슈퍼클래스 작업... */    }
    specific(){/**서브클래스 작업... */}
}
//위로 합치던가, 아래로 합치던가 판단하여 적용
class Sub{
    common(){/**슈퍼클래스 작업... */    }
    specific(){/**서브클래스 작업... */}
}

 

개요

비슷한 일을 수행하는 클래스가 보이면, 공통 부분을 추상화해 슈퍼클래스로 옮길 있다. 데이터는 필드 올리기, 동작은 메서드 올리기 리팩터링을 수행한다.

 

비슷한 대안으로 클래스 추출하기가 있다. 차이는 구성( 위임) 이용하냐 상속을 이용하냐 차이다.

 

예시

연간 비용, 월간 비용



class Employee{
    constructor(name, id, monthlyCost){
        this._id = id;
        this._name = name;
        this._monthlyCost = monthlyCost;
    }
    get monthlyCost() {return this._monthlyCost;}
    get name() {return this._name;}
    get id() {return this._id;}
    get annualCost(){ return this._monthlyCost * 12; }
}
class Department{
    constructor(name, staff){
        this._name = name;
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    get name(){return this._name;}
    get totalMonthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
    get totalAnnualCost(){
        return this.totalMonthlyCost * 12;
    }
}


class Party{} //일단 빈 슈퍼클래스를 만들고 확장한다.
class Employee extends Party{
    constructor(name, id, monthlyCost){
        super();
        this._id = id;
        this._name = name;
        this._monthlyCost = monthlyCost;
    }
    get monthlyCost() {return this._monthlyCost;}
    get name() {return this._name;}
    get id() {return this._id;}
    get annualCost(){ return this._monthlyCost * 12; }
}
class Department extends Party{
    constructor(name, staff){
        super();
        this._name = name;
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    get name(){return this._name;}
    get totalMonthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
    get totalAnnualCost(){
        return this.totalMonthlyCost * 12;
    }
}



class Party{
    constructor(name){//데이터부터 옮기기(생성자)
        this._name = name;
    }
}
class Employee extends Party{
    constructor(name, id, monthlyCost){
        super(name);
        this._id = id;
        this._monthlyCost = monthlyCost;
    }
    get monthlyCost() {return this._monthlyCost;}
    get name() {return this._name;}
    get id() {return this._id;}
    get annualCost(){ return this._monthlyCost * 12; }
}
class Department extends Party{
    constructor(name, staff){
        super(name);
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    get name(){return this._name;}
    get totalMonthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
    get totalAnnualCost(){
        return this.totalMonthlyCost * 12;
    }
}


class Party{
    constructor(name){
        this._name = name;
    }
    //옮긴 데이터 관련 메서드 올리기
    get name() {return this._name;}
}
class Employee extends Party{
    constructor(name, id, monthlyCost){
        super(name);
        this._id = id;
        this._monthlyCost = monthlyCost;
    }
    get monthlyCost() {return this._monthlyCost;}
    get id() {return this._id;}
    get annualCost(){ return this._monthlyCost * 12; }
}
class Department extends Party{
    constructor(name, staff){
        super(name);
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    get totalMonthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
    get totalAnnualCost(){
        return this.totalMonthlyCost * 12;
    }
}


class Department extends Party{
    constructor(name, staff){
        super(name);
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    //비슷한 로직은 좀 더 추상화된 쪽 이름으로, 혹은 추상화시켜 함수선언바꾸기
    get monthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
    get annualCost(){
        return this.monthlyCost * 12;
    }
}


class Party{
    constructor(name){
        this._name = name;
    }
    get name() {return this._name;}
    get annualCost(){
        return this.monthlyCost * 12;
    }
}
class Employee extends Party{
    constructor(name, id, monthlyCost){
        super(name);
        this._id = id;
        this._monthlyCost = monthlyCost;
    }
    get monthlyCost() {return this._monthlyCost;}
    get id() {return this._id;}
}
class Department extends Party{
    constructor(name, staff){
        super(name);
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    get monthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
}

 

개요

서브클래싱은 원래 데이터 구조와 다른 변종 또는 종류에 따라 다르게 동작하게 있는 유용한 메커니즘이나, 이상 사용이 안된다면 제거하는 것이 최선

 

예시



class Person{
    constructor(name){
        this._name = name;
    }
    get name(){return this._name;}
    get genderCode(){return "X";}
}
class Male extends Person{
    get genderCode(){return "M";}
}
class Female extends Person{
    get genderCode(){return "F";}
}
//클라이언트
const numberOfMales = people.filter(p=>p instanceof Male).length;


//직관적으로 생성자 마다 팩터리 메서드를 만들 경우
function createPerson(name){
    return new Person(name);
}
function createMale(name){
    return new Male(name);
}
function createFemale(name){
    return new Female(name);
}
//결국, 리팩터링하면 큰 의미가 없다.
function loadFromInput(data){
    const result = [];
    data.forEach(aRecord=>{
        let p;
        switch (aRecord.gender){
            case 'M' : p = new Male(aRecord.name); break;
            case 'F' : p = new Female(aRecord.name); break;
            default : p = new Person(aRecord.name);
        }
        result.push(p);
    });
    return result;
}


//생성할 클래스를 선택하는 로직을 함수로 추출하는 것이 더 낫다.
function createPerson(aRecord){
    let p;
    switch (aRecord.gender){
        case 'M' : p = new Male(aRecord.name); break;
        case 'F' : p = new Female(aRecord.name); break;
        default : p = new Person(aRecord.name);
    }
    return p;
}
function loadFromInput(data){
    const result = [];
    data.forEach(aRecord=>{
        result.push(createPerson(aRecord));
    });
    return result;
}


//두 함수 다듬기
function createPerson(aRecord){
    switch (aRecord.gender){
        case 'M' : return new Male(aRecord.name);
        case 'F' : return new Female(aRecord.name);
        default : return new Person(aRecord.name);
    }
}
//반복문 파이프라인으로 바꾸기
function loadFromInput(data){
    return data.map(aRecord=>createPerson(aRecord));
}


class Person{
    constructor(name){
        this._name = name;
    }
    //타입 검사 코드를 함수로 추출
    get isMale(){return this instanceof Male;}
    get name(){return this._name;}
    get genderCode(){return "X";}
}
class Male extends Person{
    get genderCode(){return "M";}
}
class Female extends Person{
    get genderCode(){return "F";}
}
//클라이언트
//클라이언트에서 instanceof 연산자를 사용하는 것부터 구린내가 난다.
const numberOfMales = people.filter(p=>p.isMale).length;


class Person{
    //생성자에 젠더코드 추가
    constructor(name, genderCode){
        this._name = name;
        this._genderCode = genderCode || "X";
    }
    get isMale(){return this instanceof Male;}
    get name(){return this._name;}
    get genderCode(){return this._genderCode;}
}


function createPerson(aRecord){
    switch (aRecord.gender){
        //하나씩 서브클래스 제거
        case 'M' : return new Person(aRecord.name, "M");
        case 'F' : return new Female(aRecord.name);
        default : return new Person(aRecord.name);
    }
}
function loadFromInput(data){
    return data.map(aRecord=>createPerson(aRecord));
}
class Person{
    //생성자에 젠더코드 추가
    constructor(name, genderCode){
        this._name = name;
        this._genderCode = genderCode || "X";
    }
    get isMale(){return "M"=== this._genderCode;}
    get name(){return this._name;}
    get genderCode(){return this._genderCode;}
}
class Female extends Person{
    get genderCode(){return "F";}
}


function createPerson(aRecord){
    switch (aRecord.gender){
        case 'M' : return new Person(aRecord.name, "M");
        case 'F' : return new Person(aRecord.name, "F");
        default : return new Person(aRecord.name, "X");
    }
}
function loadFromInput(data){
    return data.map(aRecord=>createPerson(aRecord));
}
class Person{
    constructor(name, genderCode){
        this._name = name;
        this._genderCode = genderCode ;
    }
    get isMale(){return "M"=== this._genderCode;}
    get name(){return this._name;}
    get genderCode(){return this._genderCode;}
}
//클라이언트
const numberOfMales = people.filter(p=>p.isMale).length;

 

+ Recent posts