[오브젝트] 합성과 유연한 설계

풀어갈 나의 이야기·2021년 12월 4일
1

JAVA

목록 보기
6/7

오브젝트 책을 스터디 하는과정에서 정리한 글입니다.

합성과 유연한 설계

  • 상속과 합성은 객체지향 프로그래밍에서 가장 널리 사용되는 코드 재사용 기법
    • 상속 관계는 is-a 관계, 합성 관계는 has-a 관계라 부른다.

상속

  • 자식클래스의 정의에 부모클래스의 이름을 덧붙이는 것만으로 부모 클래스의 코드를 재사용 할수 있음

    • 장 : 다른 부분만 추가하거나, 재정의 함으로써 기존 코드를 쉽게 확장할 수 있다.
    • 단 : 부모 클래스의 내부 구현에 대해 상세하게 알아야 하기 때문에 자식 클래스와 부모클래스 사이의 결합도가 높아질 수 있다.
      • 이는 부모 객체의 캡슐화를 약화시키는 문제를 가져옴
      • 높은 결합도는 코드 수정을 어렵게 만듬
      • 부모 클래스의 구현에 의존하고 있어, 부모 클래스 변경에 영향을 많이 받는다. 취약한 기반 클래스 문제 라고 부름
  • 이 문제를 해결하는 방법은 자식클래스와 부모클래스가 동시에 추상클래스에 의존하도록 만드는 것.

합성

  • 상속의 단점을 피하면서도 코드를 재사용 할 수 있는 더 좋은 방법
    • 구현에 의존하지 않음
    • 내부에 포함되는 객체의 구현이 아닌, public interface 에 의존 (캡슐화가 지켜짐)
    • 합성을 이용하면, 포함된 객체의 내부 구현이 변경되더라도 영향을 최소화 할 수 있으므로 안정적인 코드를 얻게 된다.
    • 컴파일 타임 관계를 런타임 관계로 변경함으로써 문제를 해결한다.
      • 상속에서의 부모, 자식 사이의 의존성은 컴파일타임에 해결됨

  • 상속보다 합성을 이용하는 것이 구현관점에서 번거롭고 복잡하지만, 설계는 변경과 관련된 것이라는 점을 기억하면 변경에 유연하게 대처할 수 있는 설계가 정답일 가능성이 높다.

  • 상속 대신 합성을 사용하면, 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경할 수 있다.

    • 다시말해 클래스 사이의 높은 결합도를 객체 사이의 낮은 결합도로 대체할 수 있다.

코드 재사용을 위해서는 객체 합성이 클래스 상속보다 더 좋은 방법이다.

상속으로 직면할 수 있는 문제

합성을 사용하면 세가지 문제점을 해결 할 수 있음

  • 불필요한 인터페이스 상속 문제 (Stack , Vector)
  • 메소드 오버라이딩 오작용 문제 (addCount , addAll)
  • 부모 클래스와 자식 클래스의 동시 수정 문제 (Playlist, PersonalPlaylist)

불필요한 인터페이스 상속 문제

  • java.util.Properties 와 java.util.Stack
    * 10장에 문제가 되었던, Hashtable 클래스와 Properties 클래스 사이의 상속관계를 합성관계로 바꿔보기
  • Properties 클래스는 키와 값의 쌍을 보관한다는 점에서 Map과 유사하지만, 오직 String 만 가질 수 있음
    • 이때 Map 의 조상인 Hashtable 을 상속받는데, 자바에 제네릭이 나오기 전이라 컴파일러가 키와 값의 타입이 String 인지 체크할 수 있는 방법이 없다.
    • 따라서 Hashtable의 인터페이스에 포함된 put 메소드를 이용하면, String 타입 이외에도 키와 값이 저장이 가능하다.
