왜 클래스를 상속하고 속성을 추가하지 않습니까?


39

우리의 코드베이스에서 다음과 같은 상속 트리를 찾았습니다.

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class OrderDateInfo : NamedEntity { }

내가 수집 할 수있는 것으로부터, 이것은 주로 프론트 엔드에 물건을 묶는 데 사용됩니다.

나에게 이것은 generic에 의존하는 대신 클래스에 구체적인 이름을 부여하기 때문에 의미가 있습니다 NamedEntity. 반면에 단순히 추가 속성이없는 많은 클래스가 있습니다.

이 방법에 단점이 있습니까?


25
거꾸로 언급되지 않은 : 당신은 만 관련이있는 방법을 구별 할 수 OrderDateInfo서로 관련이있는 것들의 NamedEntity
Caleth

8
같은 이유로 Identifier클래스 와 속성이 NamedEntity필요하지만 클래스와 관련이없는 클래스를 사용하면 대신 사용 하지 않을 것입니다. 클래스의 컨텍스트와 올바른 사용법은 단순히 보유하는 속성과 메서드 그 이상입니다. IdNameNamedEntity
Neil

답변:


15

나에게 이것은 일반적인 NamedEntity에 의존하는 대신 클래스에 구체적인 이름을 부여하기 때문에 의미가 있습니다. 반면에 단순히 추가 속성이없는 많은 클래스가 있습니다.

이 방법에 단점이 있습니까?

접근 방식은 나쁘지 않지만 더 나은 솔루션이 있습니다. 요컨대, 인터페이스가 훨씬 더 나은 솔루션이 될 것입니다. 인터페이스와 상속이 다른 주된 이유 는 하나의 클래스에서만 상속 할 수 있지만 많은 인터페이스를 구현할 수 있기 때문 입니다.

예를 들어 명명 된 엔터티와 감사 된 엔터티가 있다고 가정합니다. 여러 엔티티가 있습니다.

One감사 대상이 아니거나 지명 된 실체가 아닙니다. 간단합니다 :

public class One 
{ }

Two명명 된 엔터티이지만 감사 된 엔터티는 아닙니다. 그것은 본질적으로 당신이 지금 가지고있는 것입니다 :

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Two : NamedEntity 
{ }

Three명명 된 및 감사 된 항목입니다. 여기서 문제가 발생합니다. 당신은 만들 수 있습니다 AuditedEntity기본 클래스를,하지만 당신은 할 수 없습니다 Three상속 AuditedEntity NamedEntity :

public class AuditedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : NamedEntity, AuditedEntity  // <-- Compiler error!
{ }

그러나에서 AuditedEntity상속 하여 해결 방법을 생각할 수도 있습니다 NamedEntity. 이것은 모든 클래스가 다른 클래스에서 (직접적으로) 상속받을 필요가 없도록하는 영리한 해킹입니다.

public class AuditedEntity : NamedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : AuditedEntity
{ }

