구체적인 방법을 재정의하는 것이 코드 냄새입니까?


31

구체적인 방법을 재정의하는 것이 코드 냄새라는 것이 사실입니까? 구체적인 방법을 재정의해야한다고 생각하기 때문에 :

public class A{
    public void a(){
    }
}

public class B extends A{
    @Override
    public void a(){
    }
}

그것은 다음과 같이 다시 쓸 수 있습니다

public interface A{
    public void a();
}

public class ConcreteA implements A{
    public void a();
}

public class B implements A{
    public void a(){
    }
}

B가 A에서 a ()를 재사용하려면 다음과 같이 다시 작성할 수 있습니다.

public class B implements A{
    public ConcreteA concreteA;
    public void a(){
        concreteA.a();
    }
}

메소드를 재정의하기 위해 상속이 필요하지 않은 것은 사실입니까?



4
Telastyn과 Doc Brown은 재정의에 대해서는 동의하지만 "코드 냄새"의 의미에 동의하지 않기 때문에 제목 질문에 반대의 대답을 제시합니다. 따라서 코드 디자인에 관한 질문은 속어와 밀접한 관련이 있습니다. 그리고 질문은 구체적으로 한 기술이 다른 기술에 대한 것이므로 불필요하게 그렇게 생각합니다.
Steve Jessop

5
질문은 Java로 태그되지 않았지만 코드는 Java 인 것으로 보입니다. C # (또는 C ++)에서는 virtual재정의를 지원하기 위해 키워드를 사용해야합니다 . 따라서 언어의 특성에 따라 문제가 모호해집니다.
Erik Eidt

1
거의 모든 프로그래밍 언어 기능이 필연적으로 충분한 시간이 지나면 "코드 냄새"로 분류되는 것처럼 보입니다.
Brandon

2
나는 final이러한 상황 에서 수정자가 정확하게 존재하는 것처럼 느낍니다 . 메소드의 작동이 대체되도록 설계되지 않은 경우로 표시하십시오 final. 그렇지 않으면 개발자가이를 재정의하도록 선택할 수 있습니다.
Martin Tuskevicius

답변:


34

아니요, 코드 냄새가 아닙니다.

  • 클래스가 최종 클래스가 아닌 경우 서브 클래스가 될 수 있습니다.
  • 메소드가 최종이 아닌 경우 대체 할 수 있습니다.

서브 클래 싱이 적절한 지, 그리고 어떤 메소드를 오버라이드 할 수 있는지주의 깊게 고려해야하는 것은 각 클래스의 책임 내에 있습니다 .

클래스는 자체 또는 임의의 메소드를 최종으로 정의하거나 서브 클래스 화 방법 및 위치에 제한 사항 (가시성 수정 자, 사용 가능한 생성자)을 둘 수 있습니다.

메소드를 대체하는 일반적인 경우 는 서브 클래스에서 (특히 인터페이스에 기본 메소드가 출현 한 Java 8에서) 사용자 정의되거나 최적화 될 수있는 기본 클래스 의 기본 구현 입니다.

class A {
    public String getDescription(Element e) {
        // return default description for element
    }
}

class B extends A {
    public String getDescription(Element e) {
        // return customized description for element
    }
}

비헤이비어 재정의 의 대안 은 비헤이비어가 인터페이스로 추상화되고 클래스에서 구현을 설정할 수있는 전략 패턴 입니다.

interface DescriptionProvider {
    String getDescription(Element e);
}

class A {
    private DescriptionProvider provider=new DefaultDescriptionProvider();

    public final String getDescription(Element e) {
       return provider.getDescription(e);
    }

    public final void setDescriptionProvider(@NotNull DescriptionProvider provider) {
        this.provider=provider;
    }
}

class B extends A {
    public B() {
        setDescriptionProvider(new CustomDescriptionProvider());
    }
}

