class Stack extends ArrayList
{
private int topOfStack = 0;
public void push( Object article )
{
add( topOfStack++, article );
}
public Object pop()
{
return remove( --topOfStack );
}
public void pushMany( Object[] articles )
{
for( int i = 0; i < articles.length; ++i )
push( articles[i] );
}
}
Even a class as simple as this one has problems. Consider what happens when a user
leverages inheritance and uses the ArrayList 's clear() method to pop everything off
the stack, like so:
Stack aStack = new Stack();
aStack.push("1");
aStack.push("2");
aStack.clear();
The code compiles just fine, but since the base class doesn't know anything about the
index of the item at the top of the stack (topOfStack), the Stack object is now in an
undefined state. The next call to push() puts the new item at index 2 (the current value of
the topOfStack), so the stack effectively has three elements on it, the bottom two of
which are garbage.
상속을 하게 되면 원하지 않는 메소드(Clear()) 까지 모두 상속하게 된다.
해결 방법(다른 문제를 발생)
1. 스택이 스택의 내부 상태를 변경하는 모든 메소드를 오버라이드 하는 것
=> 매우 많은 수고 필요, 기반 클래스에 새로운 메소드가 추가되었을 때는 속수무책
2. 파생 클래스가 상속을 원치않는 기반 클래스의 메소드(Clear())에서 예외를 던지는 방법
=> LSP를 어기는 것이면 결과적으로 OCP까지 지킬 수 없게 된다
컴파일 에러를 런타임 에러로 바꾸는 문제를 가지게 된다.
* 구현 상속을 사용하면 기반 클래스를 수정할 떄마다 파생 클래스들이 제대로 작동하는지를 테스트 해야한다.
구성을 통한 해결 방법
class Stack
{
private int topOfStack = 0;
private ArrayList theData = new ArrayList();
public void push( Object article )
{
theData.add( topOfStack++, article );
}
public Object pop()
{
return theData.remove( --topOfStack );
}
public void pushMany( Object[] articles )
{
for( int i = 0; i < articles.length; ++i )
push( articles[i] );
}
public int size() // current stack size.
{
return theData.size();
}
}
* 기반 클래서를 수정할 때마다 파생 클래스를 검토해 보아야 한다면 이는 기반 클래스를 확장하고 있는 것이 아니라
인터페이스를 구현하고 있는 것이다.
interface를 구현한 해결 방법
interface Stack
{
void push( Object o );
Object pop();
void pushMany( Object[] articles );
int size();
}
class SimpleStack implements Stack
{
private int topOfStack = 0;
private ArrayList theData = new ArrayList();
public void push( Object article )
{
theData.add( topOfStack++, article );
}
public Object pop()
{
return theData.remove( --topOfStack );
}
public void pushMany( Object[] articles )
{
for( int i = 0; i < articles.length; ++i )
push( articles[i] );
}
public int size() // current stack size.
{
return theData.size();
}
}
class MonitorableStack implements Stack
{
private int highWaterMark = 0;
private int lowWaterMark = 0;
SimpleStack stack = new SimpleStack();
public void push( Object o )
{
stack.push(o);
if( stack.size() > highWaterMark )
highWaterMark = stack.size();
}
public Object pop()
{
Object returnValue = stack.pop();
if( stack.size() < lowWaterMark )
lowWaterMark = stack.size();
return returnValue;
}
public void pushMany( Object[] articles )
{
for( int i = 0; i < articles.length; ++i )
push( articles[i] );
if( stack.size() > highWaterMark )
highWaterMark = stack.size();
}
public int maximumSize() { return highWaterMark; }
public int minimumSize() { return lowWaterMark; }
public void resetMarks () { highWaterMark = lowWaterMark = size(); }
public int size() { return stack.size(); }
}
결론
구현 상속은 구현이 편리한 대신 위험하고, 인터페이스 상속은 안전한 대신 구현이 고될 수 있다. 이클립스와 같은 IDE가 고됨을 대신해 준다. 가능하면 인터페이스 상속을 사용하라
Holub on 실용주의 디자인패턴의 책 내용을 공부하며 정리한 내용입니다.
저자나 역자에게 문제를 일으킨다면 삭제하겠습니다.