properties.put("viewrain", 12345);
assertEquals("12345", properties.getProperty("viewrain)); // 에러발생 

불필요한 인터페이스 상속문제를 바꿔보기

  • Properties 클래스에서 상속관계를 제거하고, Hashtable을 Properties의 인스턴스 변수로 포함시키면 합성관계로 변경할 수 있다.
import java.util.Hashtable;

public class Properties {
    private Hashtable<String, String> properties = new Hashtable <>();

    public String setProperty(String key, String value) {
        return properties.put(key, value);
    }

    public String getProperty(String key) {
        return properties.get(key);
    }
}
  • String 타입의 key, value 만 허용하는 규칙을 어길 위험은 사라진다.

  • Properties 는 Hashtable 의 내부 구현에 알지 못함

    • 단지 get과 set 이 포함된 퍼블릭 인터페이스를 통해서만 협력할 수 있다.
  • Stack 의 문제 (10장)

불필요한 인터페이스 상속문제를 바꿔보기

import java.util.EmptyStackException;
import java.util.Vector;

public class Stack<E> {
    private Vector<E> elements = new Vector<>();

    public E push(E item) {
        elements.addElement(item);
        return item;
    }

    public E pop() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() - 1);
    }
}
  • Stack 의 퍼블릭 인터페이스에는 불필요한 Vecotr의 오퍼레이션들이 포함되지 않음
    • 클라이언트는 더이상 임의의 위치에 요소를 추가하거나 삭제할 수 없다.
      • 마지막 위치에만 요소를 추가하거나 삭제할 수 있다는 Stack 의 규칙을 어길수 없음

메소드 오버라이딩 오작용 문제

  • InstrumentedHashSet (10장 p.327)
import java.util.Collection;
import java.util.HashSet;

public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    public InstrumentedHashSet() {
    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

//    @Override
//    public boolean addAll(Collection<? extends E> c) {
//        addCount += c.size();
//        return super.addAll(c);
//    }

    @Override // hashset 의 addAll 과 동일한 문제는 여전히 존재
    public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }

    public int getAddCount() {
        return addCount;
    }
}
  • 수정된 코드
    • Properties와 Stack을 합성으로 변경한 이유는 불필요한 오퍼레이션들이 퍼블릭 인터페이스에 스며드는 것을 방지하기 위함
    • 하지만, InstrumentedHashSet 의 경우 HashSet이 제공하는 인터페이스를 그대로 제공해야 함
      • 자바의 인터페이스를 사용하면, 이 문제를 해결 할 수 있다.
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.Spliterator;

public class InstrumentedHashSet<E> implements Set<E> {
    private int addCount = 0;
    private Set<E> set;

    public InstrumentedHashSet(Set<E> set) {
        this.set = set;
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return set.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return set.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }

    @Override public boolean remove(Object o) {
        return set.remove(o);
    }
...
}

Set의 오퍼레이션을 오버라이딩한 인스턴스 메서드에서 내부의 HashSet 인스턴스에게 동일한 메서드 호출을 그대로 전달함.

-> 포워딩 (forwarding)
동일한 메서드를 호출하기 위해 추가된 메서드 -> 포워딩 메서드(forwarding method)

  • 기존 클래스의 인터페이스를 그대로 외부에 제공하면서 구현에 대한 결합 없이 일부 작동 방식을 변경하고 싶은경우 사용.

부모 클래스와 자식 클래스의 동시 수정 문제

  • PersonalPlaylist 예시 (10장, p.330)
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Playlist {
    private List<Song> tracks = new ArrayList<>();
    private Map<String, String> singers = new HashMap<>();

    public void append(Song song) {
        tracks.add(song);
        singers.put(song.getSinger(), song.getTitle());
    }

    public List<Song> getTracks() {
        return tracks;
    }

    public Map<String, String> getSingers() {
        return singers;
    }
}
public class PersonalPlaylist extends Playlist {
    public void remove(Song song) {
        getTracks().remove(song);
        getSingers().remove(song.getSinger());
    }
}
// 안타깝게도 Playlist 의 경우에는 합성으로 변경해도, 가수별 노래 목록을 유지하기 위해 Playlist와 PersonalPlaylist를 함께 수정해야 하는 문제는 해결되지 않음 
public class PersonalPlaylist {
    private Playlist playlist = new Playlist();

