메소드를 호출 할 수 있다는 것을 정의하는 것보다 메소드를 정의하는 것이 어떻게 강력한 약속보다 우선 할 수 있습니까?


36

보낸 사람 : http://www.artima.com/lejava/articles/designprinciples4.html

Erich Gamma : 10 년이 지난 후에도 여전히 그렇습니다. 상속은 행동을 바꾸는 멋진 방법입니다. 그러나 우리는 서브 클래스가 그것을 재정의하는 메소드가 호출되는 컨텍스트에 대해 쉽게 가정 할 수 있기 때문에 취하기 쉽다는 것을 알고 있습니다. 플러그인하는 서브 클래스 코드가 호출되는 암시 적 컨텍스트로 인해 기본 클래스와 서브 클래스 사이에 긴밀한 연결이 있습니다. 작곡에는 더 좋은 속성이 있습니다. 작은 물건을 더 큰 물건에 연결하면 커플 링이 줄어들고 큰 물건은 작은 물건을 다시 불러옵니다. API 관점에서 메소드를 대체 할 수 있음을 정의하는 것은 메소드를 호출 할 수 있음을 정의하는 것보다 더 강력합니다.

나는 그가 무엇을 의미하는지 이해하지 못한다. 누구든지 설명해 주시겠습니까?

답변:


63

약속은 미래의 옵션을 줄이는 것입니다. 메서드를 게시하면 사용자가 해당 메서드를 호출한다는 의미이므로 호환성을 유지하지 않으면이 메서드를 제거 할 수 없습니다. 당신이 그것을 유지한다면 private, 그들은 (직접적으로) 그것을 호출 할 수 없었으며 언젠가는 문제없이 그것을 리팩토링 할 수있었습니다. 따라서 메소드를 공개하는 것은 공개하지 않는 것보다 더 강력한 약속입니다. 재정가능한 방법을 게시하는 것은 더욱 강력한 약속입니다. 사용자는 호출 할 수 있습니다, 그리고 그들은 방법은 당신이하지 생각하지 않는 새로운 클래스를 만들 수 있습니다!

예를 들어 정리 방법을 게시하는 경우 사용자가 마지막 방법으로이 메서드를 호출하는 것을 기억하는 한 리소스가 올바르게 할당 해제되도록 할 수 있습니다. 그러나 메소드가 재정의 가능한 경우 누군가가 서브 클래스에서 메소드를 재정의하고 호출 하지 않을 수 있습니다 super. 결과적으로, 세 번째 사용자는 해당 클래스를 사용 하여 끝에서 정당하게 호출하더라도cleanup() 자원 누출을 유발할 수 있습니다 ! 즉, 코드의 의미를 더 이상 보장 할 수 없습니다. 이는 매우 나쁜 일입니다.

기본적으로 일부 중개인이이를 무시할 수 있기 때문에 사용자가 재정의 가능한 메소드에서 실행되는 코드에 더 이상 의존 할 수 없습니다. 즉 private, 사용자의 도움없이 모든 방법으로 정리 루틴을 완전히 구현해야합니다 . 따라서 finalAPI 사용자가 명시 적으로 재정의하기위한 것이 아니라면 요소 만 게시하는 것이 좋습니다 .


11
이것은 내가 읽은 상속에 대한 가장 좋은 주장 일 것입니다. 내가 직면 한 모든 이유 중, 나는이 두 가지 주장을 결코 접한 적이 없었지만 (재정의를 통해 기능을 결합하고 깨는 것) 상속에 대한 매우 강력한 주장입니다.
David Arno

5
@DavidArno 나는 그것이 상속에 대한 논쟁이라고 생각하지 않습니다. 나는 그것이 "기본적으로 모든 것을 무시할 수있게한다"는 것에 대한 논쟁이라고 생각한다. 상속은 그 자체로는 위험하지 않으며 생각없이 사용하는 것입니다.
svick December

15
이것이 좋은 지적처럼 들리지만 "사용자가 자신의 버그가있는 코드를 추가 할 수 있습니다"가 어떻게 논쟁인지는 알 수 없습니다. 상속을 사용하면 버그를 예방하고 수정할 수있는 업데이트 가능성을 잃지 않고 기능 부족을 추가 할 수 있습니다. API 상단의 사용자 코드가 깨지면 API 결함이 아닙니다.
Sebb

