package Goat.core.discount;
import Goat.core.member.Grade;
import Goat.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class RateDiscountPolicyTest {
DiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10%할인이 적용되어야만 한다.")
void vip_o(){
Member member = new Member(1l,"goat1", Grade.VIP);
int discount = discountPolicy.discount(member,10000);
Assertions.assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않습니다.")
void vip_x(){
Member member = new Member(1L, "goat2", Grade.BASIC);
int discount = discountPolicy.discount(member,10000);
Assertions.assertThat(discount).isEqualTo(1000);
}
}
package Goat.core.order;
import Goat.core.discount.DiscountPolicy;
import Goat.core.discount.FixDiscountPolicy;
import Goat.core.discount.RateDiscountPolicy;
import Goat.core.member.Member;
import Goat.core.member.MemberRepository;
import Goat.core.member.MemoryMemberRepository;
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member resultMember = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(resultMember,itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
이렇게 변경을 하지만 이것이 문제가 있다는 것이다.
바로 '클라이언트의 코드를 변경' 했다는 점인데, 이 점이, 좋은 객체지향 코드의 조건에 어긋난다.
DIP : OrderServiceImpl이 결국 추상(인터페이스) 인 DiscountPolicy 뿐 아니라, RateDiscountPolicy(구현체)도 의존하고 있으므로 어긋남.
OCP : 할인정책을 변경하기 위해서 OrderServiceImpl 코드도 변경 해야하므로 어긋남
package Goat.core.order;
import Goat.core.discount.DiscountPolicy;
import Goat.core.discount.FixDiscountPolicy;
import Goat.core.discount.RateDiscountPolicy;
import Goat.core.member.Member;
import Goat.core.member.MemberRepository;
import Goat.core.member.MemoryMemberRepository;
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
private DiscountPolicy discountPolicy;
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member resultMember = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(resultMember,itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
하지만 DiscountPolicy 의 구현객체가 없다 -> 문제가 생길 수 밖에 없음
final
final은 값이 반드시 할당되어야 함. (생성자 방식이든, 동호로 하든)
각 클래스마다 생성자를 통해서
AppConfig 파일에서 인터페이스와 구현체를 연결해준다.
package Goat.core.member;
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
위 처럼, MemberServiceImpl 에서 MemoryMemberRepository를 불러오는게 아닌,
package Goat.core.member;
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
생성자 방식으로 만든다. -> 생성자 주입
이렇게하면 오로지 Interface인 추상에만 의존하는 DIP를 철저히 지킬 수 있다.
그 후, AppConfig 파일에 다음과 같이 작성한다.
package Goat.core;
import Goat.core.member.MemberService;
import Goat.core.member.MemberServiceImpl;
import Goat.core.member.MemoryMemberRepository;
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
마찬가지로 OrderService도 적절히 수정해보자.
package Goat.core.order;
import Goat.core.discount.DiscountPolicy;
import Goat.core.member.Member;
import Goat.core.member.MemberRepository;
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member resultMember = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(resultMember,itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
package Goat.core;
import Goat.core.discount.FixDiscountPolicy;
import Goat.core.member.MemberService;
import Goat.core.member.MemberServiceImpl;
import Goat.core.member.MemoryMemberRepository;
import Goat.core.order.OrderService;
import Goat.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
정리
AppConfig는 실제 동작에 필요한 구현 객체를 생성자 주입을 통해 생성, 연결한다.
package Goat.core;
import Goat.core.member.Grade;
import Goat.core.member.Member;
import Goat.core.member.MemberService;
import Goat.core.member.MemberServiceImpl;
import Goat.core.order.Order;
import Goat.core.order.OrderService;
import Goat.core.order.OrderServiceImpl;
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
Long memberId = 1L;
Member member = new Member(memberId,"Goat", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId,"itemA",10000);
System.out.println("order = "+order);
}
}
한가지 예시로만 가져왔다.
TEST CODE에서는 왜 이렇게 작성해야 하는걸까?
package Goat.core.member;
import Goat.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@Test
void join(){
//given
Member member = new Member(1L,"Goat1",Grade.VIP);
//when
memberService.join(member);
Member findMember = memberService.findMember(1L);
//then
Assertions.assertThat(member).isEqualTo(findMember);
}
}
앞에서 AppConfig로 바로 불러올 수 없었다.