확장은 왜 악한가

extends키워드는 악이다; 찰스 맨슨 수준은 아니지만 가능할 때마다 피해야 할 정도로 나쁘다. Gang of Four Design Patterns 책에서는 구현 상속 ( extends)을 인터페이스 상속 ( implements)으로 대체하는 방법에 대해 자세히 설명 합니다.

훌륭한 디자이너는 구체적인 기본 클래스가 아닌 인터페이스 측면에서 대부분의 코드를 작성합니다. 이 기사에서는 디자이너가 그런 이상한 습관을 가지고 있는지 설명 하고 몇 가지 인터페이스 기반 프로그래밍 기본 사항을 소개합니다.

인터페이스 대 클래스

저는 한때 James Gosling (자바의 발명가)이 특집 연설자로 참여한 Java 사용자 그룹 회의에 참석했습니다. 기억에 남는 Q & A 세션에서 누군가 그에게 "Java를 다시 할 수 있다면 무엇을 바꾸고 싶습니까?"라고 물었습니다. "수업을 그만두 겠어요."그가 대답했다. 웃음이 사라지 자 그는 진짜 문제는 클래스 자체가 아니라 구현 상속 ( extends관계)이라고 설명했다. 인터페이스 상속 ( implements관계)이 바람직합니다. 가능하면 구현 상속을 피해야합니다.

유연성 상실

구현 상속을 피해야하는 이유는 무엇입니까? 첫 번째 문제는 구체적인 클래스 이름을 명시 적으로 사용하면 특정 구현에 잠기 게되어 다운 라인 변경이 불필요하게 어려워진다는 것입니다.

현대 애자일 개발 방법론의 핵심은 병렬 설계 및 개발의 개념입니다. 프로그램을 완전히 지정하기 전에 프로그래밍을 시작합니다. 이 기술은 프로그래밍이 시작되기 전에 설계가 완료되어야한다는 기존의 통념에 직면 해 있지만, 많은 성공적인 프로젝트에서 기존의 파이프 라인 방식보다 이런 방식으로 고품질 코드를 더 빠르게 (그리고 비용 효율적으로) 개발할 수 있음이 입증되었습니다. 그러나 병렬 개발의 핵심은 유연성이라는 개념입니다. 새로 발견 된 요구 사항을 가능한 한 쉽게 기존 코드에 통합 할 수있는 방식으로 코드를 작성해야합니다.

이 기능을 구현하는 대신 수도 필요를, 당신은 당신이 기능만을 구현 분명히 필요하지만, 변화를 수용하는 방법. 이러한 유연성이 없으면 병렬 개발이 불가능합니다.

인터페이스 프로그래밍은 유연한 구조의 핵심입니다. 그 이유를 알아보기 위해 사용하지 않을 때 어떤 일이 발생하는지 살펴 보겠습니다. 다음 코드를 고려하십시오.

