[엘레강트 오브젝트] 자동차 경주 미션에 적용하기

유소정·2025년 4월 13일
0
post-thumbnail

1️⃣ 미션 소개

스크린샷 2025-04-13 오후 8 54 07

2️⃣ 스터디 내용 반영한 저장소

3️⃣ 책을 읽으며 구현에 참고한 부분

1장

  • 적절한 클래스 이름을 짓는다. 클래스 이름은 '무엇을 하는지'가 아니라 '무엇인지'를 나타낸다.
  • '생성자 수'가 '메서드 수'보다 많게 한다. 생성자는 오버로딩으로 구현한다.
  • 주 생성자 안에는 코드를 넣지 않는다. 주 생성자 안에는 할당과 유효성 검사 로직만 있을 수 있다.
  • 유효성 검사는 주 생성자 안에서만 한다.

2장

  • 상태(식별자)는 4개 이하로 한다.
  • 인터페이스를 항상 사용한다. 인터페이스는 계약서이다.
  • 메서드 이름을 지을 때 신경쓴다.
    • 빌더: '(형용사+)명사' 형태로 짓는다. 새로운 객체를 반환한다. void를 반환하지 않는다.
    • 조정자: '동사' 형태로 짓는다. 엔티티를 수정한다. void를 반환한다.
    • Boolean 값을 반환할 때는 is-* 형태로 짓지 않는다. 형용사로 짓는다. ex) empty
    • 퍼블릭 상수를 사용하지 않는다. 아무리 사소해도 항상 작은 클래스를 만든다.
    • 불변 객체로 만든다.

3장

  • 5개 이하의 public 메서드만 노출시킨다.
  • 정적 메서드는 사용하지 않는다.
  • 주 생성자와 메서드 안에서 new 연산자를 사용하지 않는다.
  • 유틸리티 클래스에서는 클래스 인스턴스 생성을 막기 위해 private ctor를 추가한다.
  • 싱글톤은 죄악이다. 사용하지 않는다.
  • 조합 가능한 데코레이터(다른 객체를 감싸는 객체)로 만든다. 다중 계층 구조로 구성하기 시작하면 조합이 가능하다.
  • Null 사용을 금지한다. null 객체를 만들어라. (null 패턴)
    • BAD: mask === null
    • GOOD: mask.empty()
  • 절대 getter와 setter를 사용하지 않는다. (의도가 있는 getter와 setter는 가능하다. 하지만 필드가 private이라는 이유로 값을 외부로 내보내주기 위한 용도는 안된다.)

4장

  • 빠르게 실패해야 한다. (위에서 언급했듯이, 생성자에서 검증이 가능하다.)
  • 예외는 상위로 전파한다.
  • 항상 예외는 체이닝한다.
  • 예외는 각 진입점 별로 오직 하나의 try-catch 문을 갖는다.
  • 가장 최상위 수준에서 예외는 오직 한번만 복구한다.

📝 스터디 내용 정리

스터디 내용을 전반적으로 잘 정리한 글이라서 링크를 남긴다.

📝 구현하면서 참고한 자료


4️⃣ 구현에 대한 피드백

🙋‍♀️ 나 : 아래 코드에서 ❓ 표시한 질문에 대해 어떻게 생각하세요?
👨‍🍳 코치님: 의존성이 있다고 볼 수 있네요?
🙋‍♀️ 나: 최대한 의존성이 없게 코드를 짜고 싶었는데, 어쩔 수 없이 의존해야 하는 부분이 생기는 것 같아요.
👨‍🍳 코치님: 맞아요. 어쩔 수 없이 그런 부분이 생깁니다. Garage 객체가 생성될 때 Car 객체를 받도록 하는 건 어때요? 그럼 의존성을 제거할 수 있지 않을까요?
🙋‍♀️ 나: 마찬가지일 것 같아요. Garage 객체는 불변 객체로 사용하기 때문에 const car = new Car({ name, position }); 로직은 withInsertedCar 함수 내에서 불러줘야 합니다. 말씀해진대로 의존성을 제거하려면 withCar 함수가 Car 객체를 받도록 할 수는 있어요. 하지만 withCar 인자로 Car 객체를 주고 싶진 않네요.
👨‍🍳 코치님: 그렇군요. 이 정도의 의존성은 괜찮다고 봅니다. 언제나 의존성을 떼어내는 게 최선의 방법은 아니니까요.

import Car from "./Car.ts";

export default class Garage {
  readonly cars: Car[];

  constructor({ cars = [] }: { cars?: Car[] } = {}) {
    this.cars = cars;
  }

  init() {
    return new Garage({
      cars: [],
    });
  }

  withCar({ name, position }: { name: string; position: number }): Garage {
    const index = this.cars.findLastIndex((i) => i.name === name);

    return index < 0
      ? this.withInsertedCar({ name, position })
      : this.withUpdatedCar({ index, change: position });
  }

  // ❓ new Car 이것도 의존성이 있는 건가?
  private withInsertedCar({
    name,
    position,
  }: {
    name: string;
    position: number;
  }): Garage {
    const car = new Car({ name, position });

    return new Garage({
      cars: [...this.cars, car],
    });
  }

...
}

🙋‍♀️ 나 : 아래 코드에서 ❓ 표시한 질문에 대해 어떻게 생각하세요?
👨‍🍳 코치님: RacingGame은 Controller 인거죠?
🙋‍♀️ 나 : 네, 맞아요.
👨‍🍳 코치님: 그러면 입력 받은 값을 파싱하는 부분은 Controller에 있어도 괜찮겠는데요? Controller의 역할이 입력받은 값을 정제화해서 Domain으로 넘겨주는 걸 포함한다면요.
🙋‍♀️ 나 : 그렇게 생각하면 괜찮겠네요. Controller에 로직이 있는 게 맞을까? 관점으로 봤는데, 역할 관점에서 보면 데이터 정제는 해도 괜찮겠어요.

// Controller 입니다.

export default class RacingGame {
  private garageStore: GarageStore;

  constructor(garageStore: GarageStore) {
    this.garageStore = garageStore;
  }

  async start() {
    await this.자동차등록();
    await this.자동차경주();
    this.경주내용출력();
    this.우승자출력();
  }

 ...
  
  // ❓ carNames.split(",") 어디서 처리하는 게 좋을까?
  private async getCarNames() {
    const input = await InputView.readCarNames();
    return input.split(",");
  }
...

객체 지향 관점을 React에 적용해보세요!

  • JavaScript로만 구현하는 미션을 지나 React에 오면서 상태 관리 부분을 Hook이라는 도구와 함께 React에게 맡기게 되었다. 그럼 이제 어떻게 이 관점을 이어갈 것인가가 관건인데, "객체 지향 관점을 React에 적용해보세요!"라는 조언을 받았다. 앞으로 어떻게 적용해볼까...
  • 폭풍 질문에 친절하게 답변해주신 코치님 감사합니다 🙇‍♂️ (제 얘기를 잘 들어주시고 긍정적으로 반응해주셔서 더 공부하고 싶은 욕구가 상승합니다!)
profile
기술을 위한 기술이 되지 않도록!

0개의 댓글