현실에서는 구체적인 방법을 재정의하는 것이 코드 냄새를 맡을 수 있음을 이해합니다. 그러나 나는이 대답이 마음에 든다.
Darkwater23

마지막 예제에서 A구현 해서는 안 DescriptionProvider됩니까?
발견

@Spotted : A 구현할 DescriptionProvider수 있으므로 기본 설명 제공자입니다. 그러나 여기서의 아이디어는 우려의 분리입니다 A. 다른 구현이 제공하는 동안 설명이 필요합니다 (다른 클래스도 가능).
피터 월저

2
좋습니다, 전략 패턴 과 비슷합니다 .
발견

@Spotted : 맞습니다.이 예는 데코레이터 패턴이 아닌 전략 패턴을 보여줍니다 (데코레이터는 구성 요소에 동작을 추가합니다). 나는 대답을 고칠 것이다.
피터 월저

25

구체적인 방법을 재정의하는 것이 코드 냄새라는 것이 사실입니까?

일반적으로 구체적인 방법을 재정의하는 것은 코드 냄새입니다.

기본 메소드에는 개발자가 일반적으로 존중하는 동작이 연관되어 있으므로 변경하면 구현이 다른 경우 버그가 발생합니다. 더 나쁜 점은 동작이 변경되면 이전에 올바른 구현으로 인해 문제가 악화 될 수 있다는 것입니다. 그리고 일반적 으로 사소한 구체적인 방법으로 Liskov 대체 원칙 을 존중하는 것은 상당히 어렵습니다 .

이제는 이것이 가능하고 좋은 경우가 있습니다. 반 사소한 방법은 다소 안전하게 재정의 될 수 있습니다. 오버라이드 (over-ridden)되도록 의도 된 "세인 한 디폴트"종류의 행동으로 만 존재하는 기본 메소드는 잘 사용됩니다.

따라서 이것은 나에게 냄새처럼 보입니다. 때로는 좋은, 보통 나쁜 경우, 확인하기 위해 살펴보십시오.


29
귀하의 설명은 괜찮습니다, 그러나 나는 반대의 결론에 정확하게 제공 : "아니, 구체적인 방법을 재정의하는 것은 하지 일반적으로 코드 냄새"- 의도되지 않은 하나의 대체 방법 오버라이드 (override) 할 수 만 코드 냄새입니다. ;-)
Doc Brown

2
@DocBrown 정확하게! 또한 설명 (특히 결론)이 초기 진술에 반대한다는 것을 알았습니다.
Tersosauros

1
@Spotted : 가장 간단한 반례는 값을 다음 유효한 값으로 설정 Number하는 increment()메소드 가있는 클래스입니다 . 당신이로 서브 클래스 경우 EvenNumberincrement()이 하나가 아닌 추가 (그리고 세터 강제 시행이 균일), 영업 이익의 첫 번째 제안은 중복 구현해야합니다 모든 클래스의 방법을 그의 두 번째는 회원의 메소드를 호출 작성 래퍼 방법이 필요합니다. 상속의 목적 중 하나는 자녀 클래스가 다른 방법을 제공함으로써 부모와 어떻게 다른지 명확히하는 것입니다.
Blrfl

5
@DocBrown : 뭔가를 의미하지 않는 코드 냄새가되는 것은 필요 가 있다는 것만 나쁜 .
mikołak

3
@docbrown-저에게 이것은 "상속보다 선호하는 구성"과 함께 나옵니다. 구체적인 방법을 재정의하는 경우 이미 상속받은 것으로 악취가 작은 땅에 있습니다. 당신이해서는 안되는 것을 과도하게 극복 할 확률은 무엇입니까? 내 경험에 비추어 볼 때 나는 그것이 가능하다고 생각합니다. YMMV.
Telastyn

7

구체적인 방법을 재정의해야하는 경우 [...]로 다시 쓸 수 있습니다.

