본문 바로가기

독서찰기(讀書札記)/이펙티브 자바

골격 구현(skeletal implementation) 클래스란?

골격 구현(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이 강하게 결합된다.

추상 클래스의 단점

다이아몬드 문제로 인해 다중 상속을 지원하지 않는다.

 

골격 구현(추상 인터페이스)

방법

  1. 인터페이스를 만든다.
  2. 인터페이스의 구현 및 공통 메서드의 구현을 제공하는 추상 클래스를 만든다.
  3. 구체 클래스에서도 인터페이스를 구현하게 하고, 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