이것은 여전히 ​​작동합니다. 그러나 여기서 당신이 한 일은 모든 감사 대상이 본질적으로 명명 된 실체라고 명시되어 있습니다. 마지막 예제로갑니다. Four감사 된 엔터티이지만 명명 된 엔터티는 아닙니다. 하지만 당신은 할 수 없습니다 Four에서 상속 AuditedEntity은 다음 또한이 만드는 것처럼 NamedEntityAuditedEntity 사이의 상속에 의한 andNamedEntity`.

상속을 사용하면 클래스 복제를 시작하지 않으면 완전히 새로운 문제가 발생하지 않는 한 둘 다 Three만들고 Four작동 시킬 수있는 방법이 없습니다 .

인터페이스를 사용하면 쉽게 달성 할 수 있습니다.

public interface INamedEntity
{
    int Id { get; set; }
    string Name { get; set; }
}

public interface IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class One 
{ }

public class Two : INamedEntity 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Three : INamedEntity, IAuditedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class Four : IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

여기서 유일한 단점은 여전히 ​​인터페이스를 구현해야한다는 것입니다. 그러나 특정 엔티티에 대해 여러 공통 유형에 대한 변형이 필요할 때 발생하는 단점없이 공통 재사용 가능 유형을 사용하면 모든 이점을 얻을 수 있습니다.

그러나 당신의 다형성은 그대로 남아 있습니다 :

var one = new One();
var two = new Two();
var three = new Three();
var four = new Four();

public void HandleNamedEntity(INamedEntity namedEntity) {}
public void HandleAuditedEntity(IAuditedEntity auditedEntity) {}

HandleNamedEntity(one);    //Error - not a named entity
HandleNamedEntity(two);
HandleNamedEntity(three);  
HandleNamedEntity(four);   //Error - not a named entity

HandleAuditedEntity(one);    //Error - not an audited entity
HandleAuditedEntity(two);    //Error - not an audited entity
HandleAuditedEntity(three);  
HandleAuditedEntity(four);

반면에 단순히 추가 속성이없는 많은 클래스가 있습니다.

이것은 마커 인터페이스 패턴 의 변형으로, 인터페이스 유형을 사용하여 주어진 클래스가이 인터페이스로 "마킹"되었는지 확인할 수 있도록 빈 인터페이스를 구현합니다.
구현 된 인터페이스 대신 상속 된 클래스를 사용하고 있지만 목표는 동일하므로이를 "표시된 클래스"라고합니다.

액면가에서 마커 인터페이스 / 클래스에는 아무런 문제가 없습니다. 그것들은 구문 적으로 기술적으로 유효하며, 마커가 보편적으로 진실하고 (컴파일 타임에) 조건부가 아니라면 그것들을 사용하는 데 따른 단점이 없습니다 .

이는 예외에 실제로 기본 방법에 비해 추가 속성 / 방법이없는 경우에도 서로 다른 예외를 구별하는 방법입니다.

따라서 본질적으로 잘못된 것은 없지만, 신중하게 사용하는 것이 좋습니다. 잘못 디자인 된 다형성으로 기존의 건축 실수를 막으려는 것이 아닙니다.


4
"한 클래스에서만 상속 할 수 있지만 많은 인터페이스를 구현할 수 있습니다." 그러나 모든 언어에 해당되는 것은 아닙니다. 일부 언어는 다중 클래스 상속을 허용합니다.
Kenneth K.

케네스가 말했듯이, 꽤 인기있는 두 언어는 다중 상속, C ++ 및 Python을 사용할 수 있습니다. three예를 들어 인터페이스를 사용하지 않는 함정의 클래스 는 실제로 C ++에서 유효합니다. 실제로 이것은 네 가지 예제 모두에서 제대로 작동합니다. 실제로 이것은 인터페이스를 사용해야 할 때 상속을 사용하지 않는다는 경고가 아니라 C #을 언어로 제한하는 것 같습니다.
opa December

63

이것은 다형성이 사용되는 것을 막기 위해 사용하는 것입니다.

NamedEntity상속 체인의 어딘가에 기본 클래스 가있는 15 개의 다른 클래스가 있고에 적용 가능한 새로운 메소드를 작성 한다고 가정 해보십시오 OrderDateInfo.

"서명"을 그대로 서명하면됩니다

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

그리고 희망과기도는 아무도 타이핑 시스템을 남용하지 않습니다 FooBazNamedEntity.

아니면 그냥 쓸 수 void MyMethod(OrderDateInfo foo)있습니다. 이제는 컴파일러에 의해 시행됩니다. 단순하고 우아하며 실수를하지 않는 사람들에게 의존하지 않습니다.

또한 @candied_orange가 지적했듯이 예외는 예외입니다. 아주 드물게 (그리고 나는 아주, 아주, 아주 드물게) 당신이로 모든 것을 잡기를 원하십니까 catch (Exception e)? 응용 프로그램에 대해 SqlException하나 FileNotFoundException이상의 예외 를 잡으려고 할 수 있습니다. 이러한 클래스는 종종 기본 Exception클래스 보다 더 많은 데이터 또는 기능을 제공하지 않지만 클래스를 검사하고 유형 필드를 확인하거나 특정 텍스트를 검색하지 않고도 클래스의 표현을 차별화 할 수 있습니다.

전반적으로 유형 클래스를 사용하여 기본 클래스를 사용하는 경우보다 더 좁은 유형의 유형을 사용할 수 있습니다. 내 말은, 모든 변수와 인수를 type으로 정의 할 수는 Object있지만 작업을 더 어렵게 만들지 않습니까?


4
새로움을 용서하지만 OrderDateInfo에 NamedEntity 객체를 속성으로 만드는 더 좋은 방법이 아닌 이유가 궁금합니다.
Adam B

9
@AdamB 유효한 디자인 선택입니다. 그것이 더 나은지 아닌지는 그것이 무엇을 위해 사용될 것인지에 달려 있습니다. 예외의 경우 언어 (C #)를 사용하면 예외 속성을 사용하여 새 클래스를 만들 수 없습니다. 상속보다는 컴포지션을 사용하지 못하게하는 다른 디자인 고려 사항이있을 수 있습니다. 여러 번 구성이 좋습니다. 시스템에 따라 다릅니다.
Becuzz

17
@AdamB 기본 클래스에서 상속하고 DO에 기본이없는 메소드 또는 속성을 추가 할 때 새 클래스를 작성하지 않고 기본 클래스를 속성으로 설정하는 것과 같은 이유로. 포유류가되는 인간과 포유류가있는 것에는 차이가 있습니다.
Logarr

8
@Logarr 사실 나는 포유 동물과 포유 동물 사이에 차이가 있습니다. 그러나 내가 내면의 포유류에 충실하면 그 차이를 결코 알 수 없습니다. 왜 변덕에 많은 종류의 우유를 줄 수 있는지 궁금 할 것입니다.
candied_orange

5
@AdamB 당신은 그렇게 할 수 있으며, 이것은 상속 이상의 구성으로 알려져 있습니다. 그러나 설명의 구성이 이해되도록 NamedEntity예를 들어로 이름 을 바꿉니다 EntityName.
Sebastian Redl

28

이것은 내가 가장 좋아하는 상속 사용입니다. 나는 더 좋고 더 구체적인 이름을 사용할 수있는 예외에 주로 사용합니다.

긴 사슬로 이어지고 요요 문제를 일으키는 일반적인 상속 문제 는 체인에 동기를 부여 할 것이 없기 때문에 여기에는 적용되지 않습니다.


1
흠, 좋은 포인트 다시 예외. 나는 그것이 끔찍한 일이라고 말하려고했습니다. 그러나 당신은 훌륭한 유스 케이스로 나를 설득했습니다.
David Arno

요요 호와 럼주 한 병. Tilop은 드물게 orlop이 yar입니다. 널빤지 사이에 적절한 오크가 필요하기 때문에 종종 누출됩니다. 랜드 루버와 함께 Davey Jones 사물함이 그것을 만들었습니다. 번역 : 상속 체인이 너무 좋아서 기본 클래스가 추상적 / 효과적으로 무시 될 수있는 경우는 드 rare니다.
radarbob

나는 그것의 가치는 다른 상속하는 새로운 클래스가 지적 생각 않는 새로운 클래스의 이름 - 새 속성을 추가 할 수 있습니다. 이것은 더 나은 이름으로 예외를 만들 때 사용하는 것입니다.
Logan Pickup

1

이모 클래스 디자인이 잘못되었습니다. 그것은해야한다.

public class EntityName
{
    public int Id { get; set; }
    public string Text { get; set; }
}

public class OrderDateInfo
{
    public EntityName Name { get; set; }
}

OrderDateInfoHAS A Name는보다 자연스러운 관계이며 원래 질문을 유발하지 않은 이해하기 쉬운 두 가지 클래스를 만듭니다.

NamedEntity매개 변수로 승인 된 메소드 는 IdName특성 에만 관심이 있었으므로 해당 메소드를 EntityName대신 승인하도록 변경해야합니다 .

원래 디자인에 대해 수락 할 유일한 기술적 이유는 OP가 언급 한 속성 바인딩 때문입니다. 쓰레기 프레임 워크는 추가 속성에 대처하고 바인딩 할 수 없습니다 object.Name.Id. 그러나 바인딩 프레임 워크가 그에 대처할 수 없다면 목록에 추가 할 기술 부채가 더 있습니다.

@Flater의 대답과 함께 갈 것입니다. 그러나 속성을 포함하는 많은 인터페이스를 사용하면 C #의 멋진 자동 속성조차도 많은 상용구 코드를 작성하게됩니다. Java로 그렇게한다고 상상해보십시오!


1

클래스는 동작을 노출하고 데이터 구조는 데이터를 노출합니다.

수업 키워드가 표시되지만 동작이 보이지 않습니다. 이것은이 클래스를 데이터 구조로보기 시작한다는 것을 의미합니다. 이 맥락에서 나는 당신의 질문을 다음과 같이 바꾸겠습니다.

공통 최상위 레벨 데이터 구조가있는 이유는 무엇입니까?

따라서 최상위 레벨 데이터 유형을 사용할 수 있습니다. 이를 통해 유형 시스템을 활용하여 속성이 모두 존재하도록하여 다양한 데이터 구조의 여러 세트에 걸쳐 정책을 보장 할 수 있습니다.

왜 최상위 구조를 포함하지만 아무것도 추가하지 않는 데이터 구조가 있습니까?

따라서 하위 레벨 데이터 유형을 사용할 수 있습니다. 이를 통해 타이핑 시스템에 힌트를 넣어 변수의 목적을 표현할 수 있습니다.

Top level data structure - Named
   property: name;

Bottom level data structure - Person

위의 계층 구조에서 Person이라는 이름을 지정하는 것이 편리하므로 사람들은 자신의 이름을 얻고 변경할 수 있습니다. Person데이터 구조 에 추가 속성을 추가하는 것이 합리적 일 수 있지만 우리가 해결하는 문제는 Person을 완벽하게 모델링 할 필요가 없으므로 age등의 공통 속성을 추가하지 않았습니다 .

따라서 타이핑 시스템을 활용하여이 명명 된 항목의 의도를 문서와 같은 업데이트와 충돌하지 않고 age나중에 쉽게 확장 할 수있는 방식으로 표현합니다. ).

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