그런 식으로 다시 쓸 수 있다면 두 클래스를 모두 제어 할 수 있다는 의미입니다. 그러나 클래스 A가 이런 식으로 파생되도록 설계했는지 여부를 알 수 있으며 그렇게하면 코드 냄새가 아닙니다.

반면에 기본 클래스를 제어 할 수없는 경우 다시 작성할 수 없습니다. 따라서 클래스는 이런 식으로 파생되도록 설계되었습니다.이 경우 코드 냄새가 아니거나 그렇지 않은 경우 두 가지 옵션이 있습니다. 다른 솔루션을 찾거나 어쨌든 계속 진행하십시오. 작업 순도는 디자인 순도보다 우선합니다.


클래스 A가 파생되도록 설계 되었다면 의도를 명확하게 표현하는 추상적 인 방법을 사용하는 것이 더 합리적이지 않습니까? 방법을 재정의 한 사람은 방법이 이런 식으로 설계되었음을 어떻게 알 수 있습니까?
발견

@Spotted, 기본 구현 캠이 제공되는 많은 경우가 있으며 각 전문화는 기능을 확장하거나 효율성을 위해 이들 중 일부만 재정의하면됩니다. 극단적 인 예는 방문자입니다. 사용자는 방법이 문서에서 어떻게 설계되었는지 알고 있습니다. 어느 쪽이든 대체가 예상되는 것을 설명하기 위해 거기에 있어야합니다.
Jan Hudec

나는 당신의 관점을 이해하지만 여전히 개발자가 메소드를 재정의 할 수있는 유일한 방법은 문서를 읽는 것입니다 .1) 재정의 해야하는 경우, 나는 그것이 완료됩니다. 2) 재정의해서는 안되면 누군가가 편의를 위해 그것을 할 것입니다. 3) 모든 사람이 문서를 읽지는 않습니다. 또한 전체 메서드를 재정의 해야하는 경우는 없었지만 부분 만 사용했습니다 (템플릿 패턴을 사용하여 종료했습니다).
발견

@Spotted, 1)이 경우에 해야한다 오버라이드 (override), 그것이 있어야 abstract; 그러나 그럴 필요가없는 경우가 많습니다. 2) 재정의 하지 않아야하는 경우 final(가상적이지 않아야 함)해야합니다 . 그러나 여전히 메소드를 대체 할 수 있는 경우가 많습니다 . 3) 다운 스트림 개발자가 문서를 읽지 않으면 발로 쏠 수 있지만 어떤 조치를 취하 든간에 그렇게 할 것입니다.
Jan Hudec

우리 관점의 발산은 한 단어로만되어 있습니다. 모든 방법 추상적이거나 최종적 이어야 하지만 모든 방법 추상적이거나 최종적 이어야 한다고 말합니다 . :) 메소드를 오버라이드 할 때 이러한 경우는 무엇이고 안전합니까?
08:59에

3

먼저,이 질문은 특정 타이핑 시스템 에서만 적용 가능 하다는 점을 지적하겠습니다 . 예를 들어,의 구조 입력오리 타이핑 시스템 - 클래스는 단순히 가진 같은 이름 및있는 방법 서명 클래스 것은이었다 호환 입력 (오리 입력의 경우, 특정 방법에 대해 적어도).

이것은 Java, C ++ 등과 같은 정적으로 유형이 지정된 언어 의 영역과는 매우 다릅니다 . Java 및 C ++뿐만 아니라 C # 및 기타에서도 사용되는 강력한 유형 지정 의 특성입니다 .


난 당신이 방법을 명시해야 자바 배경에서오고있어 추측하고 마지막으로 표시된 경우 생성 계약 디자이너가 그들을 위해 의도 하지 오버라이드 (override) 할 수 있습니다. 그러나 C #과 같은 언어에서는 그 반대가 기본값입니다. 방법은 (어떤 장식없이, 특별한 키워드) "하는 봉인 (C 번호의 버전을" final), 그리고해야 명시 적으로 선언 virtual이러한 경우 허용 오버라이드 (override) 할 수 있습니다.