    public void append(Song song) {
        playlist.append(song);
    }

    public void remove(Song song) {
        playlist.getTracks().remove(song);
        playlist.getSingers().remove(song.getSinger());
    }
}
  • But, 여전히 상속보다는 합성을 사용하는게 좋다.
    • 향수 Playlist 의 내부 구현을 변경하더라도 파급 효과를 최대한 PersonalPlaylist 내부로 캡슐화 할수 있기 때문이다.

상속으로 인한 조합의 폭발적인 증가

  • 상속으로 인해 결합도가 높아지는 경우 생기는 문제점
    • 하나의 기능을 추가하거나 수정하기 위해 불필요하게 많은 수의 클래스를 추가하거나 수정해야 함.
    • 단일 상속만 지원하는 언어에서는 상속으로 인해 오히려 중복 코드의 양이 늘어남.

기본 정책과 부가 정책 조합하기 (10장 , p.333)

  • 현재 시스템은 일반요금제와 심야 할인 요금제 두가지 종류의 요금제가 존재

    • 새로운 요구사항 : 이 두 요금제에 새로운 부가 정책을 추가하기
  • 기본정책 + 부가정책 => 핸드폰 요금제

    • 통화량을 기반으로 한 기본 정책 (일반요금제, 심야할인 요금제)
    • 선택적으로 추가할 수 있는 부가 정책 (세금정책,기본요금 할인정책)
  • 조합가능한 모든 요금 계산 순서 (p.355)

상속을 이용해 기본 정책 구현

  • 기본정책은 Phone 추상 클래스를 루트로 삼는 기존 상속계층을 이용
    • 일반요금제 : RegularPhone
    • 심야할인요금제 : NightDiscountPhone
public abstract class Phone {
    private List<Call> calls = new ArrayList<>();

    public Money calculateFee() {
        Money result = Money.ZERO;

        for(Call call : calls) {
            result = result.plus(calculateCallFee(call));
        }

        return result;
    }

    abstract protected Money calculateCallFee(Call call);
}
public class RegularPhone extends Phone {
    private Money amount;
    private Duration seconds;

    public RegularPhone(Money amount, Duration seconds) {
        this.amount = amount;
        this.seconds = seconds;
    }

    @Override
    protected Money calculateCallFee(Call call) {
        return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
    }
}
public class NightlyDiscountPhone extends Phone {
    private static final int LATE_NIGHT_HOUR = 22;

    private Money nightlyAmount;
    private Money regularAmount;
    private Duration seconds;

    public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds) {
        this.nightlyAmount = nightlyAmount;
        this.regularAmount = regularAmount;
        this.seconds = seconds;
    }

    @Override
    protected Money calculateCallFee(Call call) {
        if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
            return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
        } else {
            return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
        }
    }
}

기본 정책에 세금 정책 조합하기

  • 위 일반 요금제에 세금정책을 조합해야 한다면?
    • 간단한 방법은 RegularPhone 클래스를 상속받는 TaxableRegularPhone 클래스를 만드는것.
    • RegularPhone 의 calculateFee 메소드를 @Override 한 후 super 호출을 통해 부모클래스에게 calculateFee 메시지를 전송한다.
    • RegularPhone 의 calculateFee 는 일반요금제 규칙에 따라 계산되므로, 이 반환값에 세금을 부과해서 반환하면, 일반 요금제와 세금정책을 조합한 요금을 계산할 수 있음
public class TaxableRegularPhone extends RegularPhone {
    private double taxRate;

    public TaxableRegularPhone(Money amount, Duration seconds,
                               double taxRate) {
        super(amount, seconds);
        this.taxRate = taxRate;
    }

