골격 구현(skeletal implementation)은 인터페이스와 추상 클래스의 이점을 함께 사용할 수 있는 디자인이다.
상황
다양한 유형의 상품을 판매하는 자판기를 만들어보자.
코드
public interface Ivending {
void start();
void chooseProduct();
void stop();
void process();
}
// 사탕 자판기
public class CandyVending implements Ivending {
@Override
public void start() {
System.out.println("Start Vending machine");
}
@Override
public void chooseProduct() {
System.out.println("Produce different candies");
System.out.println("Choose a type of candy");
System.out.println("Pay for candy");
}
@Override
public void stop() {
System.out.println("Stop Vending machine");
}
@Override
public void process() {
start();
chooseProduct();
stop();
}
}
// 음료 자판기
public class DrinkVending implements Ivending {
@Override
public void start() {
System.out.println("Start Vending machine");
}
@Override
public void chooseProduct() {
System.out.println("Produce different soft drinks");
System.out.println("Choose a type of soft drinks");
System.out.println("Pay for drinks");
}
@Override
public void stop() {
System.out.println("Stop Vending machine");
}
@Override
public void process() {
start();
chooseProduct();
stop();
}
}
// 클라이언트
public class VendingManager {
public static void main(String[] args) {
Ivending candy = new CandyVending();
Ivending drink = new DrinkVending();
candy.process();
drink.process();
}
}
문제점
start(), stop(), process() 메서드가 각각의 구현에서 동일한 작업을 수행한다.
→ 단일 책임 원칙(SRP, Single Responsibility Principle) 위반. 산탄총 수술(shotgun surgery) 문제 발생 가능.
인터페이스의 단점
인터페이스의 모든 메서드를 구현해야 하다보니 구현 클래스에서 일부 메서드가 중복될 수 있다.
추상 클래스로 해결?
// 자판기 추상 클래스
public abstract class AbstractVending {
public void start()
{
System.out.println("Start Vending machine");
}
public abstract void chooseProduct();
public void stop()
{
System.out.println("Stop Vending machine");
}
public void process()
{
start();
chooseProduct();
stop();
}
}
// 사탕 자판기
public class CandyVending extends AbstractVending {
@Override
public void chooseProduct() {
System.out.println("Produce different candies");
System.out.println("Choose a type of candy");
System.out.println("Pay for candy");
}
}
// 음료 자판기
public class DrinkVending extends AbstractVending {
@Override
public void chooseProduct() {
System.out.println("Produce different soft drinks");
System.out.println("Choose a type of soft drinks");
System.out.println("Pay for drinks");
}
}
// 클라이언트
public class VendingManager {
public static void main(String[] args) {
AbstractVending candy = new CandyVending();
AbstractVending drink = new DrinkVending();
candy.process();
System.out.println("*********************");
drink.process();
}
}
문제점
CandyVending 및 DrinkVending은 추상 클래스를 확장하고 있으므로, 다른 클래스를 상속받을 수 없다.
예를 들어, 자판기를 청소하고 점검하는 VendingServicing 클래스를 추가하고 싶다고 가정해보자.
위 코드는 이미 AbstractVending을 확장했기 때문에 VendingServicing을 확장할 수 없다. 그렇다면 컴포지션 패턴을 통해 풀어야 하지만 VendingMachine을 컴포지션 안에 넣게 됨으로써 VendingServicing과 VendingMachine이 강하게 결합된다.
추상 클래스의 단점
다이아몬드 문제로 인해 다중 상속을 지원하지 않는다.
골격 구현(추상 인터페이스)
방법
- 인터페이스를 만든다.
- 인터페이스의 구현 및 공통 메서드의 구현을 제공하는 추상 클래스를 만든다.
- 구체 클래스에서도 인터페이스를 구현하게 하고, private inner 클래스로 만들어 추상 클래스를 확장하게 한다.
이제 구체 클래스는 추상 클래스에 호출을 위임하여 공통 메서드를 사용하는 동시에 모든 인터페이스를 구현할 수 있다.
// 자판기 인터페이스
public interface Ivending {
void start();
void chooseProduct();
void stop();
void process();
}
// 자판기 청소 클래스
public class VendingService {
public void service() {
System.out.println("Clean the vending machine");
}
}
// 자판기 추상 클래스
public abstract class AbstractVending implements Ivending {
public void start() {
System.out.println("Start Vending machine");
}
public void stop() {
System.out.println("Stop Vending machine");
}
public void process() {
start();
chooseProduct();
stop();
}
}
// 사탕 자판기
public class CandyVending implements Ivending {
private class AbstractVendingDelegator extends AbstractVending {
@Override
public void chooseProduct() {
System.out.println("Produce different candies");
System.out.println("Choose a type of candy");
System.out.println("Pay for candy");
}
}
AbstractVendingDelegator delegator = new AbstractVendingDelegator();
@Override
public void start() {
delegator.start();
}
@Override
public void chooseProduct() {
delegator.chooseProduct();
}
@Override
public void stop() {
delegator.stop();
}
@Override
public void process() {
delegator.process();
}
}
// 음료 자판기
public class DrinkVending extends VendingService implements Ivending {
private class AbstractVendingDelegator extends AbstractVending {
@Override
public void chooseProduct() {
System.out.println("Produce different soft drinks");
System.out.println("Choose a type of soft drinks");
System.out.println("Pay for drinks");
}
}
AbstractVendingDelegator delegator = new AbstractVendingDelegator();
@Override
public void start() {
delegator.start();
}
@Override
public void chooseProduct() {
delegator.chooseProduct();
}
@Override
public void stop() {
delegator.stop();
}
@Override
public void process() {
delegator.process();
}
}
// 클라이언트
public class VendingManager {
public static void main(String[] args) {
Ivending candy = new CandyVending();
Ivending drink = new DrinkVending();
candy.process();
System.out.println("*********************");
drink.process();
if (drink instanceof VendingService) {
VendingService vs = (VendingService) drink;
vs.service();
}
}
}
골격 구현의 이점
- 하위 클래스는 DrinkVending과 같은 다른 클래스들을 확장할 수 있다.
- 추상 클래스에 호출을 위임함으로써 중복 코드를 제거할 수 있다.
- 하위 클래스에서 새로운 인터페이스의 구현이 가능해진다.
결론
타입 때문에 인터페이스를 구현해야하는데 공통 메서드 때문에 추상 클래스도 확장해야 한다면, 골격 구현을 사용하자!
원문: https://dzone.com/articles/favour-skeletal-interface-in-java
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 22] 인터페이스는 타입을 정의하는 용도로만 사용하라 (0) | 2022.02.05 |
---|---|
[아이템 21] 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) | 2022.02.05 |
[아이템 20] 추상 클래스보다는 인터페이스를 우선하라 (0) | 2022.02.04 |
[아이템 19] 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라. (0) | 2022.01.25 |
[아이템 18] 상속보다는 컴포지션을 사용하라 (0) | 2022.01.25 |