f () {LinkedList 목록 = new LinkedList (); // ... g (목록); } g (LinkedList 목록) {list.add (...); g2 (목록)}

이제 빠른 조회에 대한 새로운 요구 사항이 발생하여 LinkedList제대로 작동하지 않는다고 가정합니다 . 이를 HashSet. 기존 코드에서는 ( 인수를받는) f()뿐만 아니라 수정해야 하고 모든 항목 이 목록을 전달 해야하므로 변경 사항이 지역화되지 않았습니다 .g()LinkedListg()

다음과 같이 코드를 다시 작성하십시오.

f () {컬렉션 목록 = new LinkedList (); // ... g (목록); } g (컬렉션 목록) {list.add (...); g2 (목록)}

가능 단순히 대체하여 해시 테이블의 링크 된리스트를 변경할 수 new LinkedList()로모그래퍼 new HashSet(). 그게 다야. 다른 변경은 필요하지 않습니다.

또 다른 예로 다음 코드를 비교해보십시오.

f () {컬렉션 c = new HashSet (); // ... g (c); } g (Collection c) {for (Iterator i = c.iterator (); i.hasNext ();) do_something_with (i.next ()); }

이에:

f2 () {컬렉션 c = new HashSet (); // ... g2 (c.iterator ()); } g2 (반복자 i) {while (i.hasNext ();) do_something_with (i.next ()); }

g2()메서드는 이제 Collection파생 항목과 .NET Framework에서 얻을 수있는 키 및 값 목록을 탐색 할 수 있습니다 Map. 실제로 컬렉션을 순회하는 대신 데이터를 생성하는 반복기를 작성할 수 있습니다. 테스트 스캐 폴드 또는 파일에서 프로그램으로 정보를 제공하는 반복기를 작성할 수 있습니다. 여기에는 엄청난 유연성이 있습니다.

커플 링

구현 상속의 더 중요한 문제는 결합입니다. 즉, 프로그램의 한 부분이 다른 부분에 바람직하지 않게 의존하는 것입니다. 전역 변수는 왜 강한 결합이 문제를 일으키는 지에 대한 전형적인 예를 제공합니다. 예를 들어 전역 변수의 유형을 변경하면 해당 변수를 사용하는 모든 함수 (즉, 변수에 연결됨 )가 영향을받을 수 있으므로이 모든 코드를 검사, 수정 및 다시 테스트해야합니다. 또한 변수를 사용하는 모든 함수는 변수를 통해 서로 연결됩니다. 즉, 변수의 값이 어색한 시간에 변경되면 한 함수가 다른 함수의 동작에 잘못 영향을 미칠 수 있습니다. 이 문제는 다중 스레드 프로그램에서 특히 끔찍합니다.

설계자는 커플 링 관계를 최소화하기 위해 노력해야합니다. 한 클래스의 객체에서 다른 클래스의 객체로의 메서드 호출은 느슨한 결합의 한 형태이기 때문에 결합을 완전히 제거 할 수 없습니다. 커플 링 없이는 프로그램을 가질 수 없습니다. 그럼에도 불구하고 OO (객체 지향적) 원칙을 슬기롭게 따름으로써 결합을 상당히 최소화 할 수 있습니다 (가장 중요한 것은 객체의 구현이 객체를 사용하는 객체로부터 완전히 숨겨져 야한다는 것입니다). 예를 들어, 객체의 인스턴스 변수 (상수가 아닌 멤버 필드)는 항상 private. 기간. 예외 없음. 이제까지. 진심이야. (때때로 protected방법을 효과적으로 사용할 수 있지만protected 인스턴스 변수는 혐오스러운 일입니다. 같은 이유로 get / set 함수를 사용해서는 안됩니다. 필드를 공개하는 데 지나치게 복잡한 방법 일뿐입니다 (기본 유형 값이 아닌 완전한 객체를 반환하는 액세스 함수는 반환 된 객체의 클래스가 디자인의 핵심 추상화 인 상황에서 합리적입니다.)

나는 여기서 현학적이지 않다. 내 작업에서 OO 접근 방식의 엄격함, 빠른 코드 개발 및 쉬운 코드 유지 관리간에 직접적인 상관 관계를 발견했습니다. 구현 숨김과 같은 중심 OO 원칙을 위반할 때마다 해당 코드를 다시 작성하게됩니다 (일반적으로 코드를 디버깅 할 수 없기 때문에). 프로그램을 다시 작성할 시간이 없어서 규칙을 따릅니다. 제 관심은 전적으로 실용적입니다. 순결을위한 순결에는 관심이 없습니다.

취약한 기본 클래스 문제

이제 결합 개념을 상속에 적용 해 보겠습니다. 를 사용하는 구현 상속 시스템 extends에서 파생 클래스는 기본 클래스와 매우 밀접하게 연결되어 있으며 이러한 밀접한 연결은 바람직하지 않습니다. 디자이너는이 동작을 설명하기 위해 "깨지기 쉬운 기본 클래스 문제"라는 이름을 적용했습니다. 기본 클래스는 안전 해 보이는 방식으로 기본 클래스를 수정할 수 있으므로 취약한 것으로 간주되지만 파생 클래스에 의해 상속 될 때이 새로운 동작으로 인해 파생 클래스가 오작동 할 수 있습니다. 기본 클래스의 메서드를 격리하여 검사하는 것만으로는 기본 클래스 변경이 안전한지 여부를 알 수 없습니다. 모든 파생 클래스도 살펴보고 테스트해야합니다. 또한 기본 클래스 기본 클래스를 모두 사용 하는 모든 코드를 확인해야합니다.파생 클래스 개체도 마찬가지입니다.이 코드는 새로운 동작으로 인해 손상 될 수도 있기 때문입니다. 키 기본 클래스를 간단히 변경하면 전체 프로그램이 작동하지 않게 될 수 있습니다.

취약한 기본 클래스와 기본 클래스 커플 링 문제를 함께 살펴 보겠습니다. 다음 클래스 ArrayList는 스택처럼 작동하도록 Java의 클래스를 확장 합니다.

class Stack extends ArrayList {private int stack_pointer = 0; public void push (Object article) {add (stack_pointer ++, article); } public Object pop () {return remove (--stack_pointer); } public void push_many (Object [] 기사) {for (int i = 0; i <기사 길이; ++ i) push (기사 [i]); }}

이처럼 간단한 수업도 문제가 있습니다. 사용자가 상속을 활용하고 ArrayListclear()메서드를 사용 하여 스택에서 모든 것을 꺼내면 어떤 일이 발생하는지 고려하십시오 .

Stack a_stack = new Stack(); a_stack.push("1"); a_stack.push("2"); a_stack.clear(); 

The code successfully compiles, but since the base class doesn't know anything about the stack pointer, the Stack object is now in an undefined state. The next call to push() puts the new item at index 2 (the stack_pointer's current value), so the stack effectively has three elements on it—the bottom two are garbage. (Java's Stack class has exactly this problem; don't use it.)

One solution to the undesirable method-inheritance problem is for Stack to override all ArrayList methods that can modify the array's state, so the overrides either manipulate the stack pointer correctly or throw an exception. (The removeRange() method is a good candidate for throwing an exception.)

This approach has two disadvantages. First, if you override everything, the base class should really be an interface, not a class. There's no point in implementation inheritance if you don't use any of the inherited methods. Second, and more importantly, you don't want a stack to support all ArrayList methods. That pesky removeRange() method isn't useful, for example. The only reasonable way to implement a useless method is to have it throw an exception, since it should never be called. This approach effectively moves what would be a compile-time error into runtime. Not good. If the method simply isn't declared, the compiler kicks out a method-not-found error. If the method's there but throws an exception, you won't find out about the call until the program actually runs.

A better solution to the base-class issue is encapsulating the data structure instead of using inheritance. Here's a new-and-improved version of Stack:

class Stack { private int stack_pointer = 0; private ArrayList the_data = new ArrayList(); public void push( Object article ) { the_data.add( stack_pointer++, article ); } public Object pop() { return the_data.remove( --stack_pointer ); } public void push_many( Object[] articles ) { for( int i = 0; i < o.length; ++i ) push( articles[i] ); } } 

So far so good, but consider the fragile base-class issue. Let's say you want to create a variant on Stack that tracks the maximum stack size over a certain time period. One possible implementation might look like this:

class Monitorable_stack extends Stack { private int high_water_mark = 0; private int current_size; public void push( Object article ) { if( ++current_size > high_water_mark ) high_water_mark = current_size; super.push(article); } public Object pop() { --current_size; return super.pop(); } public int maximum_size_so_far() { return high_water_mark; } } 

This new class works well, at least for a while. Unfortunately, the code exploits the fact that push_many() does its work by calling push(). At first, this detail doesn't seem like a bad choice. It simplifies the code, and you get the derived class version of push(), even when the Monitorable_stack is accessed through a Stack reference, so the high_water_mark updates correctly.

One fine day, someone might run a profiler and notice the Stack isn't as fast as it could be and is heavily used. You can rewrite the Stack so it doesn't use an ArrayList and consequently improve the Stack's performance. Here's the new lean-and-mean version:

class Stack { private int stack_pointer = -1; private Object[] stack = new Object[1000]; public void push( Object article ) { assert stack_pointer = 0; return stack[ stack_pointer-- ]; } public void push_many( Object[] articles ) { assert (stack_pointer + articles.length) < stack.length; System.arraycopy(articles, 0, stack, stack_pointer+1, articles.length); stack_pointer += articles.length; } } 

Notice that push_many() no longer calls push() multiple times—it does a block transfer. The new version of Stack works fine; in fact, it's better than the previous version. Unfortunately, the Monitorable_stack derived class doesn't work any more, since it won't correctly track stack usage if push_many() is called (the derived-class version of push() is no longer called by the inherited push_many() method, so push_many() no longer updates the high_water_mark). Stack is a fragile base class. As it turns out, it's virtually impossible to eliminate these types of problems simply by being careful.

인터페이스 상속을 사용하는 경우이 문제가 발생하지 않습니다. 상속 된 기능이 나쁘지 않기 때문입니다. 경우 Stack둘 다를 구현하는 인터페이스이며, Simple_stackA는 Monitorable_stack, 다음 코드는 훨씬 더 강력합니다.