    @Override
    public Money calculateFee() {
        Money fee = super.calculateFee();
        return fee.plus(fee.times(taxRate));
    }
}
  • 부모 클래스의 메소드를 재사용하기 위해 super 호출을 사용하면, 원하는 결과는 쉽게 얻지만, 결합도가 높아짐

    • 결합도를 낮추는 방법은 자식 클래스가 부모 클래스의 메소드를 호출하지 않도록, 부모 클래스에 추상 메소드를 제공하는것.
  • Phone 클래스에 새로운 추상메소드인 afterCalculated 를 추가하면, 자식 클래스에게 전체요금을 계산한 후에, 수행할 로직을 추가할 수 있는 기회를 제공한다.

public abstract class Phone {
    private List<Call> calls = new ArrayList<>();

    public Money calculateFee() {
        Money result = Money.ZERO;

        for(Call call : calls) {
            result = result.plus(calculateCallFee(call));
        }

        return afterCalculated(result);
    }

    protected abstract Money calculateCallFee(Call call);
    protected abstract Money afterCalculated(Money fee);
    // 요기 이렇게 하면, RegularPhone 클래스는 요금수정이 필요없으므로, 
}

public class RegularPhone extends Phone {
    @Override
    protected Money afterCalculated(Money fee){
        return fee;
    } // 이렇게 전달받은 그대로 넘겨주면 되고, 심야할인요금제도 마찬가지이다. 
}
  • 위 코드처럼 부모클래스에 추상 메소드를 추가하면, 모든 자식 클래스에서 추사 메소드를 오버라이딩 해야하는 문제가 발생
    • 또한 모든 추상메소드의 구현이 동일하다. (중복코드)
    • 유연성을 유지하면서 중복 코드를 제거할 수 있는 방법은 Phone 에서 afterCalculated 메소드에 대한 기본 구현을 함께 제공하면 된다.
public abstract class Phone {
... 
    protected Money afterCalculated(Money money) {
        return fee; // hook Method 라고 부름 
    }
...
}
  • TaxableRegularPhone 은 RegularPhone 이 계산한 요금에 세금을 부과하는 클래스이다.
    • afterCalculated 메소드를 오버라이딩 한후 fee에 세금을 더해 반환하도록 구현한다.
public class TaxableRegularPhone extends RegularPhone {
    private double taxRate;

    public TaxableRegularPhone(Money amount, Duration seconds,
                               double taxRate) {
        super(amount, seconds);
        this.taxRate = taxRate;
    }

    @Override
    public Money calculateFee() {
        Money fee = super.calculateFee();
        return fee.plus(fee.times(taxRate)); // 요기, 심야도 마찬가지로 해준다. 
    }
}
  • 일반요금을 쓰고싶으면 RegularPhone 인스턴스를 생성하고, 조합하고 싶다면 TaxableRegularPhone 인스턴스를 생성하면 된다. (p.362 상속계층 그림 보기)
  • 문제는 taxableRegularPhone 과 TaxableNightDiscountPhone 의 코드가 대부분 중복이라는 것..

기본 정책에 기본요금 할인 정책 조합하기

일반요금제와 기본요금 할인 정책을 조합

  • RegularPhone 을 상속받는 RateDiscountableRecularPhone 클래스를 추가

심야할인 요금제와 기본요금 할인정책을 조합

  • NightlyDiscountPhone 을 상속받는 RateDiscountableNightlyDiscountPhone 클래스 추가

But 중복코드가 있다.

중복코드의 덫에 걸리다

  • 부가정책은 자유롭게 조합할 수 있어야 하고, 적용되는 순서 역시 임의로 결정할 수 있어야 함 (p.355 그림 11.2 다시한번 봐보기)

  • 위에서 제시한 상속을 이용한 해결방법은 모든 가능한 조합별로 자식 클래스를 하나씩 추가하는 것이다.

    • 만약 일반요금제 + 세금정책 + 기본 할인요금정책을 만들고 싶다면..
    • TaxableRegularPhone 을 상속받는 새로운 자식클래스인 TaxableAndRateDiscountableRegularPhone 를 추가하면 된다.
