728x90
인터페이스, 추상 클래스, 합성 사용 시점
인터페이스 사용 시점
- 서로 독립적으로 존재하지만 공통된 행위(계약)을 가진 객체들에게 적합하다.
- 상호작용하지 않는 객체
- 부모가 아닌 구현체 둘이 함께 고려되어야 하는 상황이 거의 없다면 사용하자.
- 구현체 둘이 함께 고려되어야한다면 합성 사용하기
크리스마스) 할인 정책
// 증정 정책에 대한 공통 행위(규약)을 정의
interface Gift {
boolean isApplicable();
BigDecimal calculateAmount();
Map<Menu, Quantity> provideGiftItems();
String getName();
}
class ChampagneGift implements Gift {
private static final int MIN_ORDER_PRICE = 120_000;
private final Orders orders;
@Override
public boolean isApplicable() {
return orders.isTotalPriceOverThan(BigDecimal.valueOf(MIN_ORDER_PRICE));
}
@Override
public Map<Menu, Quantity> provideGiftItems() {
if (isApplicable()) {
return Map.of(Menu.샴페인, new Quantity(1));
}
return Collections.emptyMap();
}
}
// 디저트 3개 이상 구매시 보너스 케이크 증정
class DessertBonusGift implements Gift {
private static final int MIN_DESSERT_COUNT = 3;
private final Orders orders;
@Override
public boolean isApplicable() {
return orders.getDessertCount() >= MIN_DESSERT_COUNT;
}
@Override
public Map<Menu, Quantity> provideGiftItems() {
if (isApplicable()) {
return Map.of(Menu.케이크, new Quantity(1));
}
return Collections.emptyMap();
}
}
- 독립적으로 동작한다.
- 샴페인 증정과 케이크 증정은 서로 영향을 주지 않는다.
- 각 증정 정책은 자신만의 조건과 증정 품목을 가진다.
- 샴페인 증정과 케이크 증정은 서로 영향을 주지 않는다.
- 공통된 행위(인터페이스)를 가진다.
- 모든 증정 정책은 적용 가능 여부 확인(
isApplicable()
) - 증정할 상품 제공(
provideGiftItems()
) - 증정 상품의 가치 계산(
calculateAmount()
)
- 모든 증정 정책은 적용 가능 여부 확인(
- 서로 다른 구현 방식
- 각각 구현 방식이 다르다.
- 확장 용이성
- 새로운 증정 정책 추가가 쉽다. (인터페이스만 구현하면 됨)
- 기존 코드 수정 없이 새로운 정책 추가 가능
추상 클래스 사용 시점
- 서로 밀접하게 연관되어 있고 공통된 상태와 로직을 가지며 기본 구현을 공유할 필요가 있으면 추상 클래스를 사용하는 것이 더 적절하다.
- 부모를 상속한 구현체끼리 함께 사용되지 않고, 부모 클래스와 강한 연관이 있을때 사용하자.
- 공통된 상태, 행위를 부모로부터 물려받음
크리스마스) 할인 정책
크리스마스 할인 정책들은 서로 밀접하게 연관되어 있고
공통된 상태와 로직을 가지며
기본 구현을 공유할 필요가 있어서 추상 클래스를 사용하는 것이 더 적절하다.
- 공통된 상태
day
와orders
는 모든 할인 정책에서 공유하는 필드이다.- 최소 주문 금액 조건도 공통으로 사용된다.
- 공통된 로직
isApplicable()
처럼 할인 적용 가능 여부 확인 로직이 동일하다.- 추상 클래스에서 기본 구현을 제공하고 하위 클래스에서 재사용된다.
- 밀접한 관계
- 모든 할인 정책이 주문과 날짜에 의존
- 할인 계산과 적용 조건이 서로 연관됨
// 크리스마스 할인 정책들의 공통 부분을 담당하는 추상 클래스
public abstract class Discount {
private static final int MIN_AMOUNT = 10_000;
protected final Day day;
protected final Orders orders;
public Discount(final Day day, final Orders orders) {
this.day = day;
this.orders = orders;
}
// 공통된 할인 적용 조건 검사
public boolean isApplicable() {
return isMoreThanMinimumOrderAmount(orders) && isWithinDeadline();
}
// 각 할인 정책마다 다른 부분
public abstract BigDecimal calculateAmount();
public abstract String getName();
protected abstract boolean isWithinDeadline();
}
// 디데이 할인
public class ChristmasDdayDiscount extends Discount {
private static final int DISCOUNT_START_PRICE = 1_000;
private static final int DISCOUNT_DAY_UNIT = 100;
public ChristmasDdayDiscount(final Day day, final Orders orders) {
super(day, orders);
}
@Override
public BigDecimal calculateAmount() {
if (isApplicable()) {
BigDecimal discount = BigDecimal.valueOf(DISCOUNT_START_PRICE);
BigDecimal untilChristmas = new BigDecimal(DISCOUNT_DAY_UNIT).multiply(
new BigDecimal(day.diffFromFirstDay()));
return discount.add(untilChristmas);
}
return BigDecimal.ZERO;
}
@Override
public String getName() {
return "크리스마스 디데이 할인";
}
@Override
protected boolean isWithinDeadline() {
return !day.isExceedChristmas();
}
}
public class DayDiscount extends Discount {
private static final int DISCOUNT_AMOUNT = 2_023;
private static final String WEEKDAYS_DISCOUNT = "평일 할인";
private static final String WEEKEND_DISCOUNT = "주말 할인";
public DayDiscount(final Day day, final Orders orders) {
super(day, orders);
}
@Override
public BigDecimal calculateAmount() {
if (isApplicable()) {
if (day.isWeekday()) {
return BigDecimal.valueOf(DISCOUNT_AMOUNT * orders.countSameTypeMenu(MenuType.DESSERT));
}
return BigDecimal.valueOf(DISCOUNT_AMOUNT * orders.countSameTypeMenu(MenuType.MAIN));
}
return BigDecimal.ZERO;
}
@Override
public String getName() {
if (day.isWeekday()) {
return WEEKDAYS_DISCOUNT;
}
return WEEKEND_DISCOUNT;
}
@Override
protected boolean isWithinDeadline() {
return day.isInDecember();
}
}
인터페이스를 사용한다면?
interface DiscountPolicy {
BigDecimal calculateAmount();
boolean isApplicable();
String getName();
}
- 공통 상태와 로직을 각각 구현해야 한다.
- 코드 중복이 발생한다.
강결합 -> 합성 사용하기
강한 결합 관계
두 객체가 서로 밀접하게 연관되어 함께 동작해야 하는 관계
예시: 편의점 재고 관리 시스템
일반 재고와 프로모션 재고
- 재고 차감시 항상 두 재고를 함께 확인해야 한다.
- 프로모션 재고를 우선적으로 사용한다.
- 총 재고량은 두 재고의 합으로 계산한다.
인터페이스 사용시 문제점
interface Stock {
void subtract(int quantity);
int getQuantity();
}
class RegularStock implements Stock {
private int quantity;
@Override
public void subtract(int quantity) {
this.quantity -= quantity;
}
}
class PromotionStock implements Stock {
private int quantity;
private Promotion promotion;
@Override
public void subtract(int quantity) {
this.quantity -= quantity;
}
}
// 사용하는 코드
class OrderService {
public void order(Stock stock, int quantity) {
// 문제점 1) instanceof 사용
if (stock instanceof PromotionStock) {
// 프로모션 재고 처리
}
// 문제점 2) 두 재고를 함께 고려해야 할 때 복잡해짐
int totalQuantity = regularStock.getQuantity() + promotionStock.getQuantity();
if (totalQuantity < quantity) {
throw new IllegalArgumentException();
}
// 문제점 3) 재고 차감 우선순위 처리가 어려움
if (promotionStock.getQuantity() > 0) {
}
}
}
문제점
- 타입 체크 남용 :
instanceof
- instanceof를 사용한다.
- 새로운 재고 타입 추가시 if문 추가해야한다.
- 책임 분산 :
stock
-RegularStock
,PromotionStock
- 재고 관리 로직이 여러 곳에 흩어진다.
- 비즈니스 로직을 수행하기 어렵다.
- 코드가 복잡해짐
- 여러 객체의 상태를 동시에 관리해야 한다.
합성을 통해 해결하기
- 밀접한 관계가 있는 개념들을 하나의 클래스 안에서 합성으로 관리하는 것이 좋다.
항상 함께 고려되거나, 서로 강한 결합관계를 가질때 합성을 사용하자.
public class Stock {
private final PromotionStock promotionQuantity;
private final RegularStock regularQuantity;
public Stock(int promotionQuantity, int regularQuantity) {
this.promotionQuantity = promotionQuantity;
this.regularQuantity = regularQuantity;
}
// 재고 차감 로직을 Stock 클래스 한 곳에서 수행 가능
public Stock subtract(int quantity) {
if (getTotalQuantity() < quantity) {
throw new IllegalArgumentException("재고 부족");
}
int remainingQuantity = quantity;
int newPromotionQuantity = promotionQuantity;
int newRegularQuantity = regularQuantity;
// 프로모션 재고 우선 사용
if (promotionQuantity > 0) {
int promotionUsed = Math.min(remainingQuantity, promotionQuantity);
newPromotionQuantity -= promotionUsed;
remainingQuantity -= promotionUsed;
}
// 남은 수량은 일반 재고에서 차감
if (remainingQuantity > 0) {
newRegularQuantity -= remainingQuantity;
}
return new Stock(newPromotionQuantity, newRegularQuantity);
}
public int getTotalQuantity() {
return promotionQuantity + regularQuantity;
}
}
합성의 장점
class OrderService {
public void order(Stock stock, int quantity) {
Stock updatedStock = stock.subtract(quantity);
}
}
- 캡슐화
- 재고 관리의 모든 책임이 Stock 클래스에 집중되어, 새로운 재고 타입 추가시 Stock 클래스 내부만 수정하면 된다.
- 내부 구현을 외부로부터 숨김으로써 캡슐화를 지킨다.
- 재고 차감 우선순위가 한 곳에서 관리됨으로써 로직을 수행하기 편하다.
강한 결합 관계에 있는 객체들은 합성을 통해 하나의 클래스로 관리하자!
결론
추상 클래스 - 클래스 상속
abstract class Discount { ... }
class ChristmasDiscount extends Discount { ... }
class WeekdayDiscount extends Discount { ... }
- 공통된 상태와 행위를 부모로부터 물려받아 사용한다.
- 부모 클래스(Discount)와 강한 연관관계를 가진다.
- 함께 사용되지 않을때 사용한다.
추상 클래스, 일반 클래스 사용시점
- 일반 클래스 사용시점 : 객체 일부가 Default class로 사용될때
인터페이스
interface Payable { ... }
class CardPayment implements Payable { ... }
class BankTransfer implements Payable { ... }
- 구현체들은 서로 독립적으로 동작한다.
- 부모와도 느슨한 결합 (구현만 강제)
- 함께 사용될 일이 거의 없을때 사용한다.
합성
class Stock {
private final int promotionQuantity;
private final int regularQuantity;
}
- 항상 함께 고려되어야 할때
- 서로 강한 결합관계를 가진다.
- 두 개념(promotionQuantity, regularQuantity)을 한 클래스에서 함께 관리된다.
- 인터페이스: 구현체들이 서로 독립적, 부모와도 느슨한 결합
- 추상클래스: 구현체들은 독립적이지만, 부모와 강한 결합
- 합성: 두 개념이 강한 결합으로 함께 관리될때
728x90
'설계' 카테고리의 다른 글
[오브젝트] 상속과 다형성 (2) | 2024.12.01 |
---|---|
[오브젝트] 객체 지향 설계 (0) | 2024.12.01 |
[설계] Okky ERD 분석하기 (0) | 2024.04.26 |