4
첫 번째 코더는 정리 인수를 만들지 만 실수를하고 모든 것을 정리하지는 않습니다. 두 번째 코더는 정리 방법을 재정의하고 제대로 작동하며 코더 # 3은 클래스를 사용하며 코더 # 1이 엉망이더라도 리소스 누수가 없습니다.
Pieter B

6
@ 도발 참으로. 그렇기 때문에 상속은 거의 모든 입문 OOP 서적과 수업에서 수업 1 위라는 비극입니다.
Kevin Krumwiede

30

일반 함수를 게시하면 단방향 계약
이 제공됩니다. 함수가 호출되면 그 기능은 무엇입니까?

콜백을 게시하는 경우 일회성 계약도 제공합니다.
언제 어떻게 호출됩니까?

재정의 가능한 함수를 게시하는 경우 한 번에 두 가지 기능을 수행하므로 양면 계약을 해야합니다 .
언제 호출되며 호출되면 어떻게해야합니까?

사용자가 API를 남용하지 않는 경우에도 ( 계약을 탐지하는 데 비용이 많이들 수있는 계약 일부를 위반 하여) 후자가 훨씬 더 많은 문서가 필요하다는 것을 쉽게 알 수 있으며 문서화하는 모든 것이 약정입니다. 당신의 추가 선택.

이러한 양면 계약 어기려고의 예에서 움직임이다 showhidesetVisible(boolean)java.awt.Component의 .


+1. 다른 답변이 왜 수락되었는지 잘 모르겠습니다. 그것은 흥미로운 점을 제시하지만 인용 된 구절의 의미가 아니기 때문에이 질문에 대한 정답 은 아닙니다.
ruakh

이것이 정답이지만 예제를 이해하지 못합니다. setVisible (boolean)으로 show and hide를 바꾸면 상속을 사용하지 않는 코드가 깨지는 것 같습니다. 뭔가 빠졌습니까?
eigensheep

3
@eigensheep : show그리고 hide여전히 존재합니다 @Deprecated. 따라서 변경으로 인해 코드를 호출하는 코드가 손상되지 않습니다. 그러나 재정의 한 경우 새 'setVisible'로 마이그레이션하는 클라이언트는 재정의를 호출하지 않습니다. (나는 스윙을 사용한 적이 없으므로, 그것들을 대체하는 것이 얼마나 흔한 지 잘 모르겠습니다. 그러나 오래 전에 일어난 이후로, Deduplicator가 그것을 기억하는 이유는 고통스럽게 물었다는 것입니다.)
ruakh

12

Kilian Foth의 답변은 훌륭합니다. 이것이 왜 문제인지에 대한 정식 예제 *를 추가하고 싶습니다. 정수 포인트 클래스를 상상해보십시오.

class Point2D {
    public int x;
    public int y;

    // constructor
    public Point2D(int theX, int theY) { x = theX; y = theY; }

    public int hashCode() { return x + y; }

    public boolean equals(Object o) {
        if (this == o) { return true; }
        if ( !(o instanceof Point2D) ) { return false; }

        Point2D that = (Point2D) o;

        return (x == that.x) &&
               (y == that.y);
    }
}

이제 3D 포인트로 하위 클래스를 만들어 봅시다.

class Point3D extends Point2D {
    public int z;

    // constructor
    public Point3D(int theX, int theY, int theZ) {
        super(x, y); z = theZ;
    }

    public int hashCode() { return super.hashCode() + z; }

    public boolean equals(Object o) {
        if (this == o) { return true; }
        if ( !(o instanceof Point3D) ) { return false; }

        Point3D that = (Point3D) o;

        return super.equals(that) &&
               (z == that.z);
    }
}

매우 간단합니다! 우리의 요점을 사용합시다 :

Point2D p2a = new Point2D(3, 5);
Point2D p2b = new Point2D(3, 5);
Point2D p2c = new Point2D(3, 7);

p2a.equals(p2b); // true
p2b.equals(p2a); // true
p2a.equals(p2c); // false

Point3D p3a = new Point3D(3, 5, 7);
Point3D p3b = new Point3D(3, 5, 7);
Point3D p3c = new Point3D(3, 7, 11);

p3a.equals(p3b); // true
p3b.equals(p3a); // true
p3a.equals(p3c); // false

내가 왜 그렇게 쉬운 예를 게시하는지 궁금 할 것입니다. 다음은 캐치입니다.

p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!

2D 점을 동등한 3D 점과 비교하면 true가되지만 비교를 반대로하면 p2a가 실패하기 때문에 false가 instanceof Point3D됩니다.