public class TaxableAndRateDiscountableRegularPhone extends TaxableRegularPhone {
    private Money discountAmount;

    public TaxableAndRateDiscountableRegularPhone(Money amount, Duration seconds, double taxRate, Money discountAmount) {
        super(amount, seconds, taxRate);
        this.discountAmount = discountAmount;
    }

    @Override
    protected Money afterCalculated(Money fee) {
        return super.afterCalculated(fee).minus(discountAmount);
    }
}
  • 기본요금제 + 기본요금 할인정책 적용 후 + 세금을 나중에 부과하고 싶다?
public class RateDiscountableAndTaxableRegularPhone
        extends RateDiscountableRegularPhone {
    private double taxRate;

    public RateDiscountableAndTaxableRegularPhone(Money amount, Duration seconds, Money discountAmount, double taxRate) {
        super(amount, seconds, discountAmount);
        this.taxRate = taxRate;
    }

    @Override
    protected Money afterCalculated(Money fee) {
        return super.afterCalculated(fee).plus(fee.times(taxRate));
    }
}
  • 이렇게 똥코드와 똥설계, 똥 아키텍처를 봐보자 (p.366 그림 11.5)
    복잡하지만, 그보다 더 큰 문제는 새로운 정책을 추가하기가 어렵다는 것...
    새로운 정책을 추가한다면.. 불필요하게 많은 수의 클래스를 상속 계층안에 추가해야 한다. (p.367 그림 11.6 만약 고정요금제를 추가한다면..?)

    이를 클래스 폭팔 (class explosion) 또는 조합의 폭발 (combinational explosion) 이라 부른다.

  • 이 현상의 근본적인 문제는 자식 클래스가 부모 클래스의 구현에 강하게 결합되도록 강요하는 상속의 근본적인 한계 때문에 발생한다.

합성관계로 변경하기

  • 위에 설명했듯.. 상속관계는 컴파일 타임에 결정되고 고정되기 때문에 코드를 실행하는 도중에는 변경 할 수 없다.
  • 합성은 컴파일타임 관계를 런타임 관계로 변경 함으로써, 이 문제를 해결한다.
    • 합성을 사용하면 구현이 아닌 퍼블릭 인터페이스에 대해서만 의존하므로, 런타임에 객체의 관계를 변경할 수 있다.
      • 실행시점에 정책들의 관계를 유연하게 변경, 실행시점에 인스턴스를 조립하는 방법

기본 정책 합성하기

  • 가장먼저 할일은 각 정책을 별도의 클래스로 구현하자.
  • 분리된 정책들을 연결할 수 있도록, 합성 관계를 이용해 구조를 개선 (p.355 그림 11.2 와 같이 실행 시점에 정책들을 조합할 수 있게 됨 )

step1: 기본정책과 부가정책을 포괄하는 RatePolicy 인터페이스 추가

public interface RatePolicy {
    Money calculateFee(Phone phone);
}

step2: 기본정책 구현 (단. 일반요금, 심야할인요금은 요금계산방식을 제외한 로직이 동일하므로, 추상클래스 추가하기)

// 중복코드를 담을 추상 클래스
public abstract class BasicRatePolicy implements RatePolicy {
    @Override
    public Money calculateFee(Phone phone) {
        Money result = Money.ZERO;

        for(Call call : phone.getCalls()) {
            result.plus(calculateCallFee(call));
        }

        return result;
    }
    protected abstract Money calculateCallFee(Call call);
}

step3: 일반요금제 구현 (BasicRatePolicy 의 자식클래스로 RegularPolicy 를 추가) // 심야도 마찬가지로..

public class RegularPolicy extends BasicRatePolicy {
    private Money amount;
    private Duration seconds;

