클린코드 chap 3. 함수

최준영·2021년 10월 4일
0

클린한 코드

목록 보기
4/16

1. SOLID


1. SRP, 단일 책임 원칙

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

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

2. OCP, 개방-폐쇄 원칙

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

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

3. LSP, 리스코프 치환 원칙

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

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

4. ISP, 인터페이스 분리 원칙

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

  • 가능한 최소한의 인터페이스만 구현한다.
  • 만약 어떤 클래스를 이용하는 클라이언트가 여러 개고, 이들이 클래스의 특정 부분만 이용한다면, 여러 인터페이스로 분류하여 클라이언트가 필요한 기능만 전달한다.
  • SRP가 클래스의 단일 책임이라면, ISP는 인터페이스의 단일 책임이다.

5. DIP, 의존성 역전 원칙

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

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

예제 1. 나쁜 예

class PaymentController {
  @RequestMapping(value = "/api/payment", method = RequestMethod.POST)
  public void pay(@RequestBody ShinhanCardDto.PaymentRequest req) {
    shinhanCardPaymentService.pay(req);
  }
}

class ShinhanCardPaymentService {
  public void payShinhanCardDto.PaymentRquest req) {
    shinhanCardApi.pay(req);
  }
}

예제 2. 예제 1에 카드사가 추가되는 경우

class PaymentController {
  @RequestMapping(value = "/api/payment", method = RequestMethod.POST)
  public void pay(@RequestBody CardPaymentDto.PaymentRequest req) {
  if (req.getType() == CardType.SHINHAN) {
    shinhanCardPaymentService.pay(req);
  } else if(req.getType() == CardType.WOORI) {
    wooriCardPaymentService.pay(req);
  }
}
  • 확장성이 떨어진다.

예제 3. 좋은 예

  • 추상화된 인터페이스를 활용한다. 확장성이 좋아졌다.
class PaymentController {
  @RequestMapping(value = "/api/payment", method = RequestMethod.POST)
  public void pay(@RequestBody CardPaymentDto.PaymentRequest req) {
    final CardPaymentService cardPaymentService = cardPaymentFactory.getType(req.getType());
    cardPaymentService.pay(req);
  }
}

public interface CardPaymentService {
  void pay(CardPaymentDto.PaymentRequest req);
}

class ShinhanCardPaymentService {
  @Override
  public void payShinhanCardDto.PaymentRquest req) {
    shinhanCardApi.pay(req);
  }
}

2. 간결한 함수 작성하기


예제 1. 나쁜 예

public static renderPageWithSetupsAndTeardowns(pageData pagedata, boolean isSuite) throws Exception {
  boolean isTestPage = pageData.hasAttribute("Test");
  if (isTestPage) {
    Wikipage testpage = pageData.getWikipage();
    StringBuffer newPageContent = new StringBuffer();
    includeSetupPages(testPage, newPageContent, isSuite);
    newPageContent.append(pageData.getContent());
    includeTeardownPages(testPage, newPageContent, isSuite);
    pageData.setContent(newPageContent.toString());
  }
  return pageDate.getHtml();
}
  • 함수가 길고, 여러가지 기능이 섞여있다.
  • append도 있고 함수도 있고 초상화 수준이 뒤죽박죽이다.

예제 2. 좋은 예

public static renderPageWithSetupsAndTeardowns(pageData pagedata, boolean isSuite) throws Exception {
  if (isTestpage(pageData)
    includeSetupAndTeardownPages(pageData, isSuite);
  return pageData.getHtml();
}
  • 작게 쪼갠다.
  • 함수 내 초상화 수준을 동일하게 맞춘다.

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

예제 3. 나쁜 예

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

예제 4. 좋은 예

public abstract class Employee {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}
 
public interface EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
 
public class EmployeeFactoryImpl implements EmployeeFactory {
    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);
    }
}
  • 계산과 타입 관리를 분리
  • 타입에 대한 처리는 최대한 Factory에서만 한다.

함수 인수

  • 인수의 갯수는 0 ~ 2개가 적당하다.
  • 3개 이상에서는?
// 객체를 인자로 넘기기
Circle makeCircle(double x, double y, double radius); // bad
Circle makeCircle(Point center, double radius); // good