결론

  1. 일반적으로 수퍼 클래스가 작동하는 방식과 더 이상 호환되지 않는 방식으로 서브 클래스에서 메소드를 구현할 수 있습니다.

  2. 부모 클래스와 호환되는 방식으로 크게 다른 서브 클래스에서 equals ()를 구현하는 것은 일반적으로 불가능합니다.

사람들이 서브 클래스를 만들도록 허용하는 클래스를 작성할 때 각 방법의 작동 방식에 대한 계약 을 작성하는 것이 좋습니다 . 더 나은 방법은 사람들이 계약을 위반하지 않음을 입증하기 위해 재정의 된 메소드 구현에 대해 실행할 수있는 단위 테스트 세트 일 것입니다. 너무 많은 일이기 때문에 아무도 그렇게하지 않습니다. 하지만 걱정한다면해야 할 일입니다.

잘 작성된 계약의 좋은 예는 Comparator 입니다. .equals()위에서 설명한 이유로 말한 내용 을 무시하십시오 . 다음은 Comparator가 할 .equals()수없는 일 을 수행하는 방법에 대한 예입니다 .

노트

  1. Josh Bloch의 "Effective Java"항목 8이이 예제의 소스이지만 Bloch는 ColorPoint를 사용하여 세 번째 축 대신 색상을 추가하고 int 대신 double을 사용합니다. Bloch의 Java 예제는 기본적으로 Odersky / Spoon / Venners에 의해 복제되어 예제를 온라인으로 제공합니다.

  2. 부모 클래스에 하위 클래스에 대해 알리면이 문제를 해결할 수 있기 때문에 여러 사람들이이 예제에 반대했습니다. 충분히 적은 수의 하위 클래스가 있고 부모가 모든 하위 클래스를 알고 있으면 사실입니다. 그러나 원래의 질문은 다른 누군가가 서브 클래스를 작성할 API를 만드는 것에 관한 것입니다. 이 경우 일반적으로 하위 구현과 호환되도록 상위 구현을 업데이트 할 수 없습니다.

보너스

Comparator는 equals ()를 올바르게 구현하는 문제를 해결하기 때문에 흥미 롭습니다. 더 나은 방법은이 유형의 상속 문제를 해결하기위한 패턴 인 전략 디자인 패턴을 따르는 것입니다. Haskell과 Scala 사람들이 열광하는 Typeclass도 전략 패턴입니다. 상속은 나쁘거나 잘못이 아니며 까다 롭습니다. 자세한 내용은 Philip Wadler의 논문 을 참조하십시오. ad-hoc 다형성을 ad ad hoc로 만드는 방법


1
그러나 SortedMap 및 SortedSet은 실제로 equalsMap 및 Set이 정의하는 방식 의 정의를 변경하지 않습니다 . 예를 들어, 요소는 같지만 정렬 순서가 다른 두 개의 SortedSet가 여전히 동일하게 비교되는 등의 효과로 인해 동일성은 순서를 완전히 무시합니다.
user2357112

1
@ user2357112 당신이 맞고 그 예를 제거했습니다. Map과 호환되는 SortedMap.equals ()는 별도의 문제로 진행됩니다. SortedMap은 일반적으로 O (log2 n)이고 HashMap (Map의 정식 임박)은 O (1)입니다. 따라서 주문 을 정말로 염려하는 경우에만 SortedMap을 사용합니다 . 이런 이유로 나는 OrderedMap 구현에서 equals () 테스트의 중요한 구성 요소가 될 정도로 순서가 중요하다고 생각합니다. 그들은 equals () 구현을 Map과 공유해서는 안됩니다 (Java의 AbstractMap을 통해 수행).
GlenPeterson

3
"상속은 나쁘거나 잘못이 아니라 까다 롭습니다." 나는 당신이 말하는 것을 이해하지만 까다로운 것은 일반적으로 오류, 버그 및 문제로 이어집니다. 보다 안정적인 방식으로 동일한 일 (또는 거의 모든 동일한 일)을 달성 할 수 있으면 더 까다로운 방법 나쁘다.
jpmc26

7
이것은 끔찍한 예입니다, 글렌 상속을 사용해서는 안되는 방식으로 상속을 사용했습니다. 클래스가 의도 한 방식으로 작동하지 않는 것은 놀라운 일이 아닙니다. 잘못된 추상화 (2D 포인트)를 제공하여 Liskov의 대체 원칙을 어겼습니다. 그러나 잘못된 예에서 상속이 잘못되었다고해서 이것이 일반적으로 나쁜 것은 아닙니다. 이 대답은 합리적으로 보일지 모르지만 그것이 가장 기본적인 상속 규칙을 어기는 것을 깨닫지 못하는 사람들을 혼란스럽게 할 것입니다.
Andy