    public RegularPolicy(Money amount, Duration seconds) {
        this.amount = amount;
        this.seconds = seconds;
    }

    @Override
    protected Money calculateCallFee(Call call) {
        return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
    }
}

step4: 기본 정책을 이용해 요금을 계산할 수 있도록 Phone 수정

  • Phone 클래스 안에 RatePolicy 참조자 포함되어 합성
public class Phone {
    private RatePolicy ratePolicy; // 요기 주목.. 이게 합성
    private List<Call> calls = new ArrayList<>();

    public Phone(RatePolicy ratePolicy) {
        this.ratePolicy = ratePolicy;
    }

    public List<Call> getCalls() {
        return Collections.unmodifiableList(calls);
    }

    public Money calculateFee() {
        return ratePolicy.calculateFee(this);
    }
}
  • Phone 이 다양한 요금정책과 협력할 수 있어야 하므로, 요금 정책의 타입이 RatePolicy 라는 인터페이스로 정의되 있는걸 주목해야 한다.

    • Phone은 컴파일타임 의존성을 구체적인 런타임 의존성으로 대체하기 위해 생성자를 통해 RatePolicy 의 인스턴스에 대한 의존성을 주입받는다.
  • 의존성 주입을 사용해 런타임에 필요한 객체를 설정하는 것이 일반적임.

  • p.372 그림 11.7 합성 관계를 사용한 기본 정책의 전체적인 구조 봐보기

  • 일반요금제의 경우 합성

Phone phone = new Phone(new RegularPolicy(Money.wons(10), Duration.ofSeconds(10)));
  • 심야할인 요금제의 경우 합성
Phone phone = new Phone(new NightlyDiscountPolicy(Money.wons(5), Money.wons(10) , Duration.ofSeconds(10)));

-> 이제 필요에 따라 적절한 인스턴스를 생성하기만 하면 된다.

부가정책 적용하기

  • 조건 : 기본 정책에 대한 계산이 끝난 후 적용되어야 함
  • 두가지 제약에 따라 부가정책 구현
    • 부가정책은 기본정책이나 다른 부가정책의 인스턴스를 참조할 수 있어야 함 : 부가정책의 인스턴스는 어떤 종류의 정책과도 합성될 수 있어야 함.
    • Phone 의 입장에서는 자신이 기본 정책의 인스턴스에게 메시지를 전송하는지, 부가 정책의 인스턴스에게 메시지를 전송하는지 몰라야 한다.
      • 기본정책과 부가정책은 협력 안에서 동일한 '역할'을 수행 : 부가정책이 기본정책과 동일한 RatePolicy 인터페이스를 구현해야 함.

step1: 부가 정책을 AdditionalRatePolicy 추상 클래스로 구현

public abstract class AdditionalRatePolicy implements RatePolicy { // RatePolicy 의 역할을 수행하므로, RatePolicy 인터페이스를 구현함 
    private RatePolicy next;

// 컴파일 타임 의존성을 쉽게 대체할 수 있도록, RatePolicy 타입의 인스턴스를 인자로 받는 생성자를 제공
    public AdditionalRatePolicy(RatePolicy next) { 
        this.next = next;  // next에 전달된 인스턴스에 대한 의존성 주입 
    }
    
    @Override
    public Money calculateFee(Phone phone) {
        // next가 참고하고 있는 인스턴스에게 calculateFee 메시지를 전송
        Money fee = next.calculateFee(phone); 
        return afterCalculated(fee) ;
    }
    
    abstract protected Money afterCalculated(Money fee);
}

step2: 세금정책 구현

public class TaxablePolicy extends AdditionalRatePolicy {
    private double taxRatio;

    public TaxablePolicy(double taxRatio, RatePolicy next) {
        super(next);
        this.taxRatio = taxRatio;
    }

    @Override
    protected Money afterCalculated(Money fee) {
        return fee.plus(fee.times(taxRatio));
    }
}
  • 기본 요금 할인 정책을 추가하는 것도 간단함
