인터페이스와 클래스?
한번은 자바 사용자 모임에 참석한 적이 있습니다. 컨퍼런스에서 James Gosling(Java의 아버지)이 창립자 연설을 했습니다. 그 기억에 남는 Q&A 부분에서 그는 "Java를 리팩토링한다면 무엇을 바꾸겠습니까?"라는 질문을 받았습니다. 그는 "수업을 없애고 싶다"고 답했다. 웃음이 잦아들자 진짜 문제는 클래스 자체가 아니라 상속관계 구현에 있다고 설명했다. 인터페이스 상속(관계 구현)이 더 좋습니다. 가능한 한 상속 구현을 피해야 합니다.
유연성 상실
왜 상속을 피해야 할까요? 첫 번째 문제는 구체적인 클래스 이름을 명시적으로 사용하면 특정 구현에 갇히게 되어 기본 변경이 불필요하게 어려워진다는 것입니다.
현재의 애자일 프로그래밍 방식에서 핵심은 병렬 설계와 개발 개념입니다. 프로그램을 세부적으로 설계하기 전에 프로그래밍을 시작합니다. 이 기술은 코딩을 시작하기 전에 디자인을 완료해야 하는 전통적인 접근 방식과는 다르지만, 많은 성공적인 프로젝트를 통해 전통적인 단계별 방법보다 고품질 코드를 더 빠르게 개발할 수 있다는 것이 입증되었습니다. 그러나 병렬 개발의 핵심은 유연성입니다. 새로 발견된 요구 사항을 가능한 한 쉽게 기존 코드에 병합할 수 있는 방식으로 코드를 작성해야 합니다.
필요할 수 있는 기능을 구현하기보다는 분명히 필요한 기능만 구현하고 변화를 적당히 허용하면 됩니다. 이러한 유연하고 병렬적인 개발이 없다면 이는 불가능합니다.
인터페이스 프로그래밍은 유연한 아키텍처의 핵심입니다. 그 이유를 설명하기 위해 이를 사용할 때 어떤 일이 발생하는지 살펴보겠습니다. 다음 코드를 고려하세요:
f()
{
LinkedList list = new LinkedList();
//...
g( list );
}
g( LinkedList list )
{
list.add( . .. );
g2( list )
}
빠른 쿼리에 대한 요구가 제기되어 LinkedList가 이를 해결할 수 없다고 가정합니다. 대신 HashSet을 사용해야 합니다. 기존 코드에서는 f()뿐만 아니라 g()(LinkedList 매개변수 사용) 및 g()가 목록을 전달하는 모든 코드를 수정해야 하기 때문에 변경 사항을 지역화할 수 없습니다. 다음과 같이 코드를 다시 작성하세요:
f()
{
Collection list = new LinkedList();
//...
g( list );
}
g( Collection list )
{
list.add ( ... );
g2( list )
}
Linked List를 해시로 수정하려면 new LinkedList() 대신 new HashSet()을 사용하는 것만큼 간단할 수 있습니다. 그게 다야. 그 외에는 수정할 것이 없습니다.
또 다른 예로 다음 두 코드를 비교해 보세요.
f()
{
Collection c = new HashSet();
//...
g( c );
}
g( Collection c )
{
for( Iterator i = c.iterator(); i.hasNext() )
do_something_with( i.next() );
}
및
f2()
{
컬렉션 c = new H 애쉬세트( ) ;
//...
g2( c.iterator() );
}
g2( Iterator i )
{
while( i.hasNext() )
do_something_with( i.next() );
}
이제 맵에서 키-값 쌍을 얻을 수 있는 것처럼 g2() 메서드는 컬렉션 파생을 반복할 수 있습니다. 실제로 컬렉션을 순회하는 대신 데이터를 생성하는 반복자를 작성할 수 있습니다. 테스트 프레임워크나 파일에서 정보를 가져오는 반복기를 작성할 수 있습니다. 이는 엄청난 유연성을 허용합니다.
결합
상속 구현에서 더 중요한 문제는 결합입니다. 즉, 프로그램의 한 부분이 다른 부분에 의존하는 것입니다. 전역 변수는 강한 결합이 문제를 일으킬 수 있는 이유에 대한 전형적인 예를 제공합니다. 예를 들어 전역 변수의 유형을 변경하면 해당 변수를 사용하는 모든 함수가 영향을 받을 수 있으므로 이 코드 전체를 검사하고 변경하고 다시 테스트해야 합니다. 또한 이 변수를 사용하는 모든 기능은 이 변수를 통해 서로 연결됩니다. 즉, 사용하기 어려운 시점에 변수 값을 변경하면 한 함수가 다른 함수의 동작에 잘못된 영향을 미칠 수 있습니다. 이 문제는 멀티스레드 프로그램에서는 상당히 숨겨져 있습니다.
디자이너로서 당신은 결합 관계를 최소화하기 위해 노력해야 합니다. 한 클래스의 개체에서 다른 클래스의 개체로의 메서드 호출은 느슨한 결합의 한 형태이므로 결합을 완전히 제거할 수는 없습니다. 커플링 없이는 프로그램을 가질 수 없습니다. 그러나 OO 규칙을 따르면 일부 결합을 최소화할 수 있습니다(가장 중요한 것은 객체의 구현이 이를 사용하는 객체로부터 완전히 숨겨져야 한다는 것입니다). 예를 들어, 객체의 인스턴스 변수(상수가 아닌 필드)는 항상 비공개여야 합니다. 일정 기간 동안, 예외 없이, 지속적으로 말입니다. (때때로 보호 메서드를 효과적으로 사용할 수 있지만 보호 인스턴스 변수는 혐오스럽습니다.) 같은 이유로 get/set 함수를 사용하면 안 됩니다. --- 반환 수정자에도 불구하고 도메인에 공개되기 때문에 지나치게 복잡하다고 느낄 뿐입니다. 원시값이 아닌 객체의 기능에 접근하는 이유가 어떤 경우에는 반환되는 객체 클래스가 디자인의 핵심 추상화입니다.
저는 책을 좋아하는 사람이 아닙니다. 내 작업에서는 OO 접근 방식의 엄격함, 신속한 코드 개발, 쉬운 코드 구현 사이에 직접적인 상관 관계가 있음을 발견했습니다. 구현 숨기기와 같은 핵심 OO 원칙을 위반할 때마다 결국 해당 코드를 다시 작성하게 됩니다(대개 코드를 디버깅할 수 없기 때문입니다). 코드를 다시 작성할 시간이 없으므로 해당 규칙을 따릅니다. 순전히 실용적인 이유에 관심이 있습니까? 깨끗한 이유에는 관심이 없습니다.
취약한 기본 클래스 문제
이제 결합 개념을 상속에 적용해 보겠습니다. 확장을 사용하는 구현 시스템에서 파생 클래스는 기본 클래스와 매우 긴밀하게 결합되므로 이러한 긴밀한 결합은 바람직하지 않습니다. 디자이너는 이 동작을 설명하기 위해 "취약한 기본 클래스 문제"라는 별명을 적용했습니다. 기본 클래스는 안전해 보이는 동안 기본 클래스를 수정하기 때문에 취약한 것으로 간주되지만 파생 클래스에서 상속할 때 새 동작으로 인해 파생 클래스가 제대로 작동하지 않을 수 있습니다. 기본 클래스를 개별적으로 검사하는 것만으로는 기본 클래스에 대한 변경 사항이 안전한지 여부를 알 수 없으며 모든 파생 클래스도 살펴보고 테스트해야 합니다. 또한 기본 클래스 개체와 파생 클래스 개체에도 사용되는 모든 코드를 확인해야 합니다. 이 코드는 새로운 동작으로 인해 손상될 수 있기 때문입니다. 기본 클래스를 간단히 변경하면 전체 프로그램이 작동하지 않게 될 수 있습니다.
취약한 기본 클래스와 기본 클래스 결합 문제를 살펴보겠습니다. 다음 클래스는 Java의 ArrayList 클래스를 확장하여 스택처럼 동작하도록 만듭니다.
class Stack 확장 ArrayList
{
private int stack_pointer = 0;
public void push( Object article )
{
add( stack_pointer++, article );
}+ +i )
push( 기사[i] );
}
}
이런 간단한 클래스도 문제가 있습니다. 사용자가 상속의 균형을 맞추고 ArrayList의 clear() 메서드를 사용하여 스택을 팝할 때 어떤 일이 발생하는지 생각해 보세요.
Stack a_stack = new Stack();
a_stack.push("1");
a_stack.push("2");
a_stack.clear();
이 코드는 성공적으로 컴파일되지만 기본 클래스가 스택 포인터 스택에 대해 알지 못하기 때문에 스택 개체는 현재 정의되지 않은 상태입니다. push()에 대한 다음 호출은 새 항목을 인덱스 2에 넣습니다. (stack_pointer의 현재 값) 따라서 스택에는 사실상 세 개의 요소가 있습니다. 아래쪽 두 개는 가비지입니다. (Java의 스택 클래스에는 정확히 이 문제가 있습니다. 사용하지 마세요.)
이 성가신 상속 메서드 문제에 대한 해결책은 배열의 상태를 수정할 수 있는 Stack의 모든 ArrayList 메서드를 재정의하여 재정의가 올바르게 되도록 하는 것입니다. 스택 포인터를 사용하거나 예외를 발생시킵니다. (removeRange() 메소드는 예외를 발생시키는 좋은 후보입니다.)
이 방법에는 두 가지 단점이 있습니다. 첫째, 모든 것을 포괄한다면 기본 클래스는 실제로 클래스가 아니라 인터페이스여야 합니다. 상속된 메서드를 사용하지 않으면 상속을 구현할 의미가 없습니다. 둘째, 더 중요한 것은 스택이 모든 ArrayList 메서드를 지원하도록 할 수 없다는 것입니다. 예를 들어, 짜증나는 RemoveRange()는 아무 작업도 수행하지 않습니다. 쓸모없는 메서드를 구현하는 유일한 합리적인 방법은 호출되어서는 안 되는 예외를 발생시키는 것입니다. 이 방법은 컴파일 오류를 런타임 오류로 효과적으로 전환합니다. 나쁜 점은 메소드가 단순히 정의되지 않은 경우 컴파일러가 메소드를 찾을 수 없음 오류를 인쇄한다는 것입니다. 메소드가 존재하지만 예외가 발생하는 경우 프로그램이 실제로 실행될 때까지 호출 오류에 대해 알 수 없습니다.
이 기본 클래스 문제에 대한 더 나은 해결책은 상속을 사용하는 대신 데이터 구조를 캡슐화하는 것입니다. 이것은 Stack의 새롭고 향상된 버전입니다.
class Stack
{
private int stack_pointer = 0;
private ArrayList the_data = new ArrayList() , article );
}
public Object pop()
{
return the_data .remove( --stack_pointer );
}
public void push_many( Object[] 기사 )
{
for( int i = 0; i < o.length )
push( 기사[i] ) ;
}
}
지금까지는 괜찮았지만 깨지기 쉬운 기본 클래스 문제를 고려하여 일정 기간 동안 최대 스택 크기를 추적하는 데 사용되는 스택 변수에 하나를 생성하고 싶다고 말합니다. 가능한 구현은 다음과 같습니다:
class Monitorable_stack 확장 Stack
high_water_mark = current_size;
super.push( article );
}
publish Object pop()
{
--current_size;
슈퍼를 돌려주세요. pop();
}
public int maximum_size_so_far()
{
return high_water_mark;
}
}
이 새로운 수업은 적어도 한동안은 잘 작동했습니다. 불행하게도 이 코드는 push_many()가 push()를 호출하여 작동한다는 사실을 이용합니다. 우선 이 디테일은 나쁘지 않은 선택인 것 같습니다. 이는 코드를 단순화하고 Stack 참조를 통해 Monitorable_stack에 액세스하는 경우에도 push()의 파생 버전을 얻을 수 있으므로 high_water_mark가 올바르게 업데이트됩니다.
위 내용은 Java의 상속 예제 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!