추상 또는 가상 방법을 사용해야합니까?


11

기본 클래스가 순수한 인터페이스 클래스가 바람직하지 않다고 가정하고 아래의 두 가지 예를 사용하면 추상 또는 가상 메소드 클래스 정의를 사용하는 것이 더 좋은 방법입니까?

  • "추상"버전의 장점은 아마도 더 깔끔해 보이고 파생 클래스가 희망적으로 의미있는 구현을하도록 강요한다는 것입니다.

  • "가상"버전의 장점은 다른 버전에서 쉽게 가져올 수 있고 추상 버전에서 요구하는 것과 같은 기본 프레임 워크를 추가하지 않고도 테스트에 사용할 수 있다는 것입니다.

초록 버전 :

public abstract class AbstractVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

가상 버전 :

public class VirtualVersion
{
    public virtual ReturnType Method1()
    {
        return ReturnType.NotImplemented;
    }

    public virtual ReturnType Method2()
    {
        return ReturnType.NotImplemented;
    }
             .
             .
    public virtual ReturnType MethodN()
    {
        return ReturnType.NotImplemented;
    }

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

인터페이스가 바람직하지 않다고 가정하는 이유는 무엇입니까?
Anthony Pegram

부분적으로 문제가 없으면 하나가 다른 것보다 낫다고 말하기가 어렵습니다.
Codism

@Anthony :이 클래스에 들어가는 유용한 기능이 있기 때문에 인터페이스는 바람직하지 않습니다.
Dunk

4
return ReturnType.NotImplemented? 진심이야? 당신이 컴파일시에 구현되지 않은 형식을 거부 할 수없는 경우, 적어도 (당신은 할 수 있습니다 추상적 인 방법을 사용) 던져 예외.
Jan Hudec

3
@Dunk : 여기에 그들이 있다 필요. 반환 값 체크되지 않습니다.
Jan Hudec

답변:


15

내가 당신의 물건을 소비한다면 내 투표는 추상적 인 방법에 대한 것입니다. 그것은 "초기 실패"와 함께 간다. 선언 할 때 모든 메소드를 추가하는 것은 어려울 수 있지만 (괜찮은 리팩토링 도구는이 작업을 신속하게 수행하지만) 적어도 문제가 무엇인지 즉시 알고 수정하십시오. 나는 왜 우리가 갑자기 구현되지 않은 예외를 얻는 지 알기 위해 나중에 6 개월과 12 명이 넘는 사람들의 변화를 디버깅하는 것보다 낫습니다.


예기치 않게 NotImplemented 오류가 발생하는 것에 대한 좋은 지적입니다. 런타임 측면 오류 대신 컴파일 타임이 발생하기 때문에 추상 측면의 장점입니다.
Dunk

3
+1-상속자는 초기에 실패했을뿐만 아니라, 런타임에 실패한 것으로 생각한 것을 수행하는 대신 "Implement Abstract Class"를 통해 구현해야하는 메소드를 즉시 확인할 수 있습니다.
Telastyn

컴파일 타임에 실패하면 나열하지 못한 플러스와 IDE를 사용하여 메소드를 신속하게 자동 구현하는 것에 대한 참조로 인해이 답변을 수락했습니다.
덩크

25

가상 버전은 버그가 발생하기 쉽고 의미 상 올바르지 않습니다.

초록은 "이 방법은 여기서 구현되지 않았습니다.이 클래스를 작동 시키려면 구현해야합니다"라고 말합니다.

가상은 "기본 구현이 있지만 필요한 경우 나를 변경할 수 있습니다"라고 말합니다.

궁극적 인 목표가 테스트 가능성이라면 인터페이스가 일반적으로 최선의 선택입니다. (이 클래스는 ax가 아닌 x를 수행합니다). 잘 작동하려면 클래스를 더 작은 구성 요소로 나누어야 할 수도 있습니다.


3

이것은 수업의 사용법에 달려 있습니다.

메소드에 합리적인 "빈"구현이있는 경우 많은 메소드가 있으며 종종 메소드 중 몇 개만 대체하는 경우 virtual메소드 를 사용 하는 것이 좋습니다. 예를 들어 ExpressionVisitor이런 식으로 구현됩니다.

그렇지 않으면 abstract방법 을 사용해야한다고 생각합니다 .

이상적으로는 구현되지 않은 메소드가 없어야하지만 경우에 따라서는 이것이 최선의 방법입니다. 그러나 그렇게하기로 결정했다면 그러한 메소드는 NotImplementedException특별한 값을 반환하지 않고 throw해야 합니다.


"NotImplementedException"은 종종 누락 오류를 나타내며 "NotSupportedException"은 명백한 선택을 나타냅니다. 그 외에는 동의합니다.
Anthony Pegram

많은 방법들이 "X와 관련된 의무를 충족시키기 위해 필요한 것은 무엇이든"이라는 관점에서 정의된다는 점에 주목할 가치가있다. X와 관련된 항목이없는 객체에서 이러한 메소드를 호출하는 것은 의미가 없지만 여전히 잘 정의되어 있습니다. 또한, 객체가 X와 관련된 의무를 갖거나 갖지 않을 수있는 경우, 먼저 X에 관련된 모든 의무를 충족시키는 것이 무조건적이라고 말하고 조건을 충족시켜 조건을 충족시키는 것보다 무조건 "X와 관련된 모든 의무를 충족시킨다"고 말하는 것이 더 효율적입니다 .
supercat

1

기본 클래스가 구현하는 별도의 인터페이스를 정의한 후 추상적 인 접근 방식을 따르는 것이 좋습니다.

다음과 같은 이미징 코드 :

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public abstract class AbstractVersion : IVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

이렇게하면 다음과 같은 문제가 해결됩니다.