public class RateDiscountablePolicy extends AdditionalRatePolicy {
    private Money discountAmount;

    public RateDiscountablePolicy(Money discountAmount, RatePolicy next) {
        super(next);
        this.discountAmount = discountAmount;
    }

    @Override
    protected Money afterCalculated(Money fee) {
        return fee.minus(discountAmount);
    }
}
  • p.375 그림 11.11 기본 정책과 부가 정책을 조합할 수 있는 상속구조

기본정책과 부가정책 합성하기

  • 이제 원하는 정책의 인스텀스를 생성한 후 의존성주입을 통해 다른 정책의 인스턴스에 전달하기만 하면 된다.
  • 일반요금제 + 세금정책 조합
Phone phone = new Phone(new TaxablePolicy(0.05, new RegularPolicy());
  • 일반 요금제에 기본요금 할인정책을 조합한 결과에 세금정책을 조합하고 싶다면?
Phone phone = new Phone( new TaxablePolicy(0.05, new RateDiscountablePolicy(Money.wons(1000),RegularPolicy(...))));
  • 상속을 통한 설계보다 복잡하고, 정해진 규칙에 따라 객체를 생성하고 조합해야 하기 때문에 코드의 이해는 어려울 수 있으나, 설계에 익숙해지면 객체를 조합하는 방식이 상속을 사용한 방식보다 예측 가능하고 일관성이 있다는 사실..

새로운 정책 추가하기

  • 합성은 이 문제를 간단히 해결할 수 있다.
    • 고정 요금제가 필요하다면? p.377 그림 11.12 처럼 클래스 하나만 추가하면 된다.

객체 합성이 클래스 상속보다 더 좋은 방법이다.

  • 코드를 재사용하기 위해 널리 사용되는 방법은 상속이다.

    • 우아한 해결책은 아님
    • 강한 결합으로 인한 코드의 진화를 방해한다.
  • 건전한 결합도를 유지하는 방법은 합성이다.

    • 상속은 구현을 재사용하지만, 합성은 객체의 인터페이스를 재사용한다.
  • 그럼 상속을 사용하지마?

    • 구현 상속과 인터페이스 상속의 두가지가 나누어 짐을 이해하자.
    • 이번장은 구현상속의 단점을 극단적으로 표현한것일뿐..

믹스인

  • 구체적인 코드를 재사용하면서도 낮은 결합도를 유지할 수 있는 유일한 방법은 재사용에 적합한 추상화를 도입하는것
  • Flavors 언어에서 처음 도입. CLOS(Common Lisp Object System)에 의해 대중화

믹스인 (mixin)

  • 믹스인은 객체를 생성 할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법
    • 합성이 실행 시점에 객체를 조합해 재사용하는 방법이라면, 믹스인은 컴파일 시점에 필요한 코드 조각을 조합해 재사용 하는 방법

믹스인 == 상속?

  • No.. 상속의 진정한 목적은 부모 자식간의 동일한 개념적인 범주 (is-a 관계)
  • 믹스인은 코드를 다른 코드안에 섞어 넣기 위한 방법

기본정책 구현하기

  • 스칼라에서 제공하는 트레이트(trait)를 이용해 믹스인을 구현해보자

    • 트레이트는 믹스인의 기본철학을 가장 유사한 형태로 재현.
  • 기본정책을 추상클래스로.

    • 표준 요금제를 구현하는 정책은 기본정책을 상속받아 개별 Call의 요금을 계산하는 calculateCallFee메서드를 오버라이딩.
    • 심야할인정책도 같은 방식으로.
abstract class BasicRatePolicy {
    def calculateFee(phone : Phone): Money =
        phone.calls.map(calculateCallFee(_)).reduce(_+_)
        
    protected def calculateCallFee(call: Call): Money;
}

class RegularPolicy(val amount: Money, val seconds: Duration) extends BasicRatePolicy {
    override protected def calculateCallFee(call: Call): Money = 
        amount*(call.duration.getSeconds / seconds.getSeconds)
}

class NightlyDiscountPolicy (
    val nightlyAmount: Money,
    val regularAmount: Money,
    val seconds: Duration) extends BasicRatePolicy {
    
    override protected def calculateCallFee(call: Call): Money = 
    if(call.from.getHour >= NightlyDiscountPolicy.LateNightHour){
        nightlyAmount*(call.duration.getSeconds / seconds.gerSeconds)
    }else{
        regularAmount*(call.duration.getSeconds / seconds.getSeconds)
    }
}

object NightlyDiscountPolicy{
    val LateNightHour: Integer = 22
}

트레이트로 부가 정책 구현하기

  • 기본정책에 조합하려는 코드는 부가 정책(세금정책)을 구현하는 코드들
// TaxablePolicy 트레이트가 BasicRatePolicy 를 확장한다는 점에 주목
trait TaxablePolicy extends BasicRatePolicy {
   def texRate: Double
   
   override def calculateFee(phone: Phone): Money = {
       val fee = super.calculateFee(phone)
       return fee + fee * taxRate
   }
}
  • 위 코드에서 extends 는 단지 TaxablePolicy가 사용될 수 있는 문맥을 제한할 뿐
    TaxablePolicy는 BasicRatePolicy를 상속받은 경우에만 믹스인 될 수 있다.
    * 상속의 개념이 아니라, TaxablePolicy가 BasicRatePolicy 나 BasicRatePolicy 의 자손에 해당하는 경우에만 믹스인 될수 있다는 것을 의미

  • RegularPolicy와 NightlyDiscountPolicy 에 믹스인 될수 있으며 심지어 미래에 추가될 새로운 BasicRatePolicy의 자손에게도 믹스인 될 수 있지만, 다른 클래스나 트레이트에는 믹스인 될 수 없다.

부가정책 트레이트 믹스인 하기

  • 스칼라는 트레이트를 클래스나 다른 트레이트에 믹스인 할 수 있도록 extends, with 키워드 제공
    • 믹스인 하려는 대상 클래스의 부모 클래스가 존재하는 경우 부모클래스는 extends 를 이용해 상속받고 트레이트는 with 를 이용해 믹스인
class TaxableRegularPolicy {
    amount: Money,
    seconds: Duration,
    val taxRate: Double)
    extends RegularPolicy(amount, seconds)
    with TaxablePolicy
}
  • 믹스인되기 전까지 상속계층 안에서 TaxablePolicy 트레이트의 위치가 결정되지 않음.
    어떤 클래스에 믹스인 할지에 따라 트레이트의 위치는 동적으로 변경
    * 믹스인에서는 클래스가 늘어나더라도 중복코드 문제가 발생하지 않음.

쌓을 수 있는 변경

  • 전통적인 믹스인은 특정한 클래스의 메소드를 재사용하고 기능을 확장하기 위해 사용해 왔다.
    • 핸드폰 과금 시스템의 경우, BasicRatePolicy 의 cacluateFee 메소드의 기능을 확장하기 위해 믹스인을 사용했다.
  • 믹스인은 상속 계층 안에서 확장한 클래스보다 더 하위에 위치하게 됨
    • 대상클래스의 자식클래스처럼 사용될 용도로 만들어지는 것
    • aka 추상 서브클래스 abstract subclass (믹스인의 또다른 불림)
  • 특정한 클래스에 대한 변경 또는 확장을 독립적으로 구현한 후 필요한 시점에 차례대로 추가 가능.
    • -> 쌓을수 있는 변경 stackable modification
profile
깨끗한 스케치북 일수록 우아한 그림이 그려지법, 읽기 쉽고, 짧은 코드가 더 아름다운 법.. 또한 프로그래머의 개발은 구현할 프로그래밍이 아닌, 풀어갈 이야기로 써내려가는것.

0개의 댓글