// 가변 인자를 넘기가 -> 특별한 경우가 아니라면 잘 사용하지 않는다.
String.format(String format, Object... args);

3. 안전한 함수 작성하기


부수 효과(side Effect)없는 함수

  • 부수효과란 값을 반환하는 함수가 외부 상태를 변경하는 경우이다.
  • 예) checkPassword 함수 내에 session.initialize();가 있는 경우
    • 함수와 관계없는 외부 상태를 변경시킨다.

4. 함수 리팩터링


  1. 기능을 구현하는 서투른 함수를 작성한다.
    • 길고, 복잡하고, 중복도 있다.
  2. 테스트 코드를 작성한다.
    • 함수 내부의 분기와 엣지값마다 빠짐없이 테스트하는 코드를 짠다.
    • 정상적으로 동작하는 경우에 다음단계로 넘어가야한다.
  3. 리팩터링 한다.
    • 코드를 다듬고, 함수를 쪼개고, 이름을 바꾸고, 중복을 제거한다.

6. 실습 강의에서 배운 것


enum 타입 필드에 추가 설명 붙이기

  • description을 선언하고 해당 필드가 무엇인지 설명하는 용도로 사용했다.
  • enum에 대해 더 알아봐야겠다.

@AllArgsConstructor

  • 롬북 코드에 속하며, 해당 클래스의 모든 멤버필드를 파라미터로 갖는 생성자를 자동으로 생성한다.
@AllArgsConstructor
pubic enum ActionType {
  PURCHASE(description: "매매");
  RENT(description: "임대차");
  
  private String description;
}

// TODO

  • 코드를 짜기 전에 어떤 코드를 짜야할지 해당 주석과 함께 써놓는다.

폴더 구조

  • 참고하기위해 작성했다.

숫자 사이에 _

  • 123_456_789와 같이 숫자 사이에 언더바를 넣어서 가독성을 향상시킬 수 있다.

인터페이스 디폴트 메소드

  • 인터페이스에서 선언한 메소드는 구현 클래스에서 오버라이드 해야한다.
  • 하지만, 인터페이스의 메소드에 default를 붙여주면, 구현클래스에서 오바라이드 할 필요없이 바로 사용 가능하다.

리팩터링

나쁜 예 : 계산이 목적인 함수에 객체를 생성 기능도 포함됨.

calcBrokerage.java

@GetMapping("/api/calc/brokerage")
public Long calcBrokerage(@RequestParam ActionType actionType, @RequestParam Long price) {
  if (actionType == ActionType.PURCHASE) {
    PurchaseBrokeragePolicy policy = new PurchaseBrokeragePoicy();
    return policy.calcuate(price);
  }
  if (actionType == ActionType.Rent) {
    PurchaseBrokeragePolicy policy = new RentBrokeragePoicy();
    return policy.calcuate(price);
  }
}

좋은 예 : 객체 생성은 Factory 클래스에서 구현

BrokeragePolicy.java

public interface BrokeragePolicy {
  brokeraeRule createBrokerageRule(Long price);
  
  default Long calculate(Long price) {
    BrokerageRule rule = createBrokerageRule(price);
    return rule.calcuMaxBrokerage(price);
  }
}

BrokeragePolicyFactory.java

  • factory 클래스 구현 : 타입 처리 부분은 해당 클래스에서 분기되도록 한다.
  • default를 사용하여 case에 없는 작업이 들어온 경우 예외를 던짐
public class BrokeragePolicyFactory {
  public static BrokeragePolicy of(ActionType actionType) {
    switch (actionType) {
      case RENT:
        return new RentBrokeragePolicy();
      case PURCHASE:
        return new PurchaseBrokeragePolicy();
      default:
        throw new IllegalArgumentException("해당 actionType에 대한 정책이 존재하지 않습니다.");
    }
  }
}

calcBrokerage.java

@GetMapping("/api/calc/brokerage")
public Long calcBrokerage(@RequestParam ActionType actionType, @RequestParam Long price) {
  BrokeragePolicy policy = BrokeragePolicyFactory.of(actionType);
  return policy.calculate(price);
}
profile
do for me

0개의 댓글