본문 바로가기
Study/오브젝트

[오브젝트] 10장. 상속과 코드 재사용

by Nahwasa 2023. 1. 3.

스터디 메인 페이지

목차

    - ☆ 표시가 붙은 부분은 스터디 중 나온 얘기 혹은 제 개인적인 생각이나 제가 이해한 방식을 적어놓은 것으로, 책에 나오지 않는 내용입니다. 따라서 책에서 말하고자 하는 바와 다를 수 있습니다.

    - 모든 이미지의 출처는 오브젝트(조영호 저) 책 입니다.

     


     

    CHAPTER 10. 상속과 코드 재사용                                                       

    ⚈ 코드 재사용

    • 전통적인 패러다임 : 코드를 복사한 후 수정
    • 객체지향 : 코드를 재사용하기 위해 '새로운' 코드를 추가. 객체지향에서 클래스를 재사용하는 전통적인 방법은 새로운 클래스를 추가하는 것.

     

    ⚈ 이번 장에서는 상속을 통한 코드 재사용을 알아보고, 11장에서 코드를 효과적으로 재사용할 수 있는 합성을 알아본 후 상속과 합성의 장단점을 비교하게 된다.

     


    01 상속과 중복 코드

    ⚈ 중복 코드는 사람들의 마음속에 의심과 불신의 씨앗을 뿌린다.

    • ☆ ㄹㅇㅋㅋ 단순 복붙해둔 중복코드 보면 다른 부분은 보고싶지도 않음.

     

    ⚈ 중복 여부를 판단하는 기준은 변경이다.

    • 요구사항이 변경됐을 때 두 코드를 함께 수정해야 한다면 이 코드는 중복이다.
    • ☆ 똑같이 생긴걸 복붙한걸 중복 코드라고 생각했는데, 이 책에서 말하는 중복의 기준을 보니 확실히 책의 말이 맞는 것 같다. 동일한 이유로 수정이 되는 부분이라면 중복이 맞지!
    • DRY 원칙 : Don't Repeat Yourself - 동일한 지식을 중복하지 말라. 모든 지식은 시스템 내에서 단일하고, 애매하지 않고, 정말로 믿을 만한 표현 양식을 가져야 한다.

     

    ⚈ 중복 코드는 서로 다르게 수정하기가 쉽다

    • 많은 코드 더미 속에서 어떤 코드가 중복인지를 파악하는 일은 쉬운 일이 아니다.
    • 중복 코드는 항상 함께 수정돼야 한다.
    • 중복 코드는 새로운 중복 코드를 부른다.
    • 중복 코드를 제거하지 않은 상태에서 코드를 수정할 수 있는 유일한 방법은 새로운 중복 코드를 추가하는 것뿐이다.
    • ☆ 변경되는 부분과 안되는 부분을 잘 파악해서 디자인 패턴을 적용하자!

     

    ⚈ 타입 코드 사용하기

    • 두 클래스 사이의 중복 코드를 제거하는 한 가지 방법은 클래스를 하나로 합치는 것이다.
    • 타입 코드를 추가하고 타입 코드의 값에 따라 로직을 분기시키는 방식
    • 낮은 응집도와 높은 결합도라는 문제에 시달리게 된다.
    public Money calculateFee() {
        Money result = Money.ZERO;
    
        for(Call call : calls) {
            if (type == PhoneType.REGULAR) {
                result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
            } else {
                if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                    result = result.plus(nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
                } else {
                    result = result.plus(regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
                }
            }
        }
    
        return result;
    }

     

    ⚈ 상속 사용하기

    • 상속은 타입 코드를 사용하지 않고도 중복 코드를 관리할 수 있는 효과적인 방법을 제공한다.
    • 이미 존재하는 클래스와 유사한 클래스가 필요하다면 코드를 복사하지 말고 상속을 이용해 코드를 재사용하자 -> ☆ 바로 단점이 나오긴 하지만, 어쨌든 코드 복붙보단 낫다!
    public class NightlyDiscountPhone extends Phone {
        private static final int LATE_NIGHT_HOUR = 22;
    
        private Money nightlyAmount;
    
        public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds) {
            super(regularAmount, seconds);
            this.nightlyAmount = nightlyAmount;
        }
    
        @Override
        public Money calculateFee() {
            // 부모클래스의 calculateFee 호출
            Money result = super.calculateFee();
    
            Money nightlyFee = Money.ZERO;
            for(Call call : getCalls()) {
                if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                    nightlyFee = nightlyFee.plus(
                        getAmount().minus(nightlyAmount).times(
                            call.getDuration().getSeconds() / getSeconds().getSeconds()));
                }
            }
    
            return result.minus(nightlyFee);
        }
    }

     

    ⚈ 위 코드로 알 수 있는 상속을 통한 코드 재사용의 문제점

    • 위 코드의 부모 클래스인 Phone의 calculateFee()는 아래와 같다.
    public Money calculateFee() {
        Money result = Money.ZERO;
    
        for(Call call : calls) {
            result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
        }
    
        return result;
    }

     

    • 저걸 재사용하기 위해 상속을 통해 짰더니 minus를 통한 계산 식이 복잡하게 짜여졌다. -> 개발자의 가정을 이해하기 전에는 코드를 이해하기 어렵다.
    • 상속을 염두에 두고 설계되지 않은 클래스를 상속을 이용해 재사용하는 것은 생각처럼 쉽지 않다. 또한 직관에도 어긋날 수 있다.
    • 상속을 이용해 코드를 재사용하기 위해서는 부모 클래스의 개발자가 세웠던 가정이나 추론 과정을 정확하게 이해해야 한다.
    • 따라서 상속은 결합도를 높인다. 상속이 초래하는 부모 클래스와 자식 클래스 사이의 강한 결합이 코드를 수정하기 어렵게 만든다.

     

    ⚈  자식 클래스의 메서드 안에서 super 참조를 이용해 부모 클래스의 메서드를 직접 호출할 경우 두 클래스는 강하게 결합된다. super 호출을 제거할 수 있는 방법을 찾아 결합도를 제거하라.

    • 위 코드에 추가로 세금을 부과하는 요구사항이 추가되었다고 해보자. 그럼 아래와 같이 변경될 것이다.
    // Phone에 추가된 세금 요구사항 로직(taxRate)
    public Money calculateFee() {
        Money result = Money.ZERO;
    
        for(Call call : calls) {
            result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
        }
    
        return result.plus(result.times(taxRate));
    }
    
    // NightlyDiscountPhone에 추가된 세금 요구사항 로직(taxRate)
    @Override
    public Money calculateFee() {
        // 부모클래스의 calculateFee() 호출
        Money result = super.calculateFee();
    
        Money nightlyFee = Money.ZERO;
        for(Call call : getCalls()) {
            if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                nightlyFee = nightlyFee.plus(
                    getAmount().minus(nightlyAmount).times(
                        call.getDuration().getSeconds() / getSeconds().getSeconds()));
            }
        }
    
        return result.minus(nightlyFee.plus(nightlyFee.times(getTaxRate())));
    }

     

    • Phone의 코드를 재사용하고 중복 코드를 제거하기 위해 Phone의 자식 클래스로 NightlyDiscountPhone을 만든건데, 세금을 부과하는 로직을 추가하기 위해 Phone을 수정할 때 유사한 코드를 NightlyDiscountPhone에도 추가해야 했다. 즉 새로운 중복 코드를 만들었다.
    • 이것은 NightlyDiscountPhone이 Phone의 구현에 너무 강하게 결합돼 있기 때문에 발생하는 문제다.
    • ☆ 부모클래스를 알고 있어야 하는 것도 캡슐화를 어기는 것이다. 부모가 바뀌면 자식도 바뀌어야 하니

     


    02 취약한 기반 클래스 문제

    ⚈ 위에서 본 것 처럼 상속 관계로 연결된 자식 클래스가 부모 클래스의 변경에 취약해지는 현상을 가리켜 취약한 기반 클래스 문제(Fragile Base Class Problem, Brittle Base Class Problem)라고 한다.

    • 상속을 사용한다면 피할 수 없는 객체지향 프로그래밍의 근본적인 취약성이다.
    • 상속이라는 문맥 안에서 결합도가 초래하는 문제점을 가리키는 용어다. 상속 관계를 추가할수록 전체 시스템의 결합도가 높아진다는 사실을 알고 있어야 한다.
    • 객체를 사용하는 이유는 구현과 관련된 세부사항을 퍼블릭 인터페이스 뒤로 캡슐화할 수 있기 때문이다. 캡슐화는 변경에 의한 파급효과를 제어할 수 있기 때문에 가치있다.
    • 안타깝게도 상속을 사용하면 부모 클래스의 퍼블릭 인터페이스가 아닌 구현을 변경하더라도 자식 클래스가 영향을 받기 쉬워진다. 상속은 코드의 재사용을 위해 캡슐화의 장점을 희석시키고 구현에 대한 결합도를 높임으로써 객체지향이 가진 강력함을 반감시킨다.

     

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

    • 자바의 Stack은 Vector를 상속받는다. Vector의 퍼블릭 인터페이스를 이용하면 임의의 위치에서 요소를 추가하거나 삭제할 수 있다. 따라서 Stack의 규칙을 쉽게 위반할 수 있다.
    • 아래와 같은 코드는 스택 구조의 규칙에 따르면 3이 출력되야 하지만, 실제론 2가 출력된다.
    Stack<Integer> stack = new Stack<>();
    stack.push(0);
    stack.push(1);
    stack.push(2);
    stack.add(0, 3);
    System.out.println(stack.pop());

     

    • 상속받은 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨트릴 수 있다.

     

    ⚈ 메서드 오버라이딩의 오작용 문제

    • 이하의 코드는 set에 요소가 추가된 횟수를 기록하기 위해 HashSet을 상속해 작성된 코드이다.
    • addAll에 3개의 요소를 집어넣을 경우 예상은 addCount가 3이 되는 것이지만, 실제론 6이 된다. 부모 클래스인 HashSet의 addAll에서 add를 호출하기 때문이다.
    public class InstrumentedHashSet<E> extends HashSet<E> {
    	private int addCount = 0;
        
        @Override
        public boolean add(E e) {
        	addCount++;
            return super.add(e);
        }
        
        @Override
        public boolean addAll(Collection<? extends E> c) {
        	addCouhnt += c.size();
            return super.addAll(c);
        }
    }

     

    • 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 경우 부모 클래스가 자신의 메서드를 사용하는 방법에 자식 클래스가 결합될 수 있다.
    • 조슈아 블로치 : 클래스가 상속되기를 원한다면 상속을 위해 클래스를 설계하고 문서화해야 하며, 그렇지 않은 경우에는 상속을 금지시켜야 한다.
    • 객체지향의 핵심이 구현을 캡슐화하는 것인데도 이렇게 내부 구현을 공개하고 문서화하는 것이 옳은가?
    • 설계는 트레이드오프 활동이다.
    • 상속은 코드 재사용을 위해 캡슐화를 희생한다. 완벽한 캡슐화를 원한다면 코드 재사용을 포기하거나 상속 이외의 다른 방법을 사용해야 한다.

     

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

    • 음악 목록을 추가할 수 있는 플레이리스트를 구현한다고 가정하자.음악 정보를 저장할 Song 클래스와 음악 목록을 저장할 Playlist는 아래와 같다.
    public class Song {
        private String singer;
        private String title;
    
        public Song(String singer, String title) {
            this.singer = singer;
            this.title = title;
        }
    
        public String getSinger() {
            return singer;
        }
    
        public String getTitle() {
            return title;
        }
    }
    
    ----
    
    public class Playlist {
        private List<Song> tracks = new ArrayList<>();
    
        public void append(Song song) {
            getTracks().add(song);
        }
    
        public List<Song> getTracks() {
            return tracks;
        }
    }

     

    • 이제 노래를 삭제할 수 있는 기능이 추가된 PersonalPlaylist가 필요하다고 해보자. 상속을 통해 Playlist의 코드를 재사용하는게 가장 빠른 방법일 것이다.
    public class PersonalPlaylist extends Playlist {
        public void remove(Song song) {
            getTracks().remove(song);
        }
    }

     

    • 문제는 요구사항이 변경돼서 Playlist에서 노래의 목록뿐만 아니라 가수별 노래의 제목도 함께 관리해야 한다고 가정하자. 그럼 PlayList와 PersonalPlaylist는 아래와 같이 변경될 것이다. 
    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());
        }
    }

     

    • 위의 예를 통해 자식 클래스가 부모 클래스의 메서드를 오버라이딩하거나 불필요한 인터페이스를 상속받지 않았음에도 부모 클래스를 수정할 때 자식 클래스를 함께 수정해야 할 수도 있다는 사실을 알 수 있다.
    • 코드 재사용을 위한 상속은 부모 클래스와 자식 클래스를 강하게 결합시키기 때문에 함께 수정해야 하는 상황 역시 빈번하게 발생할 수박에 없다.
    • 클래스를 상속하면 결합도로 인해 자식 클래스와 부모 클래스의 구현을 영원히 변경하지 않거나, 자식 클래스와 부모 클래스를 동시에 변경하거나 둘 중 하나를 선택할 수밖에 없다.

     


    03 Phone 다시 살펴보기

    ⚈ 상속으로 인한 피해를 최소화할 수 있는 방법을 찾아보자. 취약한 기반 클래스 문제를 완전히 없앨 수는 없지만 어느 정도까지 위험을 완화시키는 것은 가능하다.

     

    ⚈ 열쇠는 추상화다. 추상화에 의존하자.

    • 부모 클래스와 자식 클래스 모두 추상화에 의존하도록 해야 한다.

     

    ⚈ 코드 중복을 제거하기 위해 상속을 도입할 때 따르는 두 가지 원칙

    • 두 메서드가 유사하게 보인다면 차이점을 메서드로 추출하라.
    • 부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올려라.

     

    ⚈ 기존 Phone, NightlyDiscountPhone

    public class Phone {
        private Money amount;
        private Duration seconds;
        private List<Call> calls = new ArrayList<>();
        private double taxRate;
    
        public Phone(Money amount, Duration seconds, double taxRate) {
            this.amount = amount;
            this.seconds = seconds;
            this.taxRate = taxRate;
        }
    
        public void call(Call call) {
            calls.add(call);
        }
    
        public List<Call> getCalls() {
            return calls;
        }
    
        public Money getAmount() {
            return amount;
        }
    
        public Duration getSeconds() {
            return seconds;
        }
    
        public Money calculateFee() {
            Money result = Money.ZERO;
    
            for(Call call : calls) {
                result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
            }
    
            return result.plus(result.times(taxRate));
        }
    }
    public class NightlyDiscountPhone {
        private static final int LATE_NIGHT_HOUR = 22;
    
        private Money nightlyAmount;
        private Money regularAmount;
        private Duration seconds;
        private List<Call> calls = new ArrayList<>();
        private double taxRate;
    
        public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds, double taxRate) {
            this.nightlyAmount = nightlyAmount;
            this.regularAmount = regularAmount;
            this.seconds = seconds;
            this.taxRate = taxRate;
        }
    
        public Money calculateFee() {
            Money result = Money.ZERO;
    
            for(Call call : calls) {
                if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                    result = result.plus(nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
                } else {
                    result = result.plus(regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
                }
            }
    
            return result.minus(result.times(taxRate));
        }
    }

     

    ⚈ 차이를 메서드로 추출 후 중복 코드를 부모 클래스로 올린 코드

    public abstract class AbstractPhone {
        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 Phone extends AbstractPhone {
        private Money amount;
        private Duration seconds;
    
        public Phone(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 AbstractPhone {
        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());
            }
        }
    }

     

    ⚈ 자식 클래스들 사이의 공통점을 부모 클래스로 옮김으로써 실제 코드를 기반으로 상속 계층을 구성할 수 있다. 이제 우리의 설계는 추상화에 의존하게 된다.

    • '위로 올리기' 전략은 실패했더라도 수정하기 쉬운 문제를 발생시킨다.
    • 위로 올리기에서 실수하더라도 추상화할 코드는 눈에 띄고 결국 상위 클래스로 올려지면서 코드의 품질은 높아진다.

     

    ⚈ 추상화가 핵심이다.

    • 위처럼 공통 코드를 이동시킨 후에 각 클래스는 서로 다른 변경의 이유를 가진다. (☆ 즉, 중복이 없어졌다.) 
    • AbstractPhone은 전체 통화 목록을 계산하는 방법이 바뀔 경우에만 변경된다.
    • Phone은 일반 요금제의 통화 한 건을 계산하는 방식이 바뀔 경우에만 변경된다.
    • NightlyDiscountPhone은 심야 할인 요금제의 통화 한 건을 계산하는 방식이 바뀔 경우에만 변경된다.
    • 세 클래스는 각각 하나의 변경 이유만을 가진다 -> 단일 책임 원칙을 준수하기 때문에 응집도가 높다.
    • 또한 caculateCallFee 메서드의 시그니처가 변경되지 않는한 부모 클래스의 내부 구현이 변경되더라도 자식 클래스는 영향을 받지 않는다 -> 낮은 결합도를 유지하고 있다.
    • 새로운 요금제를 추가하기도 쉽다. 새로운 요금제가 필요하다면 AbstractPhone을 상속받는 새로운 클래스를 추가한 후 caculateCallFee 메서드만 오버라이딩하면 된다. -> OCP (개방-폐쇄 원칙) 역시 준수한다.
    • 위의 모든 장점은 클래스들이 추상화에 의존하기 때문에 얻어지는 장점이다.

     

    ⚈ 의도를 드러내는 이름 선택하기

    • 기존 Phone은 '일반 요금제'와 관련된 내용을 구현한다는 사실을 명시적으로 전달하지 못한다. 또한 AbstractPhone은 전화기를 포괄한다는 의미를 명확하게 전달하지 못한다. 따라서 이하와 같이 변경하는 것이 적절할 것이다.
    • AbstractPhone -> Phone
    • Phone -> RegularPhone
    • NighlyDiscountPhone -> 그대로

     

    ⚈ 이번에 '01'에서 봤던 것 처럼 세금 요구사항을 추가하는건 더 쉬워졌을까?

    public abstract class Phone {
        private double taxRate;
        private List<Call> calls = new ArrayList<>();
    
        public Phone(double taxRate) {
            this.taxRate = taxRate;
        }
    
        public Money calculateFee() {
            Money result = Money.ZERO;
    
            for(Call call : calls) {
                result = result.plus(calculateCallFee(call));
            }
    
            return result.plus(result.times(taxRate));
        }
    
        protected abstract Money calculateCallFee(Call call);
    }
    public class RegularPhone extends Phone {
        private Money amount;
        private Duration seconds;
    
        public RegularPhone(Money amount, Duration seconds, double taxRate) {
            super(taxRate);
            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, double taxRate) {
            super(taxRate);
            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());
            }
        }
    }
    • 책임을 아무리 잘 분리하더라도 인스턴스 변수의 추가는 종종 상속 계층 전반에 걸친 변경을 유발한다.
    • 하지만 인스턴스 초기화 로직을 변경하는 것이 두 클래스에 동일한 세금 계산 코드를 중복시키는 것보다는 현명한 선택이다.
    • 객체 생성 로직의 변경에 유연하게 대응할 수 있는 다양한 방법이 존재한다 (8장, 9장 내용이다.)  따라서 객체 생성 로직에 대한 변경을 막기보다는 핵심 로직의 중복을 막아라. 핵심 로직은 한 곳에 모아 놓고 조심스럽게 캡슐화해야 한다. 그리고 공통적인 핵심 로직은 최대한 추상화해야 한다.
    • 상속으로 인한 클래스 사이의 결합을 피할 수 있는 방법은 없다. 상속은 어떤 방식으로든 부모 클래스와 자식 클래스를 결합시킨다.

     


    04 차이에 의한 프로그래밍

    ⚈ 기존 코드와 다른 부분만을 추가함으로써 애플리케이션의 기능을 확장하는 방법을 차이에 의한 프로그래밍(programming by difference)이라고 부른다.

    • 차이에 의한 프로그래밍의 목표는 중복 코드를 제거하고 코드를 재사용하는 것이다.

     

    ⚈ 중복 코드는 악의 근원이다.

    • 코드를 재사용하기 위해서는 중복 코드를 제거해서 하나의 모듈로 모아야 한다.
    • 중복 코드 제거와 코드 재사용은 동일한 행동을 가리키는 서로 다른 단어다.
    • 중복 코드를 제거하기 위해 최대한 코드를 재사용해야 한다.

     

    ⚈ 상속 (Inheritance)

    • 객체지향에서 중복 코드를 제거하고 코드를 재사용할 수 있는 가장 유명한 방법이다.

    • 상속은 너무나도 매력적이기 때문에 객체지향에 갓 입문한 프로그래머들은 이에 도취된 나머지 모든 설계에 상속을 적용하려고 시도하곤 한다.
    • 상속이 코드 재사용이라는 측면에서 매우 강력한 도구이지만, 강력한 만큼 잘못 사용할 경우 돌아오는 피해 역시 크다.
    • 상속의 오용과 남용은 애플리케이션을 이해하고 확장하기 어렵게 만든다.
    • 정말로 필요한 경우에만 상속을 사용하라.

     

    ⚈ 합성 (Composition)

    • 상속은 코드 재사용과 관련된 대부분의 경우에 우아한 해결 방법이 아니다.
    • 상속의 단점을 피하면서도 코드를 재사용할 수 있는 더 좋은방법이 합성이다.
    • ☆ 11장에서 살펴볼 내용임.

     

    댓글