위 책을 보면서 정리한 글입니다.
다중 상속
예)
-'클래스 A'는 ArrayList로 생성된 '인스턴스 B'와 LinkedList로 생성된 '인스턴스 C'를 지니고 있다.
-'클래스 A'의 '메서드 B'는 '인스턴스 B'에서 관리하는 자료를 하나씩 처리하는 기능을 구현.
-'클래스 A'의 '메서드 C'는 '인스턴스 C'에서 관리하는 자료를 하나씩 처리하는 기능을 구현.
-이를 개선하기 위해 '메서드 Z'를 만들고, List Interface를 통해 값을 하나씩 처리하는 기능을 만든다.
-ArrayList와 LinkedList를 묶어 List로 instance를 만들고, 이를 메서드 Z에 위임하여 '메서드 B'와 '메서드 C'의 중복 코드를 제거
규약으로 만들어진 다중 상속 때문에 '구상 클래스(Concrete Class)'를 사용하는 클래스는 어쩔 수 없이 중복 코드를 만들어야 했다.
인터페이스를 다중 상속하는 것이 나쁜 방법일까
- 다중 상속이 나쁘 다는 것은 아니다. 다만 상속 관계, 인터페이스, 클래스의 추상화가 명확한 기준으로 구분되어 있다면 상속처럼 좋은 도구도 없다.
- 대부분의 현업에서는 구현된 다중 상속은 여러 가지 문제를 내포하는 경우가 있다.
- 다중 상속이 하나의 책임에 대한 기능 확장의 개념으로 사용되지 않는다면 여러 가지 책임의 집합 클래스로 전락하기 마련.
- 이는 클래스간 결합도를 높이는 꼴이 되고, 가독성을 나쁘게 만들어 결국 생산성 저하로 이어지게 된다.
- C++의 다이아 몬드 문제, 자바의 interface는 구현체가 없어 이렇게 될 확률은 낮지만 문제가 생긴다면 상속받은 클래스의 오버라이드 메서드가 어느 인터페이스의 기능을 담당하는지 알 수 없게 된다.
- 여러 구문이 자연스럽게 구현된다면 해당 인터페이스는 결국 하나의 타입으로 재구성해야 한다.
public interface DiscountGoods {
public void setGoodsID(Lisat<Integer> goodsIDList);
public float getDiscountPercent();
public List<GoodsVO> getDiscountGoodsList();
}
public interface TimeEvent {
public void setEventPeriod(long beginTime, long finishTime);
public long getLeftTime();
}
마감이 임박한 할인 상품에 대한 구상 클래스 할인 상품에 대한 인터페이스와, 시간적 제약 요소를 정의하고 있는 인터페이스를 상속받아 구현체로 만들어졌다.
LastMinuteGoods.setSaleGoodsID()
- 상품의 고유 ID를 받아서 DB 에 있는 데이터를 매핑한 후, 매핑된 데이터를 VO 객체로 전환하는 작업
LastMinuteGoods.setEventPeriod()
- 이벤트 기간의 시작 시간과 종료 시간을 받아서 저장하고 잔여 시간을 계산하여 저장하며, 합당한 시작 시간과 종료 시간이 매개변수로 전달되었는지를 점검
import java.net.CacheRequest;
import java.security.DrbgParameters.Reseed;
public class LastMinuteGoods implements DiscountGoods, TimeEvent {
private String eventTitle;
private float salePercent;
private long leftTime;
private List<GoodsVO> saleGoods = new ArrayList();
@Override
public void setGoodsID(LisT<Integer> goodsIDList) {
for(int goodsID : goodsIDList) {
GoodsVO goodsVO = selectGoods(goodsID);
addGoods(goodsVO);
}
}
@Override
public gloat getDiscountPercent() {
return this.salePercent;
}
@Override
public List<GoodsVO> getDiscountGoodsList() {
return this.saleGoods;
}
@Override
public void setEventPeriod(long beginTime, long finishTime) {
boolean reasonable = initPeriod(beginTime, finishTime);
if(reasonable) {
long leftTime = calculateLeftTime();
timeToEnroll(leftTime);
}
// 중략
}
@Override
public long getLeftTime() {
return calculateLeftTime();
}
// 중략
}
public class Recommend {
// 중략
private void initTimeEventGoods(RecoomendType type, List<Integer> goodsIDList, long beginTime, long finishTime) {
switch(type) {
case LAST_MINUTE: {
LastMinuteGoods goods = new LastMinuteGoods();
goods.setGoodsID(goodsIDList);
goods.setEventPeriod(beginTime, finishTime);
addRecommendDataLastMinuteSaleGoods(goods);
} break;
case FLASH: {
FlashDiscountGoods goods = new FlashDiscountGoods();
goods.setGoodsID(goodsIDList);
goods.setEventPeriod(beginTime, finishTime);
addRecommendDataFlashSaleGoods(goods);
} break;
}
}
private boolean lastMinuteGoodsDisplay(LastMinuteGoods goods) {
float percent = goods.getDiscountPercent();
long leftTime = goods.getLeftTime();
List<GoodsVO> saleGoodVOItemList = goods.getDiscountGoodsList();
for(GoodsVO item : saleGoodsVOItemList) {
// 중략
}
// 중략
}
private boolean flashSaleGoodsDisplay(FlashDiscountGoods goods) {
float percent = goods.getDiscountPercent();
long leftTime = goods.getLeftTime();
List<GoodsVO> saleGoodVOItemList = goods.getDiscountGoodsList();
for(GoodsVO item : saleGoodsVOItemList) {
// 중략
}
// 중략
}
}
문제가 발생하는 부분은 구상 클래스가 아닌, 구상 클래스를 사용하는 클래스를 구현하는 순간부터 문제가 발생한다.
(Recommend 클래스를 구현할 때)
이전의 Recommend 클래스의 세 개의 메서드에서 중복 부분이 있는 것을 볼 수 있을 것이다.
따라서 이를 해결하기 위해서는 가장 먼저 인터페이스의 구조를 살펴봐야 한다.
규약이라는 측면에서 TimeEvent를 별도로 정의하였으나, DiscountGoods에 종속적인 인터페이스이다.
- TimeEvent가 DiscountGoods를 상속받도록 한다.
- TimeEvent와 DiscountGoods를 상속받는 클래스들을 수정
- 2의 클래스를 사용하는 클래스의 중복 코드를 제거
public interface TimeEvent extends DisocuntGoods {
public void setEventPeriod(long beginTime, long finishTime);
public long getLeftTime();
}
public class LastMinuteGoods implements TimeEventSaleGoods {
// 중략
}
public class FlashDiscountGoods implements TimeEventSaleGoods {
// 중략
}
public class Recommend {
// 중략
private void initTimeEventGoods(RecoomendType type, List<Integer> goodsIDList, long beginTime, long finishTime) {
TimeEventSaleGoods goods = null;
switch(type) {
case LAST_MINUTE:
goods = new LastMinuteGoods();
break;
case FLASH:
goods = new FlashDiscountGoods();
break;
}
goods.setGoodsID(goodsIDList);
goods.setEventPeriod(beginTime, finishTime);
addRecommedDataDeadLineSaleGoods(goods);
}
private boolean timeEventSaleGoodsDisplay(TimeEventSaleGoods goods) {
float percent = goods.getDiscountPercent();
long leftTime = goods.getLeftTime();
List<GoodsVO> saleGoodVOItemList = goods.getDiscountGoodsList();
for(GoodsVO item : saleGoodsVOItemList) {
// 중략
}
// 중략
}
}
단순히 메서드 규약으로 만들려고 인터페이스를 사용하는 것은 좋은 아이디어가 아니다.
- 잘못 사용하면 안티 패턴을 양산, 이 때 가장 크게 야기되는 문제가 중복 코드 발생과 유연성 저하
- 같은 인터페이스를 다중 상속하는 구상 클래스가 여러 개인지 확인
- 구상 클래스를 사용하는 객체에 이와 관련된 중복 코드가 있는지 확인
- 다중 상속하는 인터페이스들이 하나의 타입으로 묶일 수 있다면, 새로운 인터페이스로 만들거나, 인터페이스 상속을 이용
- 구상 클래스를 사용하는 객체는 새로운 인터페이스로 인스턴스를 만들어 처리하는 구문을 만들고 이에 해당하는 중복 코드를 제거하고 모듈화를 진행