아래의 예시에 나온 Dog과 Animal의 관계가 바로 상속의 예시이다.
```java
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
class Dog extends Animal {
void sleep() {
System.out.println(this.name+" zzz");
}
}
public class Sample {
public static void main(String[] args) {
Dog dog = new Dog();
dog.setName("poppy");
System.out.println(dog.name); // poppy 출력
dog.sleep(); // poppy zzz 출력
}
}
```
Dog는 Animal에 포함되기 때문에, “Dog is a
Animal”이라고 말할 수 있는 관계를 상속관계라고 이야기한다. 이는 코드를 재사용하고, 계층적인 관계를 구성하는
것에 있어서 효과적으로 사용된다.
구현상속: 다른 클래스를 추가로 확장하여 사용하는 의미.
인터페이스 상속 : 확장의 의미보단 인터페이스에 정의된 멤버들을 클래스에서 대신 구현하겠다는 의미.
→ 해당 아이템에서 이야기하는 ‘상속’은 구현 상속에 한정된다.
컴포지션 = 새로운 클래스를 만들어 기존 클래스가 새로운 클래스의 구성요소로 사용되는 설계.
새로운 클래스의 역할
- private 필드로 기존 클래스의 인스턴스 참조
- 기존 클래스의 대응하는 메서드를 호출해 결과 반환 = 전달(forwarding)
- 새 클래스의 메서드는 전달메서드라고 부른다.
→ 이는 내부 구현 방식에서 독립적으로 구성할 수 있다.
- 상속은 캡슐화를 꺠트리고, 상위 클래스에 의존성 관계를 가지고 있기 때문에 상위클래스의 변화에 따라 하위클래스가 변경될 가능성이 있다. (OCP 규칙에 위반)
- 메소드는 언제든지 재정의될 가능성이 있음
- 다른 패키지의 구체클래스를 상속하는 일은 위험하고, 같은 패키지 내에서라도 문제를 야기할수 있다.
- 보통 IS-A 관계일떄 상속을 사용하고, HAS-A 관계일때 컴포지션을 사용하라고 하지만 이는 잘못된 것( But 관점의 차이일뿐임 CAR IS-A AUTOMOBILE 이기도 하지만, CAR HAS-A ENGINE, WHEEL 관계이기도 함)
예시
-
상속 코드
class Animal{ void eat(); } class Cat extends Animal{ void meow(); } class Dog extends Animal{ void bark(); } class Robot{ void move(); } class CleaningRobot extends Robot{ void clean(); } class KillingRobot extends Robot{ void kill(); }
위와 같은 코드에서 만약 요구사항이 바뀌어 KillingRobotDog이 필요하게 되면 어떻게될까?
→ 상속은 객체의 특성을 단정짓기 때문에, 상속으로 구성된 구현은 요구사항의 변경에 유연하게 대처하지 못한다.
-
컴포지션 코드
아래 코드는 컴포지션 관계로 해당 코드를 변경한 예시이다.
class Dog{ BarkingStrategy bark; EatingStrategy eat; } class ManyEatingDog{ BarkingStrategy bark; EatingStrategy eat;// eat 구현체를 많이 먹는 것으로 갈아낀다 ! } class Cat{ MeowingStrategy meow; EatingStrategy eat; } class CleaningRobot{ CleaningStrategy clean; MovingStrategy move; } class KillingRobot{ KillingStrategy kill; MovingStrategy move; } class KillingRobotDog { KillingStrategy kill; MovingStrategy move; BarkingStrategy bark; }
행동을 하는 부분을 객체화시키고, 그 안의 Killing Strategy 같은 클래스 내부에서 구현하는 방식으로 진행하는 것이다.
-
교재의 예시
public class InstrumentedHashSet<E> extends ForwardingSet<E> { //추가된 원소의 수 private int addCount = 0; public InstrumentedHashSet(Set<E> s){ super(s); } @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); } public int getAddCount() { return addCount; } }
public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s) { this.s= s;} public void clear() {s.clear();} public boolean contains(Object o) { return s.contains(o);} public boolean isEmpty() { return s.isEmpty();} public int size() { return s.size();} public Iterator<E> iterator() { return s.iterator(); } public boolean add(E e) { return s.add(e); } public boolean addAll(Collection<? extends E> c) { return s.addAll(c); } }
아래에서 set 인스턴스를 감싸고 있다는 뜻에서 바로 위에 제시된 교재 내의 예시인 InstrumentedSet 과 같은 클래스를 래퍼 클래스
라고 하며,
이는 데코레이터 패턴
이라고도 불린다.
이런 래퍼 클래스는 단점이 거의 없으나, callback (콜백) 프레임워크와 어울리지 않는다는 점만 주의하면 된다.
- 상속은 강력하지만 캡슐화를 해치고 + 내부 구현을 불필요하게 노출할 수 있으므로, 확실하게
IS-A
관계일 때에만 사용하자. - 컴포지션과 전달을 사용하자.
- 래퍼 클래스는 충분히 강력한 성능을 가지고 있다.