API 또는 라이브러리 재정의 가능한 구체적인 메소드를 사용하여 이러한 언어로 디자인 된 경우 ( 어떤 경우에는) 재정의되는 것이 합리적이어야합니다. 따라서 봉인되지 않은 / 가상 / final구체적 이지 않은 방법 을 재정의하는 것은 코드 냄새가 아닙니다 .     (물론 이것은 API 디자이너가 규칙을 따르고 있으며 virtual(C #)으로 표시하거나 final(Java)로 설계하고있는 적절한 방법을 표시한다고 가정합니다.)


참조


오리 타이핑에서 동일한 메소드 서명을 가진 두 클래스가 유형 호환 가능하기에 충분합니까, 또는 메소드가 동일한 의미를 가져서 일부 암시 적 기본 클래스에 대해 liskov로 대체 할 수 있어야합니까?
Wayne Conrad

2

예, 슈퍼 안티 패턴 을 호출 할 수 있기 때문 입니다. 또한 메소드의 초기 목적을 변경하고 부작용 (=> 버그)을 도입하며 테스트를 실패하게 할 수 있습니다. 단위 테스트가 빨간색 (실패) 인 클래스를 사용 하시겠습니까? 나는하지 않습니다.

나는 초기 코드 냄새가 방법이 아니 abstract거나 때라고 말함으로써 더 나아갈 것입니다 final. 생성 된 엔터티의 동작을 재정의 (재정의) 할 수 있기 때문에 (이 동작은 손실되거나 변경 될 수 있음) 로 abstractfinal의도는 명백하다 :

  • 초록 : 행동을 제공 해야합니다
  • 결선 : 행동을 수정할 수 없습니다

기존 클래스에 동작을 추가하는 가장 안전한 방법은 마지막 예제에서 보여주는 것입니다 : 데코레이터 패턴 사용.

public interface A{
    void a();
}

public final class ConcreteA implements A{
    public void a() {
        ...
    }
}

public final class B implements A{
    private final ConcreteA concreteA;
    public void a(){
        //Additionnal behaviour
        concreteA.a();
    }
}

9
흑백 접근 방식이 실제로 그렇게 유용한 것은 아닙니다. Object.toString()Java로 생각해보십시오 ... 이것이이면 abstract많은 기본 기능이 작동하지 않으며 누군가가 toString()메소드를 재정의하지 않으면 코드가 컴파일되지 않습니다 ! 그렇다면 finalJava 방식을 제외하고 객체를 문자열로 변환 할 수있는 능력이 없어 상황이 훨씬 나빠질 것입니다. 따라 @ 피터 발저 약의 대답 회담, 기본 구현 (예 :가 Object.toString()) 있습니다 중요합니다. 특히 Java 8 기본 메소드에서.
Tersosauros

경우 @Tersosauros 당신은, 맞다 toString()중 하나였다 abstract또는 final그것은 고통이 될 것입니다. 내 개인적인 견해로는이 방법이 손상되어 ( equals 및 hashCode와 같은) 전혀 사용하지 않는 것이 좋습니다 . Java 기본값의 초기 목적은 역 호환성을 가진 API 진화를 허용하는 것입니다.
발견

"retrocompatibility"라는 단어에는 많은 음절이 있습니다.
Craig

나는 정말이 사이트에 대한 구체적인 상속의 일반적인 수용에 실망하고 @Spotted, 더 나은 예상 : / 당신의 대답은 더 많은 upvotes을 가져야한다
TheCatWhisperer

1
@TheCatWhisperer 나는 동의하지만 다른 한편으로는 콘크리트 상속보다 장식을 사용하는 미묘한 이점을 찾기 위해 너무 오래 걸렸습니다 (사실 테스트를 처음 시작할 때 분명해졌습니다) . 최선의 방법은 진실 을 발견 할 때까지 다른 사람들을 교육하려고 시도하는 것입니다 (농담). :)
목격
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.