오브젝트; 추상화에 의존하기

🗓️

유연한 설계

의존성과 결합도

모든 의존성이 나쁜것은 아니다.
의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서 자연스러운 것.

바람직한 의존성 = 재사용성

  • 어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 그 의존성은 바람직하지 못한 것.
  • 즉, 컨텍스트에 독립적인 의존성은 바람직하다.

의존성에 대한 정도 = 결합도 (약한 결합도, 강한 결합도)

의존성과 결합도

일반적으로 의존성과 결합도를 동의어로 사용하지만 사실 두 용어는 서로 다른 관점에서 관계의 특성을 설명하는 용어다. 의존성은 두 요소 사이의 관계 유무를 설명한다. 따라서 의존성의 관점에서는 “의존성이 존재한다” 또는 “의존성이 존재하지 않는다” 라고 표현해야 한다. 그에 반해 결합도는 두 요소 사이에 존재하는 의존성의 정도를 상대적으로 표현한다. 따라서 결합도의 관점에서는 “결합도가 강하다” 또는 “결합도가 느슨하다” 라고 표현한다.

지식이 결합을 낳는다

  • 서로에 대해 알고 있는 지식의 양이 결합도를 결정한다.
  • 더 많이 알 수록 더 많이 결합된다. 이는 더 적은 컨텍스트에서 재사용이 가능하다는 것.
  • 결합도를 느슨하게 유지하려면 협력하는 대상에 대해 더 적게 알아야 한다.
  • 결합도를 느슨하게 만드는 방법 -> 추상화

추상화에 의존하라

  • 추상화 : 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법.

의존대상을 구분하는 방법. (아래로 갈수록 결합도가 느슨해짐)

  1. 구체 클래스 의존성 Concrete class dependency
  2. 추상 클래스 의존성 Abstract class dependency
    • 여전히 상속 계층에 대해 알고 있어야함.
  3. 인터페이스 의존성 Interface dependency
    • 계층 상속을 몰라도 협력이 가능.
    • 객체가 어떤 메시지를 수신할 수 있는지에 대한 지식만을 남김.

명시적인 의존성

public class Movie {
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, Money fee) {
        // 숨겨진 의존성
        this.discountPolicy = new AmountDiscountPolicy(...); //<<

    }
}

discountPolicyAmountDiscountPolicy라는 구체에 의존하고 있다.

public class Movie {
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        // 명시적 의존성
        this.discountPolicy = discountPolicy;

    }
}

구체가 아닌 추상 또는 인터페이스에 의존하도록 변경한다.

의존성의 대상을 생성자의 인자로 전달받는 방법과 생성자 안에서 직접 생성하는 방식 사이의 가장 큰 차이점은 퍼블릭 인터페이스를 통해 할인 정책을 설정할 수 있는 방법을 제공하는지 여부다.

  • 명시적 의존성 Explicit dependency : 모든 의존성은 명시적으로 퍼블릭 인터페이스에 노출된다.
  • 숨겨진 의존성 Hidden dependency : 의존성이 퍼블릭 인터페이스에 표현되지 않는다.

의존이 명시적이지 않은 경우..

  • 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다.

의존이 명시적일때..

  • 코드를 직접 수정할 필요가 없다.
  • 실행 컨텍스트에 적절한 의존성을 선택할 수 있다.
  • 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 적절한 런타임 의존성으로 교체할 수 있다.

new는 해롭다

결합도 측면에서 new가 해로운 이유

  • new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 한다. 따라서 new를 사용하는 클라이언트는 추상화가 아닌구체 클래스에 의존할 수 밖에 없기 때문에 결합도가 높아진다.
  • new 연산자는 생성하려는 구체 클래스 뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 한다. 따라서 new를 사용하면 클라이언트가 알아야 하는 지식의 양이 늘어나기 때문에 결합도가 높아진다.

한마디로 new를 언급하는것 자체가 직접 내부를 알아하고, 변경에 취약하고 결합도가 높아진다는 이야기. 객체를 생성하는 책임을 객체 내부가 아닌 클라이언트로 옮기는 것.

표준 클래스에 대한 의존은 해롭지 않다

  • JDK에 포함된 표준 클래스 (ArrayList..)

Context 확장하기

앞선 내용의 검증

  1. 할인 혜택을 제공하지 않는 영화
  2. 다수의 할인 정책을 중복해서 적용하는 영화

할인 혜택을 제공하지 않는 영화

public class Movie {
    public Movie(String title, Duration runningTime, Money fee) {
        this(title, runningTime, fee, null);
    }

    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        //...
        this.discountPolicy = discountPolicy;
    }

    public Money calculcateMovieFee(Screening screening) {
        if (discountPolicy == null) { //<<<
            return fee;
        }

        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}
  • 예외 케이스를 처리하기 위해 Movie를 직접 수정해야 하는 문제가 있음.
public class NoneDiscountPolicy extends DiscountPolicy {
    @Override
    protected Money getDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}
  • DiscountPolicy가 협력하던 방식대로 할인 정책을 하나 더 추가한다.

다수의 할인 정책을 중복해서 적용하는 영화

public class OverlappedDiscountPolicy extends DiscountPolicy {
    private List<DiscountPolicy> discountPolicies = new ArrayList<>();

    public OverlappedDiscountPolicy(DiscountPolicy ... discountPolicies) {
        this.discountPolicies = Arrays.asList(discountPolicies);
    }

    @Override
    protected Money getDiscountAmount(Screening screening) {
        Money result = Money.ZERO;
        for(DiscountPolicy each : discountPolicies) {
            result = result.plus(each.calculateDiscountAmount(screening));
        }
        return result;
    }
}
  • OverlappedDiscountPolicyMovie에 전달하는 것만으로도 중복할인을 쉽게 다룰 수 있다.

조합 가능한 행동

어떤 객체와 협력하느냐에 따라 객체의 행동이 달라지는 것은 유연하고 재사용 가능한 설계가 가진 특징이다. 유연하고 재사용 가능한 설계는 응집도 높은 책임들을 가진 작은 객체들을 다양한 방식으로 연결함으로써 어플리케이션의 기능을 쉽게 확장할 수 있다.

  • 유연하고 재사용 가능한 설계 : 객체가 어떻게 HOW 하는지 장환하게 나열하지 않고도 객체들의 조합을 통해 무엇 WHAT 을 하는지 표현하는 클래스로 구성됨.
  • 클래스의 인스턴스를 생성하는 코드를 보는것만으로도 객체가 어떤 일을 하는지 쉽게 파악 가능
  • 코드에 드러난 로직을 해석할 필요 없이 객체가 어떤 객체와 연결됐는지를 보는 것만으로도 객체의 행동을 쉽게 예상하고 이해할 수 있다.
  • 선언적으로 객체의 행동을 정의할 수 있다