함수

ppby·2021년 9월 8일
0

ppby.TIL

목록 보기
21/26
post-thumbnail

책의 예시들을 typescript로 작성하였습니다. 의미가 안 맞을 수 있지만 최대한 비슷한 느낌으로 작성하려 노력했습니다🥺


1. SOLID 원칙

SRP (단일 책임 원칙)

한 클래스는 하나의 책임만 가져야 한다.

  • 클래스는 하나의 기능만 가지며, 어떤 변화에 의해 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
  • SRP 책임이 분명해지기 떄문에, 변경에 의한 연쇄작용에서 자유로워질 수 있다.
  • 가독성 향상과 유지보수가 용이해진다.
  • 실전에서는 쉽지 않지만 늘 상기해야 한다.

OCP (개발-폐쇄 원칙)

소프트웨어 요소는 확장에는 열려 있느나 변경에는 닫혀 있어야 한다.

  • 변경을 위한 비용은 가능한 줄이고, 확장을 위한 비용은 가능한 극대화 해야 한다.
  • 요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소에는 수정이 일어나지 않고, 기존 구성 요소를 쉽게 확장해서 재사용한다.
  • 객체지향의 추상화와 다형성을 활용한다.

LSP (리스코프 치환 원칙)

서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다.

  • 서브 타입은 기반 타입이 약속한 규약(접근제한자, 예외 포함)을 지켜야 한다.
  • 클래스 상속, 인터페이스 상속을 이용해 확장성을 획득한다.
  • 다형성과 확장성을 극대화하기 위해 인터페이스를 사용한느 것이 더 좋다.
  • 합성(composition)을 이용할 수도 있다.

ISP (인터페이스 분리 원칙)

자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.

  • 가능한 최소한의 인터페이스만 구현한다.
    (인터페이스를 뚱뚱하게 만들기 보다는 나눌 수 있는 정도로 나눠서 사용하는 것)
  • 만약 어떤 클래스를 이용하는 클라이언트가 여러 개고, 이들이 클래스의 특정 부분만 이용한다면, 여러 인터페이스로 분류하여 클라이언트가 필요한 기능만 전달한다.
  • SRP가 클래스의 단일 책임이라면, ISP는 인터페이스의 단일 책임

DIP (의존성 역전 원칙)

상위 모델은 하위 모델에 의존하면 안된다. 둘 다 추상화에 읜존해야 한다. 추상화는 세부 사항에 의존해서는 안된다. 세부 사항은 추상화에 따라 달라진다.

  • 하위 모델의 변경이 상위 모듈의 변경을 요구하는 위계관계를 끊는다.
  • 실제 사용관계는 그대로이지만, 추상화를 매개로 메시지를 주고 받으면서 관계를 느슨하게 만든다.

예제) 결제 api라고 가정

// 대략 클라이언트에서 요청을 받으면 카드사 api를 호출하는 과정
type PaymentRequest = {
  cardType: 'SHINHAN' | 'WOORI';
  ...
}
  
class PaymentController {
  pay(req: PaymentRequest) {
    shinhanCardPaymentService.pay(req);
  }
}

class ShinhanCardPaymentService {
  pay(req: PaymentRequest) {
    shinhanCardApi.pay(req);
  } 
}

새로운 카드사가 추가된다면??

// 확장에 유연하지 않다... 카드사 마다 의존성을 가짐
...

pay(req: PaymentRequest) {
  if (req.cardType === 'SHINHAN') {
    shinhanCardApi.pay(req);
  } else if (req.cardType === 'WOORI') {
    wooriCardApi.pay(req);
  }
} 

사이에 추상화된 인터페이스 넣기

  
  // 확장에 유연하지 않다... 카드사 마다 의존성을 가짐
  type PaymentRequest = {
    cardType: 'SHINHAN' | 'WOORI';
    ...
  }

  interface CardPaymentService {
    pay(req: PaymentRequest): void;
  }

  class PaymentController {
    pay(req: PaymentRequest) {
      // cardPaymentFactory <- 카드사들을 묶어 놓음
      const carPaymentService: CardPaymentService = cardPaymentFactory(req.cardType)
      carPaymentService.pay(req)
    } 
  }

  class ShinhanCardPaymentService implements CardPaymentService {
    pay(req: PaymentRequest) {
      shinhanCardApi.pay(req);
    } 
  }

  class WooriCardPaymentService implements CardPaymentService {
    pay(req: PaymentRequest) {
      wooriCardApi.pay(req);
    } 
  }

2. 간결한 함수 작성하기

