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


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()메소드 가있는 클래스입니다 . 당신이로 서브 클래스 경우 EvenNumber곳 increment()이 하나가 아닌 추가 (그리고 세터 강제 시행이 균일), 영업 이익의 첫 번째 제안은 중복 구현해야합니다 모든 클래스의 방법을 그의 두 번째는 회원의 메소드를 호출 작성 래퍼 방법이 필요합니다. 상속의 목적 중 하나는 자녀 클래스가 다른 방법을 제공함으로써 부모와 어떻게 다른지 명확히하는 것입니다.
— 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. 생성 된 엔터티의 동작을 재정의 (재정의) 할 수 있기 때문에 (이 동작은 손실되거나 변경 될 수 있음) 로 abstract와 final의도는 명백하다 :

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

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

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.