3
Liskov의 Substituton 원리의 ELI5는 말한다 : 해야 클래스는 B클래스의 자식 A당신이 클래스의 객체를 생성해야합니다 B, 당신은 클래스 캐스트 할 수 있어야합니다 B의 구현 세부 사항을 잃지 않고 그것의 부모 개체를하고 주조 변수의 API를 사용 아이. 세 번째 속성을 제공하여 규칙을 어겼습니다. 기본 클래스에 그러한 속성이 존재한다는 z것을 모를 때 Point3D변수를로 캐스팅 한 후 좌표에 어떻게 액세스 할 계획 Point2D입니까? 자식 클래스를 기본 클래스로 캐스팅하여 공개 API를 중단하면 추상화가 잘못됩니다.
Andy

4

상속은 캡슐화를 약화시킵니다

상속이 허용 된 인터페이스를 게시하면 인터페이스의 크기가 크게 늘어납니다. 재정의 가능한 각 메서드를 대체 할 수 있으므로 생성자에 제공된 콜백으로 생각해야합니다. 클래스에서 제공하는 구현은 단지 콜백의 기본값입니다. 따라서, 방법에 대한 기대치가 무엇인지를 나타내는 일종의 계약이 제공되어야한다. 이것은 거의 일어나지 않으며 객체 지향 코드가 취성이라고 불리는 주된 이유입니다.

아래는 Peter Norvig ( http://norvig.com/java-iaq.html )가 제공하는 Java 컬렉션 프레임 워크의 실제 (간체 화 된) 예입니다 .

Public Class HashTable{
    ...
    Public Object put(K key, V value){
        try{
            //add object to table;
        }catch(TableFullException e){
            increaseTableSize();
            put(key,value);
        }
    }
}

우리가 이것을 서브 클래 싱하면 어떻게 될까요?

/** A version of Hashtable that lets you do
 * table.put("dog", "canine");, and then have
 * table.get("dogs") return "canine". **/

public class HashtableWithPlurals extends Hashtable {

    /** Make the table map both key and key + "s" to value. **/
    public Object put(Object key, Object value) {
        super.put(key + "s", value);
        return super.put(key, value);
    }
}

버그가 있습니다. 때때로 "dog"을 추가하고 hashtable은 "dogss"에 대한 항목을 얻습니다. 원인은 Hashtable 클래스를 디자인하는 사람이 기대하지 않은 put을 구현 한 사람이었습니다.

상속은 확장 성을 깬다

클래스의 서브 클래스 화를 허용하면 클래스에 메소드를 추가하지 않겠다고 약속 한 것입니다. 그렇지 않으면 아무것도 중단하지 않고 수행 할 수 있습니다.

인터페이스에 새 메소드를 추가 할 때 클래스에서 상속받은 사람은 해당 메소드를 구현해야합니다.


3

메소드를 호출하려는 경우 올바르게 작동해야합니다. 그게 다야. 끝난.

메서드를 재정의하도록 디자인 된 경우 메서드의 범위를 신중하게 고려해야합니다. 범위가 너무 큰 경우 자식 클래스는 종종 부모 메서드의 복사하여 붙여 넣은 코드를 포함해야합니다. 너무 작 으면 원하는 새로운 기능을 갖기 위해 많은 메소드를 재정의해야합니다. 이는 복잡성과 불필요한 라인 수를 추가합니다.

따라서 상위 메소드의 작성자는 클래스 및 해당 메소드가 향후 대체 될 수있는 방법을 가정해야합니다.

그러나 저자는 인용 된 텍스트에서 다른 문제에 대해 이야기하고 있습니다.

그러나 우리는 서브 클래스가 그것을 재정의하는 메소드가 호출되는 컨텍스트에 대해 쉽게 가정 할 수 있기 때문에 취하기 쉽다는 것을 알고 있습니다.

방법을 고려 a일반적으로 메서드에서 호출되고 b있지만, 방법에서 드문 비 명백한 경우 c. 메소드 재정의 작성자가 c메소드와에 대한 기대치를 간과한다면 a어떻게 잘못 될 수 있는지는 분명합니다.

따라서 a명확하고 명확하게 정의되고 "한 가지 일만하고 잘 수행" 하는 것이 더 중요합니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.