작게 만들어라!

  1. 가로 150자 미만, 20줄 이하
  2. 블록과 들여쓰기 → 한번의 들여쓰기만

한 가지만 해라!

  • 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수있다면 그 함수는 여러 작업을 하는 셈이다.

함수 당 추상화 수준은 하나로! (동일한 수준으로)

  • high, middle, low 로 나뉜다

  • 함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.

    한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다. 하지만 문제는 이 정도로 그치지 않는다. 근본 개념과 세부사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다.

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

    • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.

Switch 문

switch 문은 작게 만들기 어렵다. case 분기가 단 두 개인 switch 문도 내 취향에는 너무 길며, 단일 블록이나 함수를 선호한다. 또 '한 가지' 작업만 하는 switch 문도 만들기 어렵다. 본질적으로 switch 문은 N가지를 처리한다. 불행하게도 switch 문을 완전히 피할 방법은 없다.
하지만 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다. 물론 다형성(polymorphism)을 이용한다.

  • switch 문을 추상 팩토리(ABSTRACT FACTORY)에 꽁꽁 숨긴다.
  • switch 문을 단 한번만 쓰자 → 다형적 객체를 생성하는 코드 안에서만 → 상송 관계로 숨긴 후에는 절대로 다른 코드에 노출하지 않는다.

한 가지만 하기(SRP), 변경에 닫게 만들기(OCP)

예시)

...

// 계산도 하고, Money도 생성한다.. 두 가지 기능이 보임, 또 새로운 직원 타입이 추가된다면??ㅠ
calculatePay(e: Employee): Money {
  switch (e.type) {
    case 'A':
      return calculateAPay(e);
    case 'A':
      return calculateAPay(e);
    case 'A':
      return calculateAPay(e);
    default:
      throw new Error(`invalid type ${e.type}`)
  }
}

...
/* 분리 - 계산을 하는 부분과 타입을 처리하는 부분 */

type EmployeeRecord = {
  type: 'A' | 'B' | 'C';
  ...
};

// 추상 클래스
abstract class Employee {
  abstract isPaydat(): boolean;
  abstract calculatePay(): Money;
  abstract deliverPay(pay: Money): void;
}

// type에 대한 처리는 최대한 Factory에서만
interface EmployeeFactory {
  makeEmployee(r: EmployeeRecord): Employee;
}

// 이제 Money는 calculatePay를 통해서 계산
class EmployeeFactoryImpl implements EmployeeFactory {
  makeEmployee(r: EmployeeRecord): Employee {
    switch (r.type) {
      case 'A':
        return new AEmployee(r);
      case 'B':
        return new BEmployee(r);
      case 'C':
        return new CEmployee(r);
      default:
        throw new Error(`Invalid Employee type ${r.type}`);
    }
  }
}

...

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

  • 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
  • 또, 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
  • 함수 이름 작성할 때는 여러 단어가 쉽게 읽히는 명명법을 사용
  • 일관성이 있게 붙히기

3. 안전하 함수 작성하기

함수 인수(Argument)

함수에서 이상적인 인수 개수는 0개(무항),
다음은 1개(단항)
다음은 2개(이항)
다음은 3개(삼항) → 가능하면 피하는 편이 좋다
다음은 4개(다항) → 특별한 이유가 필요하다 → 이유가 있더라도 쓰지 말자

헷갈리는 용어
인자 값 === 매개변수 === Parameter
인수(Argument)는? → 함수를 호출할 때 사용되는 값(들)

부수 효과(side effect)를 일으키지 마라!

명령과 조회를 분리하라!

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다.

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

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다. 자칫하면 if문에서 명명을 표현식으로 사용하기 쉬운 탓이다.

  • try/catch 블록 뽑아내기
    • 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤썪는다. 그러무로 try/catch 블록을 별도 함수로 뽑아내는 편이 좋다.

반복하지 마라!

구조적 프로그래밍

  • 함수는 return 문이 하나, 루프 안에서 breakcontinue 를 사용해선 안된다.

4. 리팩터링

  1. 기능을 구현하는 서투른 함수를 작성한다. (길고, 복잡하고, 중복도 있다.)
  2. 테스트 코드를 작성한다. (함수 내부의 분기와 엣지값마다 빠짐없이 테스트하는 코드를 짠다.)
  3. 리팩터링 한다. (코드를 다듬고, 함수를 쪼개고, 이름을 바꾸고, 중복을 제거한다.)

결론

  • 함수는 동사, 클래스는 명사
profile
(ง •̀_•́)ง 

0개의 댓글