  1. 이제 AbstractVersion에서 파생 된 객체를 사용하는 모든 코드를 대신 구현하여 IVersion 인터페이스를 수신 할 수 있으므로 단위 테스트가 더 쉬워 질 수 있습니다.

  2. 그런 다음 제품의 릴리스 2는 인터페이스 IVersion2를 구현하여 기존 고객 코드를 손상시키지 않고 추가 기능을 제공 할 수 있습니다.

예.

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public interface IVersion2
{
    ReturnType Method2_1();
}

public abstract class AbstractVersion : IVersion, IVersion2
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();
    public abstract ReturnType Method2_1();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

이 클래스에 효과적인 단위 테스트를 방해하는 하드 코딩 된 종속성이 포함되지 않도록하려면 종속성 반전에 대해서도 읽어 볼 가치가 있습니다.


다른 버전을 처리 할 수있는 기능을 제공한다고 공언했습니다. 그러나 나는 디자인의 일반적인 부분으로 인터페이스 클래스를 효과적으로 사용하기 위해 다시 시도하고 시도했으며 항상 인터페이스 클래스가 가치를 제공하지 않거나 거의 제공하지 않고 실제로 삶을 더 쉽게 만드는 대신 코드를 모호하게한다는 것을 깨달았습니다. 인터페이스 클래스와 같이 여러 클래스가 다른 클래스를 상속 받게되는 경우는 거의 없으며 공유 할 수없는 상당한 공통점이 없습니다. 인터페이스가 제공하지 않는 공유 가능한 측면이 내장되어 있으므로 추상 클래스가 더 잘 작동하는 경향이 있습니다.
Dunk

클래스 이름이 좋은 상속을 사용하면 전체적으로 정신적으로 묶기가 어려운 여러 기능 인터페이스 클래스 이름보다 클래스 (및 시스템)를 쉽게 이해할 수있는 훨씬 직관적 인 방법을 제공합니다. 또한 인터페이스 클래스를 사용하면 많은 추가 클래스가 생성되어 시스템을 다른 방식으로 이해하기가 더 어려워집니다.
Dunk

-2

의존성 주입은 인터페이스에 의존합니다. 여기 짧은 예가 있습니다. Student 클래스에는 "IReporting"인터페이스 (ReportAction 메소드 사용)를 구현하는 매개 변수가 필요한 CreateStudent라는 함수가 있습니다. 학생을 만든 후 구체적 클래스 매개 변수에 대해 ReportAction을 호출합니다. 시스템이 학생을 만든 후 이메일을 보내도록 설정된 경우 ReportAction 구현에서 이메일을 보내는 구체적 클래스로 보내거나 ReportAction 구현에서 프린터로 출력을 보내는 다른 구체적인 클래스로 보낼 수 있습니다. 코드 재사용에 좋습니다.


1
이것은 이전의 5 가지 답변에서 제시되고 설명 된 점들에 비해 실질적인 것을 제공하지 않는 것 같습니다
gnat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.