List <T>에서 상속받지 않겠습니까?


1398

프로그램을 계획 할 때 종종 다음과 같은 생각으로 시작합니다.

축구 팀은 축구 선수 목록입니다. 따라서 다음과 같이 표현해야합니다.

var football_team = new List<FootballPlayer>();

이 목록의 순서는 선수가 명단에 나열되는 순서를 나타냅니다.

그러나 나중에 팀에는 단순한 플레이어 목록 외에도 기록해야 할 다른 속성이 있다는 것을 알고 있습니다. 예를 들어, 이번 시즌의 총 점수, 현재 예산, 균일 한 색상, string팀 이름을 나타내는 등

그래서 나는 생각합니다 :

풋볼 팀은 선수 목록과 비슷하지만, 이름 (a string)과 총점 (an int) 을가 집니다. .NET은 풋볼 팀을 저장하기위한 수업을 제공하지 않기 때문에 나만의 수업을 만들 것입니다. 가장 유사하고 관련성이 높은 기존 구조는입니다 List<FootballPlayer>.

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

그러나 지침에 따르면 상속해서는 안됩니다List<T> . 나는이 지침에서 두 가지 측면에서 완전히 혼란스러워합니다.

왜 안돼?

분명히 List성능에 최적화되어 있습니다. 어떻게 요? 연장하면 어떤 성능 문제가 발생 List합니까? 정확히 무엇을 깰까요?

내가 본 또 다른 이유 List는 Microsoft에서 제공 한 것이므로이를 제어 할 수 없으므로 "public API"노출 후 나중에 변경할 수 없습니다 . 그러나 나는 이것을 이해하려고 노력합니다. 공개 API 란 무엇이며 왜 관심을 가져야합니까? 현재 프로젝트에이 공용 API가 없거나 포함되어 있지 않을 경우이 지침을 무시해도됩니까? 상속을 List 받고 공개 API가 필요한 것으로 밝혀지면 어떤 어려움이 있습니까?

왜 중요합니까? 리스트는리스트입니다. 무엇이 변경 될 수 있습니까? 무엇을 바꾸고 싶습니까?

마지막으로, Microsoft가 내가 상속받지 않기를 원한다면 List왜 수업을하지 않았 sealed습니까?

내가 무엇을 사용해야합니까?

분명히, 사용자 지정 컬렉션을 위해 Microsoft는 Collection대신에 확장해야하는 클래스를 제공했습니다 List. 그러나이 클래스는 매우 나쁘고 예를 들어와 같은AddRange 유용한 것들이 많지 않습니다 . jvitor83의 대답 은 특정 방법에 대한 성능 이론적 근거를 제공하지만 느리게는 AddRange아니지 않은 방법은 AddRange무엇입니까?

에서 상속하는 Collection것이 에서 상속하는 것보다 훨씬 많은 작업이며 List이점이 없습니다. 분명히 Microsoft는 아무런 이유없이 추가 작업을 수행하도록 지시하지 않으므로 어떻게 든 무언가를 오해하고 있다고 느끼는 것을 도울 수 없으며 상속 Collection은 실제로 내 문제에 대한 올바른 해결책이 아닙니다.

구현과 같은 제안을 보았습니다 IList. 안돼. 이것은 아무것도 얻지 못하는 수십 줄의 상용구 코드입니다.

마지막으로, 일부는 List무언가를 감싸는 것이 좋습니다 .

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

이것에는 두 가지 문제가 있습니다 :

  1. 내 코드를 불필요하게 장황하게 만듭니다. 이제 my_team.Players.Count그냥 대신에 전화해야합니다 my_team.Count. 고맙게도 C #을 사용하면 인덱서를 정의하여 인덱싱을 투명하게 만들고 내부의 모든 메소드를 전달할 수 있습니다 List...하지만 많은 코드입니다! 그 모든 일을 위해 무엇을 얻습니까?

  2. 그냥 평범하지 않습니다. 풋볼 팀은 선수 목록을 가지고 있지 않습니다. 그것은 이다 플레이어의 목록입니다. "John McFootballer가 SomeTeam의 플레이어에 합류했습니다"라고 말하지 않습니다. "John이 SomeTeam에 가입했습니다"라고 말합니다. "문자열 문자"에 문자를 추가하지 않고 문자열에 문자를 추가합니다. 책을 도서관의 책에 추가하지 않고 책을 도서관에 추가합니다.

"후드에서"발생하는 일이 "Y의 내부 목록에 X 추가"라고 말할 수 있지만, 이것은 세상에 대해 매우 반 직관적 인 사고 방식처럼 보입니다.

내 질문 (요약)

"논리적"( "인간의 마음에"라고하는 것입니다)이 단지 인 데이터 구조, 표현의 올바른 C #을 방법은 무엇입니까 listthings몇 종과 경적은?

상속은 List<T>항상 허용되지 않습니까? 언제 받아 들일 수 있습니까? 왜 왜 안돼? 상속 여부를 결정할 때 프로그래머가 고려해야 할 사항은 무엇입니까 List<T>?


90
따라서 상속을 사용할 때 (일반 사각형이 요청 될 때 항상 사각형을 제공하는 것이 합리적이므로 사각형은 사각형입니다)와 구성을 사용하는 경우 (FootballTeam에는 축구 선수 목록이 있습니다. 이름과 같이 "기본"과 같은 다른 속성)? 코드의 다른 곳에서 간단한 목록을 기대할 때 FootballTeam에 전달하면 다른 프로그래머가 혼란 스러울 것입니까?
Flight Odyssey

77
풋볼 팀이 추가 속성을 가진 플레이어 목록 대신 플레이어 계약 목록을 소유 한 비즈니스 회사라고 말하면 어떻게됩니까?
STT LCU

75
정말 간단합니다. 축구 팀은 선수 명단입니까? 당신이 말하는 것처럼 다른 관련 속성이 있기 때문에 분명히 아닙니다. 축구 팀에는 선수 명단이 있습니까? 예, 목록이 있어야합니다. 그 외에도 컴포지션은 일반적으로 상속보다 선호됩니다. 나중에 수정하기가 쉽기 때문입니다. 상속과 컴포지션이 동시에 논리적 인 경우 컴포지션을 사용하십시오.
Tobberoth

45
@FlightOdyssey : 사각형을 취하고 높이를 반으로 줄이고 너비를 두 배로 늘리는 SquashRectangle 메소드가 있다고 가정하십시오. 이제 사각형 전달 발생 4 × 4를 수 있지만 형 구형의, 그리고 4 × 4 인 정사각형. SquashRectangle이 호출 된 후 객체의 크기는 얼마입니까? 사각형의 경우 분명히 2x8입니다. 정사각형이 2x8이면 더 이상 정사각형이 아닙니다. 2x8이 아닌 경우 동일한 모양에서 동일한 작업을 수행하면 다른 결과가 생성됩니다. 결론은 가변 사각형이 일종의 사각형이 아니라는 것입니다.
Eric Lippert

29
@ 강 너스 : 당신의 진술은 단순히 거짓입니다. 실제 서브 클래스는 수퍼 클래스의 수퍼 인 기능을 갖도록 요구된다 . (A)는 string모든 것을 할 필요 object할 수 있습니다 .
Eric Lippert

답변:


1564

여기에 좋은 답변이 있습니다. 나는 그들에게 다음과 같은 점을 덧붙일 것입니다.

데이터 구조를 나타내는 올바른 C # 방식은 무엇입니까? "논리적으로"(즉, "사람의 마음에") 몇 개의 종소리와 휘파람이있는 것의 목록 ​​일 뿐입니 까?

축구의 존재에 익숙한 컴퓨터 프로그래머가 아닌 10 명에게 빈 칸을 채우라고 요청하십시오.

축구 팀은 특별한 종류의 _____

셨습니까 사람의 말은 "몇 종과 경적을 가진 축구 선수의 목록", 또는 그들은 모두 말 "스포츠 팀"또는 "클럽"또는 "조직"를했다? 축구 팀이 특정 종류의 선수 목록 이라는 생각은 인간의 마음과 인간의 마음에만 있습니다.

List<T>A는 기구 . 풋볼 팀은 비즈니스 객체 , 즉 프로그램 의 비즈니스 영역 에있는 일부 개념을 나타내는 객체입니다 . 그것들을 섞지 마십시오! 축구 팀 은 일종의 팀입니다. 그것은 명단은 명단을 플레이어의 목록입니다 . 명단은 특정 종류의 선수 목록 이 아닙니다 . 명단 선수 목록입니다. 그래서라는 속성 수 있도록 Roster입니다 List<Player>. ReadOnlyList<Player>풋볼 팀에 대해 알고있는 모든 사람이 명단에서 선수를 삭제한다고 믿지 않는 한, 당신이있는 동안 그것을 만드십시오 .

상속이 List<T>항상 허용되지 않습니까?

누구에게 받아 들여지지 않습니까? 나를? 아니.

언제 받아 들일 수 있습니까?

당신이 메커니즘을 구축 할 때 확장 List<T>메커니즘을 .

상속 여부를 결정할 때 프로그래머가 고려해야 할 사항은 무엇입니까 List<T>?

나는 건물입니다 메커니즘 또는 비즈니스 오브젝트를 ?

그러나 그것은 많은 코드입니다! 그 모든 일을 위해 무엇을 얻습니까?

List<T>50 번 이상 관련 멤버를위한 전달 방법을 작성하는 데 걸리는 질문에 더 많은 시간을 투자 했습니다. 당신은 명백하게 자세한 것을 두려워하지 않으며, 우리는 여기서 아주 적은 양의 코드에 대해 이야기하고 있습니다; 이것은 몇 분 작업입니다.

최신 정보

나는 그것을 좀 더 생각했고 축구 팀을 선수 목록으로 모델링하지 않는 또 다른 이유가 있습니다. 실제로 축구 팀 을 선수 목록 도 가지고 있는 것으로 모델링하는 것은 좋지 않습니다 . 팀이 플레이어 목록을 가지고 있거나 가지고있는 문제는 당신이 가진 것이 순간 의 팀 스냅 샷 이라는 것 입니다. 이 클래스의 비즈니스 사례가 무엇인지 모르겠지만 축구 팀을 대표하는 클래스가 있다면 "2003 년에서 2013 년 사이에 부상으로 몇 명의 Seahawks 선수가 게임을 놓쳤습니까?"와 같은 질문을하고 싶습니다. 또는 "이전에 다른 팀에서 뛰었던 덴버 선수는 전년 대비 가장 많은 야드를 기록 했습니까?" 또는 " Piggers는 올해도 끝까지 갔습니까? "

즉, 풋볼 팀은 선수가 모집, 부상, 퇴직했을 때와 같은 역사적 사실의 모음 으로 잘 모델링 된 것 같습니다 . 분명히 현재 선수 명단은 아마도 전면 및-해야 할 중요한 사실입니다 보다 역사적인 관점을 필요로하는이 대상으로하고 싶은 다른 흥미로운 일이있을 수 있습니다.


84
다른 답변이 매우 도움이되었지만이 답변이 내 관심사를 가장 직접적으로 해결한다고 생각합니다. 코드의 양을 과장하는 것은 결국 그다지 효과가 없다는 것이 옳습니다.하지만 왜 코드를 포함하는지 이해하지 않고 프로그램에 일부 코드를 포함 시키면 매우 혼란스러워집니다.
Superbest

128
@Superbest : 기꺼이 도와 드리겠습니다! 그러한 의심을들을 권리가 있습니다. 왜 일부 코드를 작성하는지 이해하지 못하면 이해하거나 이해하지 못하는 다른 코드를 작성하십시오.
Eric Lippert

48
@Mehrdad 그러나 최적화의 황금률을 잊어 버리는 것 같습니다. 1)하지 말아야합니다. 2) 아직하지 마십시오. 3) 먼저 성능 프로파일을 수행하지 않고 최적화가 필요한 것을 보여주기 전에하지 마십시오.
AJMansfield

107
@Mehrdad : 솔직히 말해서, 애플리케이션이 가상 메소드의 성능 부담에 대해 신경 써야한다면, 현대 언어 (C #, Java 등)는 당신을위한 언어가 아닙니다. 저는 어린 시절에 동시에 플레이했던 모든 아케이드 게임을 시뮬레이션 할 수있는 랩탑을 가지고 있습니다 . 이를 통해 여기저기서 간접적 인 수준에 대해 걱정할 필요가 없습니다.
Eric Lippert

44
@Superbest : 이제 우리는 스펙트럼의 "구성이 상속되지 않음"에 확실히 도달했습니다. 로켓은 로켓 부품으로 구성 됩니다. 로켓은 "특별한 종류의 부품 목록"이 아닙니다! 나는 당신이 그것을 더 다듬을 것을 제안합니다. 왜 같은 반대 목록 IEnumerable<T>- 순서 ?
Eric Lippert

161

와우, 게시물에는 수많은 질문과 요점이 있습니다. Microsoft에서 얻은 대부분의 추론은 바로 그 시점에 있습니다. 에 대한 모든 것부터 시작합시다List<T>

  • List<T> 되어 최적화. 주요 사용법은 개체의 개인 구성원으로 사용됩니다.
  • 때로는 친숙한 이름을 가진 클래스를 만들고 싶을 수도 있기 때문에 Microsoft는 봉인하지 않았습니다 class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. 이제하는 것만 큼 쉽습니다 var list = new MyList<int, string>();.
  • CA1002 : 일반 목록을 공개하지 마십시오 . 기본적으로이 앱을 단독 개발자로 사용하려는 경우에도 좋은 코딩 방법을 사용하여 개발하는 것이 좋습니다. IList<T>소비자가 색인화 된 목록을 보유해야하는 경우 에도 목록을 노출로 표시 할 수 있습니다 . 이것으로 나중에 클래스 내에서 구현을 변경할 수 있습니다.
  • 마이크로 소프트는 Collection<T>일반적인 개념이기 때문에 매우 일반적으로 사용되었습니다. 그것은 단지 모음 일뿐입니다. 같은 더 정확한 버전이있다 SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>구현, 각각의 등 IList<T>하지 않음 List<T> .
  • Collection<T>구성원 (가상, 제거 등)이 가상이기 때문에 재정의 할 수 있습니다. List<T>하지 않습니다.
  • 당신의 질문의 마지막 부분은 자리에 있습니다. 풋볼 팀은 단순한 플레이어 목록 그 이상이므로 해당 플레이어 목록을 포함하는 클래스 여야합니다. 구성 대 상속을 생각하십시오 . 풋볼 팀 에는 선수 명단이 있습니다 (명단). 선수 명단은 아닙니다.

이 코드를 작성하는 경우 클래스는 다음과 같이 보일 것입니다.

public class FootballTeam
{
    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    // Yes. I used LINQ here. This is so I don't have to worry about
    // _roster.Length vs _roster.Count vs anything else.
    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

6
" 귀하의 게시물에는 수많은 질문이 있습니다 "-글쎄, 나는 완전히 혼란스러워했다. @Readonly의 질문에 연결해 주셔서 감사합니다. 이제 내 질문의 큰 부분이 더 일반적인 컴포지션과 상속 문제의 특별한 경우라는 것을 알았습니다.
Superbest

3
@Brian : 별칭을 사용하여 제네릭을 만들 수 없다는 점을 제외하고 모든 유형은 구체적이어야합니다. 내 예에서는 List<T>의 사용법과 선언을 단순화하기 위해 제네릭을 사용하는 개인 내부 클래스로 확장되었습니다 List<T>. 확장이 단순히이라면, class FootballRoster : List<FootballPlayer> { }대신 별칭을 사용할 수 있습니다. 추가 멤버 / 기능을 추가 할 수 있다는 점은 주목할 가치가 있지만의 중요한 멤버를 재정의 할 수 없으므로 제한적입니다 List<T>.

6
이것이 Programmers.SE 인 경우 현재 답변 투표 범위에 동의하지만 SO 게시물 이므로이 답변은 일반적인 C # (.NET) 관련 문제에 대한 답변을 시도합니다. 디자인 문제.
Mark Hurd

7
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden이제 이것이 List에서 상속하지 않는 좋은 이유입니다. 다른 답변에는 너무 많은 철학이 있습니다.
Navin

10
@my : 수줍음이 형사이기 때문에 당신이 질문의 "slew"를 의미한다고 생각합니다.
batty

152
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

이전 코드는 거리에서 축구를하는 많은 사람들을 의미하며 이름이 생깁니다. 다음과 같은 것 :

축구하는 사람들

어쨌든,이 코드 (내 대답에서)

public class FootballTeam
{
    // A team's name
    public string TeamName; 

    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

의미 : 이것은 관리, 선수, 관리자 등이있는 축구 팀입니다.

맨체스터 유나이티드 팀의 구성원 (및 기타 속성)을 보여주는 이미지

이것은 당신의 논리가 그림에 어떻게 표현되어 있는가…


9
두 번째 예는 축구 팀이 아니라 축구 클럽 일 것으로 기대합니다. 풋볼 클럽에는 관리자와 관리자 등 이 있습니다 . " 출처 : 풋볼 팀은 선수 그룹에 부여 된 총체적인 이름입니다 . 이러한 팀은 상대 팀과의 경기에서 풋볼 클럽을 대표하여 경기를하도록 선택할 수 있습니다. ... ". 따라서 팀 플레이어 목록 (또는 더 정확하게는 플레이어 모음)이라고 생각합니다.
Ben

3
더 최적화private readonly List<T> _roster = new List<T>(44);
SiD

2
System.Linq네임 스페이스 를 가져와야 합니다.
Davide Cannizzo

1
영상 : +1
V.7

115

이것은 구성상속 의 고전적인 예입니다 .

이 특정한 경우에 :

팀에 행동이 추가 된 선수 목록이 있습니까?

또는

팀이 플레이어 목록을 포함하는 자체 개체입니까?

List를 확장하면 여러 가지 방법으로 자신을 제한 할 수 있습니다.

  1. 액세스를 제한 할 수 없습니다 (예 : 명단 변경 사람들 중지). 모든 List 메소드는 필요한지 여부에 관계없이 얻을 수 있습니다.

  2. 다른 것들의 목록을 원한다면 어떻게됩니까? 예를 들어, 팀에는 코치, 관리자, 팬, 장비 등이 있습니다. 그 중 일부는 자신의 목록 일 수도 있습니다.

  3. 상속 옵션이 제한됩니다. 예를 들어 일반 Team 객체를 만든 다음 그로부터 상속받은 BaseballTeam, FootballTeam 등을 원할 수 있습니다. List에서 상속하려면 Team에서 상속을 수행해야하지만 이는 모든 다양한 유형의 팀이 해당 명부를 동일하게 구현해야 함을 의미합니다.

컴포지션-객체 내부에서 원하는 동작을 제공하는 객체를 포함합니다.

상속-개체가 원하는 동작을 가진 개체의 인스턴스가됩니다.

둘 다 용도가 있지만 구성이 바람직한 명확한 경우입니다.


1
# 2로 확장하면 List에 List가 있는 것이 의미가 없습니다 .
Cruncher

첫째, 질문은 구성 대 상속과 관련이 없습니다. 대답은 OP 가보다 구체적인 종류의 목록을 구현하고 싶지 않기 때문에 List <>를 확장해서는 안된다는 것입니다. 나는 당신이 stackoverflow에서 매우 높은 점수를 얻었고 원인을 명확하게 알고 사람들이 당신의 말을 신뢰해야하기 때문에 뒤틀 렸습니다. 시스템이지만 분명히 그렇지 않습니다! # no-rage
Mauro Sampietro

6
@sam 그는 목록의 동작을 원합니다. 그는 두 가지 선택을 할 수 있으며, 목록 (상속)을 확장하거나 객체 (구성) 내부에 목록을 포함 할 수 있습니다. 아마도 당신은 55 명의 사람들이 틀린 것이 아니라 질문이나 대답의 일부를 잘못 이해했을 것입니다. :)
Tim B

4
예. 슈퍼와 서브가 하나 뿐인 퇴보 사건이지만 나는 그의 구체적인 질문에 대해 더 일반적인 설명을하고 있습니다. 그는 하나의 팀만 가질 수 있으며 항상 하나의 목록을 가질 수 있지만 선택은 여전히 ​​목록을 상속하거나 내부 오브젝트로 포함하는 것입니다. 여러 유형의 팀 (축구, 크리켓 등)을 포함시키기 시작하고 플레이어 목록 이상이되기 시작하면 전체 구성과 상속 질문이 어떻게 나타나는지 알 수 있습니다. 큰 선택을 보는 것은 잘못된 선택을 피하기 위해 이와 같은 경우에 중요하며 나중에 많은 리팩토링을 의미합니다.
Tim B

3
OP는 작곡을 요구하지 않았지만 유스 케이스는 4 개의 객체 지향 프로그래밍 원칙 중 하나가 깨지거나 오용되거나 완전히 이해되지 않은 X : Y 문제의 명확한 예입니다. 더 명확한 대답은 스택 또는 대기열과 같은 특수 컬렉션을 작성하거나 (사용 사례에 맞지 않는 것으로 보임) 구성을 이해하는 것입니다. 축구 팀은 축구 선수 목록이 아닙니다. 영업 이익이 요구 여부를 명시 적으로 여부를 그들이 대답을 이해하지 못할 추상화, 캡슐화, 상속을 이해하지 않고, 무관하며, 다형성, 에고, X : Y는 닥치는
줄리아 맥기

67

모두가 지적했듯이, 플레이어 팀은 플레이어 목록이 아닙니다. 이 실수는 다양한 수준의 전문 지식을 가진 곳곳의 많은 사람들에 의해 이루어집니다. 이 경우와 같이 종종 문제는 미묘하고 때로는 매우 심각합니다. 이러한 설계는 Liskov 대체 원칙을 위반하기 때문에 좋지 않습니다 . 인터넷에는이 개념을 설명하는 좋은 기사가 많이 있습니다. 예 : http://en.wikipedia.org/wiki/Liskov_substitution_principle

요약하면, 클래스 간 부모 / 자식 관계에서 유지해야 할 두 가지 규칙이 있습니다.

  • 아동은 부모를 완전히 정의하는 것보다 적은 특성을 요구해서는 안됩니다.
  • 부모는 아동을 완전히 정의하는 것 외에 특성이 필요하지 않아야합니다.

다시 말해, 부모는 자녀에 대한 필수 정의이고, 자녀는 부모에 대한 충분한 정의입니다.

여기에 솔루션을 통해 생각하고 그러한 실수를 피하는 데 도움이되는 위의 원칙을 적용하는 방법이 있습니다. 부모 클래스의 모든 작업이 파생 클래스에 대해 구조적 및 의미 적으로 유효한지 확인하여 가설을 테스트해야합니다.

  • 축구 팀은 축구 선수 목록입니까? (리스트의 모든 속성이 같은 의미로 팀에 적용됩니까)
    • 팀은 동종 단체의 집합입니까? 예, 팀은 플레이어의 모음입니다
    • 플레이어 포함 순서는 팀의 상태를 설명하고 팀은 명시 적으로 변경되지 않는 한 순서가 유지되도록 보장합니까? 아니, 아니
    • 팀의 순서에 따라 선수를 포함 / 삭제해야합니까? 아니

보시다시피, 목록의 첫 번째 특성 만 팀에 적용됩니다. 따라서 팀은 목록이 아닙니다. 목록은 팀을 관리하는 방법에 대한 구현 세부 사항이므로 플레이어 개체를 저장하고 Team 클래스의 메서드로 조작하는 데만 사용해야합니다.

이 시점에서 나는 Team 클래스가 List를 사용하여 구현되어서는 안된다고 말하고 싶습니다. 대부분의 경우 Set 데이터 구조 (예 : HashSet)를 사용하여 구현해야합니다.


8
List와 Set을 잘 잡습니다. 그것은 너무 일반적으로 만들어진 오류 인 것 같습니다. 컬렉션에 요소의 인스턴스가 하나만있을 수있는 경우, 일종의 세트 또는 사전이 구현에 선호되는 후보 여야합니다. 같은 이름의 두 선수로 구성된 팀을 갖는 것은 괜찮습니다. 한 명의 플레이어를 동시에 두 번 포함시키는 것은 좋지 않습니다.
Fred Mitchell

1
"데이터 구조가 유용한 것보다 더 많은 일을 하는가"보다는 "데이터 구조가 유용한 모든 것 (이상적으로는 장 단기적으로 지원할 수 있는가)"을 묻는 것이 더 중요하지만 좋은 점은 +1입니다.
Tony Delroy 2013

1
@TonyD 글쎄, 내가 제기하는 요점은 "데이터 구조가 유용한 것 이상을 수행하는지"를 확인해야한다는 것이 아닙니다. "부모 데이터 구조가 Child 클래스가 의미하는 동작과 관련이 없거나 의미가 없거나 반 직관적 인 작업을 수행하는지 확인"입니다.
Satyan Raina

1
@TonyD 실제로 많은 경우에 부정적인 테스트에 실패 할 것이므로 부모로부터 파생 된 관련없는 특성을 갖는 데 문제가 있습니다. 프로그래머는 Human {eat (); 운영(); 고릴라의 write ();} {eat (); 운영(); swing ();} 스윙 할 수있는 추가 기능이있는 인간에게는 아무런 문제가 없다고 생각합니다. 그런 다음 게임 세계에서 인간은 갑자기 나무 위로 스윙하여 예상되는 모든 장애물을 우회하기 시작합니다. 명시 적으로 명시되지 않는 한 실제적인 인간은 흔들리지 않아야합니다. 그러한 디자인은 API를 남용과 혼란에 매우 개방되게합니다
Satyan Raina

1
@TonyD Player 클래스가 HashSet에서 파생되어야한다고 제안하지는 않습니다. Player 클래스는 컴포지션을 통해 HashSet을 사용하여 '대부분의 경우'를 구현해야하며 이는 디자인 수준이 아니라 구현 수준 세부 사항입니다 (그래서 필자가 대답에 대한 주석으로 언급 한 이유). 그러한 구현에 대한 정당성이 정당하다면 목록을 사용하여 구현할 수 있습니다. 따라서 귀하의 질문에 대답하려면 키로 O (1) 조회가 필요합니까? 따라서 HashSet에서 플레이어를 확장해서는 안됩니다.
Satyan Raina

50

FootballTeam예비 팀에 주 팀과 함께 예비 팀이 있으면 어떻게 됩니까?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
}

그걸 어떻게 모델링 하시겠습니까?

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

관계는 명확하게 A가 들어 하지 입니다 .

또는 RetiredPlayers?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
    List<FootballPlayer> RetiredPlayers { get; set; }
}

일반적으로 컬렉션에서 상속하려면 클래스 이름을 지정하십시오 SomethingCollection.

당신이 있습니까 SomethingCollection의미 이해? 유형 이의 모음 인 경우에만이 작업을 수행하십시오 Something.

이 경우 FootballTeam제대로 들리지 않습니다. A Team는 이상 Collection입니다. A는 Team다른 답변이 지적한대로 등 코치, 트레이너를 가질 수 있습니다.

FootballCollection축구 모음 또는 축구 도구 모음처럼 들립니다. TeamCollection팀의 모음입니다.

FootballPlayerCollectionList<FootballPlayer>당신이 정말로 그것을 원한다면 상속받은 클래스의 유효한 이름이 될 플레이어 모음처럼 들립니다 .

정말 List<FootballPlayer>완벽하게 다루는 유형입니다. 아마도 IList<FootballPlayer>당신이 메소드에서 그것을 반환한다면.

요약하자면

자신에게 물어

  1. X a는 Y? 또는 가지고 X A는 Y?

  2. 수업 이름이 무엇인가요?


모든 플레이어의 종류 중 하나에 속하는으로 분류 할 경우 DefensivePlayers, OffensivePlayers또는 OtherPlayers기대 코드에 의해 사용될 수있는 유형 가지고, 그것은 합법적으로 유용 할 수 있습니다 List<Player>뿐만 아니라 포함 구성원 DefensivePlayers, OffsensivePlayers또는 SpecialPlayers유형 IList<DefensivePlayer>, IList<OffensivePlayer>등을 IList<Player>. 별도의 객체를 사용하여 별도의 목록을 캐시 할 수 있지만 기본 목록과 동일한 객체 내에 캡슐화하는 것이 더 깔끔해 보일 것입니다. [목록 열거 자의 무효화 사용 ...
supercat

... 기본 목록이 변경되었고 다음에 액세스 할 때 하위 목록을 다시 생성해야한다는 사실에 대한 신호로].
supercat

2
내가 만든 요점에 동의하는 동안 누군가가 디자인 조언을하고 비즈니스 객체에서 공개 getter 및 setter로 구체적인 List <T> 노출을 제안하는 것을 보는 것은 정말로 영혼을 찢어
버립니다.

38

디자인> 구현

어떤 방법과 속성이 노출되는지는 디자인 결정입니다. 상속받은 기본 클래스는 구현 세부 사항입니다. 전 단계로 돌아가는 것이 가치가 있다고 생각합니다.

객체는 데이터와 행동의 모음입니다.

따라서 첫 번째 질문은 다음과 같아야합니다.

  • 이 개체는 내가 만드는 모델에서 어떤 데이터를 구성합니까?
  • 이 객체는 해당 모델에서 어떤 동작을 보입니까?
  • 미래에는 어떻게 변할까요?

상속은 "isa"(is) 관계를 의미하는 반면, composition은 "has"(hasa) 관계를 의미합니다. 응용 프로그램의 발전에 따라 상황이 달라질 수 있다는 점을 염두에두고보기에 상황에 맞는 것을 선택하십시오.

어떤 사람들은 두뇌를 "디자인 모드"에 놓는 것이 더 쉽다는 것을 알기 때문에 구체적인 유형으로 생각하기 전에 인터페이스를 생각하는 것을 고려하십시오.

이것은 일상적으로 코딩 할 때이 수준에서 모든 사람이 의식적으로하는 것이 아닙니다. 그러나 이런 종류의 주제를 궁리하고 있다면 디자인 수역을 밟고있는 것입니다. 그것을 알고 있으면 해방 될 수 있습니다.

설계 특성 고려

MSDN 또는 Visual Studio에서 List <T> 및 IList <T>를 살펴보십시오. 그들이 어떤 방법과 속성을 노출하는지 확인하십시오. 이 방법들이 모두 누군가가 당신의 관점에서 FootballTeam에하고 싶은 것처럼 보입니까?

footballTeam.Reverse ()는 당신에게 의미가 있습니까? footballTeam.ConvertAll <TOutput> ()이 원하는 것입니까?

이것은 속임수가 아닙니다. 대답은 "예"일 수 있습니다. List <Player> 또는 IList <Player>를 구현 / 상속하면이 문제가 발생합니다. 그것이 모델에 이상적이라면 그렇게하십시오.

당신이 그렇다고 생각한다면, 그것은 말이되고, 당신의 객체가 플레이어들의 행동 / 모음으로 취급 될 수 있기를 원하기 때문에, 당신은 반드시 ICollection 또는 IList를 구현하기를 원합니다. 명목상 :

class FootballTeam : ... ICollection<Player>
{
    ...
}

객체에 플레이어의 컬렉션 / 목록 (데이터)이 포함되도록하려는 경우 컬렉션 또는 목록이 속성 또는 멤버가되기를 원하는 경우 반드시 그렇게하십시오. 명목상 :

class FootballTeam ...
{
    public ICollection<Player> Players { get { ... } }
}

사람들이 플레이어 수를 열거하거나 추가하거나 제거하는 대신 플레이어 세트 만 열거 할 수 있기를 원할 수도 있습니다. IEnumerable <Player>는 고려해야 할 완벽하게 유효한 옵션입니다.

이러한 인터페이스 중 어느 것도 모델에 전혀 유용하지 않다고 생각할 수도 있습니다. 이것은 가능하지 않지만 (IEnumerable <T>는 많은 상황에서 유용합니다) 여전히 가능합니다.

이 중 하나가 모든 경우에 범주 및 결정적으로 잘못되었다고 말하려는 사람 은 잘못 안내됩니다. 당신에게 그것을 말하려고 시도하는 사람 은 모든 경우에 범주 적으로 그리고 결정적으로 습니다.

구현으로 이동

데이터와 행동을 결정한 후에는 구현에 대한 결정을 내릴 수 있습니다. 여기에는 상속 또는 구성을 통해 의존하는 구체적인 클래스가 포함됩니다.

이것은 큰 걸음이 아닐 수 있으며 사람들은 종종 디자인과 구현을 혼동하기 때문에 머리에서 두세 초 만에 완전히 입력하고 입력을 시작할 수 있기 때문에 가능합니다.

생각 실험

인공적인 예 : 다른 사람들이 언급했듯이 팀은 항상 플레이어의 모음 일뿐입니다. 팀의 경기 점수 수집을 유지합니까? 귀하의 모델에서 팀이 클럽과 교환이 가능합니까? 그렇다면 팀이 플레이어 모음 인 경우 직원 모음 및 / 또는 점수 모음 일 수도 있습니다. 그런 다음 다음과 같이 끝납니다.

class FootballTeam : ... ICollection<Player>, 
                         ICollection<StaffMember>,
                         ICollection<Score>
{
    ....
}

그럼에도 불구하고 디자인은 C #에서이 시점에서 List <T>에서 상속하여이 모든 것을 구현할 수는 없습니다. C # "only"는 단일 상속 만 지원하기 때문입니다. (당신이 C에서이 malarky을 시도한 경우 ++,이 좋은 것을 고려할 수 있습니다.) 구성을 통해 하나 개의 상속을 통해 수집 및 하나를 구현하는 것입니다 느낌이 더러운. 또한 ILIst <Player> .Count 및 IList <StaffMember> .Count 등을 명시 적으로 구현하지 않으면 Count와 같은 속성이 사용자에게 혼란스러워지고 혼란 스럽기만하면 고통 스럽습니다. 어디로 가는지 알 수 있습니다. 이 길을 생각하는 동안 직감은이 방향으로 향하는 것이 잘못되었다고 말할 수도 있습니다 (올바르거나 잘못하면, 당신이 이런 식으로 구현하면 동료가 할 수도 있습니다!)

짧은 대답 (너무 늦음)

컬렉션 클래스에서 상속하지 않는 것에 대한 지침은 C #에 국한되지 않으며 많은 프로그래밍 언어에서 찾을 수 있습니다. 그것은 법이 아닌 지혜를받습니다. 실제로 컴포지션은 이해, 구현 및 유지 관리 측면에서 상속을이기는 것으로 간주되기 때문입니다. 실제 세계 / 도메인 객체에서는 일반적으로, 특히 시간이 지나고 코드에서 객체의 정확한 데이터와 동작이 깊지 않은 한 유용하고 일관된 "사"관계보다 유용하고 일관된 "하사"관계를 찾는 것이 더 일반적입니다 변화. 컬렉션 클래스에서 상속을 항상 배제하지는 않습니다. 그러나 암시 할 수 있습니다.


34

우선, 유용성과 관련이 있습니다. 상속을 사용하면 Team클래스는 객체 조작을 위해 설계된 동작 (방법)을 노출합니다. 예를 들어, AsReadOnly()또는 CopyTo(obj)방법은 팀 객체에 대한 의미가 없습니다. AddRange(items)방법 대신 더 설명적인 AddPlayers(players)방법을 원할 것입니다 .

LINQ를 사용하려는 경우 ICollection<T>또는 IEnumerable<T>보다 일반적인 인터페이스를 구현하십시오 .

언급했듯이 구성은 올바른 방법입니다. 플레이어 목록을 개인 변수로 구현하십시오.


30

축구 팀 축구 선수 목록 이 아닙니다 . 축구 팀 축구 선수 목록으로 !

논리적으로 잘못되었습니다.

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

그리고 이것은 맞습니다 :

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}

19
왜 그런지 설명하지 않습니다 . OP는 대부분의 다른 프로그래머가 이런 식으로 느끼는 것을 알고 있으며 왜 중요한지 이해하지 못합니다. 이것은 실제로 질문에 이미있는 정보 만 제공합니다.
Servy

2
이것은 내가 생각한 첫 번째 것입니다. 그의 데이터가 잘못 모델링 된 방법. 따라서 그 이유는 데이터 모델이 올바르지 않습니다.
rball

3
왜 목록에서 상속하지 않습니까? 그는 더 구체적인 종류의 목록을 원하지 않기 때문입니다.
Mauro Sampietro

2
두 번째 예가 맞지 않다고 생각합니다. 상태가 노출되어 캡슐화가 중단됩니다. 상태는 객체에 숨겨져 있거나 내부에 있어야하며이 상태를 변경하는 방법은 공용 메서드를 통해 이루어져야합니다. 정확히 어떤 방법이 비즈니스 요구 사항에서 결정되어야하는지, 그러나 "지금 당장 필요로하는"기반이되어야합니다. API를 단단히 유지하면 시간과 노력을 절약 할 수 있습니다.
sara sep

1
캡슐화는 문제의 핵심이 아닙니다. 유형이 더 구체적인 방식으로 작동하지 않게하려면 유형을 확장하지 않는 데 중점을 둡니다. 이러한 필드는 다른 언어로 된 c # 및 get / set 메소드의
자동 속성이어야

28

상황에 따라 다릅니다

팀을 선수 목록으로 생각하면 풋볼 팀의 "아이디어"를 한 측면으로 투사합니다. "팀"을 현장에서 보는 사람들에게 줄입니다. 이 투영은 특정 상황에서만 정확합니다. 다른 상황에서는 이것이 완전히 잘못되었을 수 있습니다. 팀의 후원자가되고 싶다고 상상해보십시오. 따라서 팀 관리자와 대화해야합니다. 이러한 맥락에서 팀은 관리자 목록에 투영됩니다. 이 두 목록은 일반적으로 겹치지 않습니다. 다른 상황은 현재 대 이전 플레이어 등입니다.

불명확 한 의미

따라서 팀을 플레이어 목록으로 고려할 때의 문제는 의미가 컨텍스트에 따라 다르며 컨텍스트가 변경되면 확장 할 수 없다는 것입니다. 또한 어떤 컨텍스트를 사용하고 있는지 표현하기가 어렵습니다.

수업은 확장 가능

멤버가 하나만있는 클래스 (예 :) IList activePlayers를 사용하는 경우 멤버 이름 (및 주석)을 사용하여 컨텍스트를 명확하게 할 수 있습니다. 추가 컨텍스트가 있으면 추가 멤버를 추가하기 만하면됩니다.

수업은 더 복잡합니다

경우에 따라 추가 클래스를 작성하는 것이 과도 할 수 있습니다. 각 클래스 정의는 클래스 로더를 통해로드되어야하며 가상 머신에 의해 캐시됩니다. 런타임 성능과 메모리 비용이 발생합니다. 매우 구체적인 상황이 있으면 축구 팀을 선수 목록으로 간주하는 것이 좋습니다. 그러나이 경우 실제로 IList 파생 된 클래스가 아닌을 사용해야 합니다.

결론 / 고려

매우 구체적인 배경이 있다면 팀을 플레이어 목록으로 간주해도됩니다. 예를 들어 메소드 내부에서는 다음과 같이 작성해도됩니다.

IList<Player> footballTeam = ...

F #을 사용할 때 형식 약어를 작성해도됩니다.

type FootballTeam = IList<Player>

그러나 상황이 더 넓거나 불분명 한 경우에는 그렇게하지 않아야합니다. 미래에 사용될 컨텍스트가 분명하지 않은 새 클래스를 만들 때 특히 그렇습니다. 경고 표시는 클래스 (팀, 코치 등)에 속성을 추가 할 때 나타납니다. 이것은 클래스가 사용될 컨텍스트가 고정되어 있지 않으며 앞으로 변경 될 것이라는 명확한 신호입니다. 이 경우에는 팀을 선수 목록으로 간주 할 수 없지만 (현재 활동 중이거나 다 치지 않은 등) 선수 목록을 팀의 속성으로 모델링해야합니다.


27

질문을 다시 작성하겠습니다. 다른 관점에서 피사체를 볼 수 있습니다.

축구 대표팀을 대표해야 할 때는 기본적으로 이름이라는 것을 이해합니다. "독수리"

string team = new string();

나중에 팀에 선수가 있다는 것을 깨달았습니다.

왜 플레이어 유형을 보유하도록 문자열 유형을 확장 할 수 없습니까?

문제에 대한 당신의 입장은 임의적입니다. 팀이 무엇을 생각하는 시도 그렇지 않은 것 (특성) 입니다 .

그 후에 다른 클래스와 속성을 공유하는지 확인할 수 있습니다. 그리고 상속에 대해 생각하십시오.


1
좋은 지적입니다. 팀을 단순한 이름으로 생각할 수 있습니다. 그러나 내 응용 프로그램이 플레이어의 행동으로 작업하는 것을 목표로한다면 그 생각은 조금 덜 분명합니다. 어쨌든 문제는 결국 구성 대 상속에 관한 것 같습니다.
Superbest

6
부주의 한 사람은 여러 축구 팀을 소유하고, 친구에게 선물로 친구를주고, 친구는 팀의 이름을 바꾸고, 코치를 해고합니다. 모든 플레이어를 대체합니다. 친구 팀은 녹색에서 남자 팀을 만나고 남자 팀이 패배하면서 "내가 준 팀으로 나를 때리고 있다고 믿을 수 없다!" 남자가 맞습니까? 이것을 어떻게 확인 하시겠습니까?
user1852503

20

축구 팀이 "is-a" List<FootballPlayer>인지 "has-a" 인지에 대한 다른 답이 거의 없다고 생각하기 때문에List<FootballPlayer> 없다고 생각하기 때문에 실제로이 질문에 대한 답은 서면으로 답변하지 않습니다.

OP는 주로 List<T>다음 으로부터 상속 지침을 명확하게 요구합니다 .

가이드 라인에 따르면 상속하지 말아야합니다 List<T>. 왜 안돼?

때문에 List<T>어떤 가상 방법이 없습니다. 일반적으로 비교적 적은 고통으로 구현을 전환 할 수 있지만 공개 API에서는 훨씬 더 큰 거래가 될 수 있기 때문에 이것은 자체 코드에서 문제가되지 않습니다.

공개 API 란 무엇이며 왜 관심을 가져야합니까?

공개 API는 타사 프로그래머에게 노출되는 인터페이스입니다. 프레임 워크 코드를 생각하십시오. 참조되는 지침은 ".NET 응용 프로그램 디자인 지침" 이 아니라 ".NET Framework 디자인 지침" 이라는 것을 기억하십시오 . 차이점이 있으며 일반적으로 퍼블릭 API 디자인은 훨씬 엄격합니다.

현재 프로젝트에이 공용 API가 없거나 포함되어 있지 않을 경우이 지침을 무시해도됩니까? List에서 상속 받고 공개 API가 필요한 것으로 밝혀지면 어떤 어려움이 있습니까?

그래요 어쨌든 상황에 적용되는지 확인하기위한 배후의 이론적 근거를 고려할 수도 있지만 공개 API를 작성하지 않는 경우 버전 관리와 같은 API 문제에 대해 특별히 걱정할 필요가 없습니다 (이 중 하나는 하위 집합).

향후 공개 API를 추가하는 경우 구현에서 API를 추상화하거나 ( List<T>직접 노출하지 않음 ) 향후 발생할 수있는 고통으로 가이드 라인을 위반해야합니다.

왜 중요합니까? 리스트는리스트입니다. 무엇이 변경 될 수 있습니까? 무엇을 바꾸고 싶습니까?

상황에 따라 다르지만 우리는 FootballTeam예제로 사용 하고 FootballPlayer있기 때문에 팀이 급여 상한을 넘을 수 있다면 if를 추가 할 수 없다고 상상하십시오 . 추가하는 가능한 방법은 다음과 같습니다.

 class FootballTeam : List<FootballPlayer> {
     override void Add(FootballPlayer player) {
        if (this.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
 }

아 ...하지만 당신은하지 override Add않기 때문에 할 수 없습니다virtual 성능상의 이유로 .

응용 프로그램에 있으면 (기본적으로 사용자와 모든 발신자가 함께 컴파일됨을 의미 함) 이제 IList<T>컴파일 오류 를 사용 하고 수정 하도록 변경할 수 있습니다.

 class FootballTeam : IList<FootballPlayer> {
     private List<FootballPlayer> Players { get; set; }

     override void Add(FootballPlayer player) {
        if (this.Players.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
     /* boiler plate for rest of IList */
 }

그러나 공개적으로 타사에 노출 된 경우 컴파일 및 / 또는 런타임 오류가 발생하는 주요 변경 사항을 만들었습니다.

TL; DR-가이드 라인은 퍼블릭 API를위한 것입니다. 개인 API의 경우 원하는 작업을 수행하십시오.


18

사람들이 말할 수 있도록

myTeam.subList(3, 5);

전혀 이해가되지? 그렇지 않으면 목록이 아니어야합니다.


3
당신이 그것을라는 경우는 수도myTeam.subTeam(3, 5);
샘 리치를

3
@SamLeach 그것이 사실이라고 가정하면 여전히 상속이 아닌 구성이 필요합니다. 으로는 subList도를 반환하지 않습니다 Team더 이상.
Cruncher

1
이것은 다른 어떤 대답보다 저를 설득했습니다. +1
FireCubez

16

"팀"개체 의 동작 에 따라 다릅니다 . 컬렉션처럼 동작한다면 먼저 일반 List로 나타내는 것이 좋습니다. 그런 다음 목록에서 반복되는 코드가 계속 복제됨을 알 수 있습니다. 이 시점에서 플레이어 목록을 감싸는 FootballTeam 객체를 생성 할 수 있습니다. FootballTeam 클래스는 플레이어 목록에서 반복되는 모든 코드의 홈이됩니다.

내 코드를 불필요하게 장황하게 만듭니다. 이제 my_team.Count 대신 my_team.Players.Count를 호출해야합니다. 고맙게도 C #을 사용하면 인덱서를 정의하여 인덱싱을 투명하게 만들고 내부 List의 모든 메소드를 전달할 수 있습니다 ...하지만 많은 코드입니다! 그 모든 일을 위해 무엇을 얻습니까?

캡슐화. 고객은 FootballTeam 내부에서 어떤 일이 일어나고 있는지 알 필요가 없습니다. 모든 고객에게 데이터베이스에서 플레이어 목록을 찾아서 구현할 수 있습니다. 알 필요가 없으므로 디자인이 향상됩니다.

그냥 평범하지 않습니다. 풋볼 팀은 선수 목록을 가지고 있지 않습니다. 플레이어 목록입니다. "John McFootballer가 SomeTeam의 플레이어에 합류했습니다"라고 말하지 않습니다. "John이 SomeTeam에 가입했습니다"라고 말합니다. "문자열 문자"에 문자를 추가하지 않고 문자열에 문자를 추가합니다. 책을 도서관의 책에 추가하지 않고 책을 도서관에 추가합니다.

정확히 :) 당신은 footballTeam.List.Add (john)가 아니라 footballTeam.Add (john)이라고 말할 것입니다. 내부 목록이 보이지 않습니다.


1
OP는 MODEL REALITY를 이해하는 방법을 알고 싶지만 축구 팀을 축구 선수 목록으로 정의합니다 (개념적으로 잘못된). 이게 문제 야. 다른 의견은 내 의견으로는 그를 잘못 인도하고 있습니다.
Mauro Sampietro

3
나는 선수 명단이 개념 상 틀렸다는 것에 동의하지 않는다. 반드시 그런 것은 아닙니다. 이 페이지에서 다른 사람이 쓴 것처럼 "모든 모델은 잘못되었지만 일부는 유용합니다"
xpmatteo

올바른 모델은 얻기가 어렵고 일단 완료되면 유용한 모델입니다. 모델을 잘못 사용할 수 있으면 개념적으로 잘못되었습니다. stackoverflow.com/a/21706411/711061
Mauro Sampietro

15

여기에는 많은 훌륭한 답변이 있지만 언급하지 않은 것을 만져보고 싶습니다. 객체 지향 디자인은 객체힘을 실어주는 것입니다 .

모든 규칙, 추가 작업 및 내부 세부 정보를 적절한 개체 안에 캡슐화하려고합니다. 이런 식으로이 개체와 상호 작용하는 다른 개체는 모든 것을 걱정할 필요가 없습니다. 실제로 한 단계 더 나아가 다른 객체가 이러한 내부를 우회하는 것을 적극적으로 방지 하려고합니다 .

에서 상속하면 List다른 모든 객체가 사용자를 목록으로 볼 수 있습니다. 그들은 플레이어를 추가하고 제거하는 방법에 직접 액세스 할 수 있습니다. 그리고 당신은 당신의 통제력을 잃어 버릴 것입니다; 예를 들면 다음과 같습니다.

퇴직, 사임 또는 해고 여부를 알고 플레이어가 퇴장 할 때 차별화하려고한다고 가정합니다. RemovePlayer적절한 입력 열거 형을 취하는 메소드를 구현할 수 있습니다. 그러나 상속에 의해 List, 당신은 직접 액세스 방지 할 수없는 것 Remove, RemoveAll심지어는과 Clear. 결과적으로, 당신은 실제로 당신의 수업을 무력화 시켰습니다FootballTeam .


캡슐화에 대한 추가 생각 ... 다음과 같은 문제가 발생했습니다.

내 코드를 불필요하게 장황하게 만듭니다. 이제 my_team.Count 대신 my_team.Players.Count를 호출해야합니다.

맞습니다. 모든 고객이 귀하의 팀을 사용하는 것은 불필요하게 장황합니다. 그러나이 문제는 당신이 List Players모든 것에 노출 되어 있다는 사실과 비교할 때 매우 작기 때문에 귀하의 동의없이 팀과 함께 바이올린을 칠 수 있습니다.

당신은 말을 계속 :

그냥 평범하지 않습니다. 풋볼 팀은 선수 목록을 가지고 있지 않습니다. 플레이어 목록입니다. "John McFootballer가 SomeTeam의 플레이어에 합류했습니다"라고 말하지 않습니다. "John이 SomeTeam에 가입했습니다"라고 말합니다.

당신은 첫 번째 비트에 대해 틀 렸습니다 : '목록'이라는 단어를 버리고 팀에 선수가 있다는 것이 분명합니다.
그러나 두 번째로 머리에 못을 박았습니다. 고객이 전화하는 것을 원하지 않습니다 ateam.Players.Add(...). 당신은 그들이 전화를 원합니다 ateam.AddPlayer(...). 그리고 당신의 이행은 (다른 것들 중에서도) Players.Add(...)내부적으로 부를 것 입니다.


바라건대 당신은 캡슐화가 객체를 강화하는 목표에 얼마나 중요한지 알 수 있기를 바랍니다. 각 클래스가 다른 객체의 간섭에 대한 두려움없이 잘 작동하도록하려고합니다.


12

데이터 구조를 나타내는 올바른 C # 방법은 무엇입니까 ?

"모든 모델은 잘못되었지만 일부는 유용합니다." - 조지 EP 박스

"올바른 방법"은 없으며 유용한 방법 만 있습니다.

귀하 및 / 또는 사용자에게 유용한 것을 선택하십시오. 그게 다야. 경제적으로 개발하고 과도하게 엔지니어링하지 마십시오. 적은 코드를 작성할수록 더 적은 코드를 디버깅해야합니다. (다음 판을 읽으십시오).

-편집

내 최선의 대답은 ... 그것은 달려 있습니다. FootballTeam은 비즈니스 엔티티처럼 보이기 때문에 목록에서 상속하면이 클래스의 클라이언트가 노출되지 않아야하는 메소드에 노출됩니다.

-에디션 2

“엔지니어링하지 말아라”라는 의견을 언급 한 것을 진심으로 기억하지 않습니다. KISS 사고 방식이 좋은 가이드 라고 생각하지만 List에서 비즈니스 클래스를 상속하면 추상화 누출 로 인해 해결하는 것보다 더 많은 문제가 발생할 수 있음을 강조하고 싶습니다 .

반면에 List에서 상속하는 것이 유용한 경우는 제한적이라고 생각합니다. 이전 판에서 썼 듯이 그것은 다릅니다. 각 사례에 대한 답변은 지식, 경험 및 개인적 선호에 의해 크게 영향을받습니다.

답변에 대해 더 정확하게 생각하도록 도와 준 @kai에게 감사합니다.


내 최선의 대답은 ... 그것은 달려 있습니다. FootballTeam은 비즈니스 엔티티처럼 보이기 때문에 목록에서 상속하면이 클래스의 클라이언트가 노출되지 않아야하는 메소드에 노출됩니다. 이 답변을 수정하거나 다른 아이디어를 게시 해야할지 모르겠습니다.
ArturoTena

2
"에 대한 비트 A로부터 상속하는 List노출되어서는 안 될 수있다 방법이 클래스의 클라이언트에 노출 될 수는 "상속하지하게 무엇을 정확하게 List절대 노 더. 일단 노출되면 시간이 지남에 따라 점차 학대와 오용이 발생합니다. "경제적으로 개발"함으로써 시간을 절약하면 남용을 디버깅하고 결국에는 상속을 수정하기 위해 리팩토링을함으로써 미래에 잃어버린 비용을 10 배나 쉽게 줄일 수 있습니다. YAGNI의 원칙은 다음과 같은 의미로도 생각할 수 있습니다. 귀하는 오지 않을 것입니다.의 모든 방법이 필요 List하므로 노출하지 마십시오.
환멸

3
"엔지니어링하지 마십시오. 작성하는 코드가 적을수록 디버깅에 필요한 코드가 줄어 듭니다." <-이것이 오도라고 생각합니다. 상속에 대한 캡슐화 및 컴포지션은 오버 엔지니어링이 아니며 디버깅이 더 필요하지 않습니다. 캡슐화를 통해 클라이언트가 클래스를 사용하고 남용 할 수있는 방법의 수를 제한하므로 테스트 및 입력 검증이 필요한 진입 점이 줄어 듭니다. List빠르고 쉬운 버그로 이어질 수 있기 때문에 상속하는 것은 명백한 잘못이며, 나쁜 설계 일 뿐이며 나쁜 설계는 "과잉 엔지니어링"보다 훨씬 많은 버그로 이어집니다.
sara sep

@kai 나는 모든 점에서 당신과 동의합니다. “엔지니어링하지 말아라”라는 의견을 언급 한 것을 진심으로 기억하지 않습니다. OTOH, 나는 List에서 상속하는 것이 유용한 경우가 제한적이라고 생각합니다. 나중 판에서 썼 듯이, 그것은 다릅니다. 각 사례에 대한 답변은 지식, 경험 및 개인적 선호에 의해 크게 영향을받습니다. 인생의 모든 것처럼. ;-)
ArturoTena

11

이것은 "Is a"대 "has a"트레이드 오프를 생각 나게합니다. 때로는 수퍼 클래스에서 직접 상속하는 것이 더 쉽고 더 의미가 있습니다. 때로는 독립형 클래스를 만들고 멤버 변수로 상속받은 클래스를 포함시키는 것이 더 합리적입니다. 클래스의 기능에 계속 액세스 할 수 있지만 클래스에서 상속 될 수있는 인터페이스 또는 기타 제한 조건에는 바인드되지 않습니다.

어느 쪽을합니까? 많은 것들과 마찬가지로 ... 그것은 상황에 달려 있습니다. 내가 사용할 가이드는 다른 클래스에서 상속 받으려면 "is"관계가 있어야한다는 것입니다. 따라서 BMW라는 클래스를 작성하는 경우 BMW는 실제로 자동차이기 때문에 자동차에서 상속받을 수 있습니다. Horse 클래스는 Mammal 클래스에서 상속받을 수 있습니다. 말은 실제로는 실제 포유 동물이므로 모든 Mammal 기능은 Horse와 관련이 있어야합니다. 그러나 팀이 목록이라고 말할 수 있습니까? 내가 알 수 있듯이 팀이 실제로 "목록"인 것처럼 보이지 않습니다. 따라서이 경우에는 List를 멤버 변수로 사용합니다.


9

나의 더러운 비밀 : 나는 사람들이하는 말에 신경 쓰지 않고 그것을한다. .NET Framework는 "XxxxCollection"(제 머리 위에 UIElementCollection)가 포함되어 있습니다.

그래서 내가 말하는 것을 멈추게하는 것 :

team.Players.ByName("Nicolas")

내가 그것을 더 잘 찾을 때

team.ByName("Nicolas")

또한 내 PlayerCollection은 코드 복제없이 "Club"과 같은 다른 클래스에서 사용될 수 있습니다.

club.Players.ByName("Nicolas")

어제의 모범 사례는 내일이 아닐 수도 있습니다. 대부분의 모범 사례 뒤에는 이유가 없으며 대부분 커뮤니티간에 광범위한 동의 만 있습니다. 스스로에게 물어볼 때 자신을 비난 할 것인지 커뮤니티에 묻는 대신, 더 읽기 쉽고 유지 보수가 쉬운 것은 무엇입니까?

team.Players.ByName("Nicolas") 

또는

team.ByName("Nicolas")

정말. 당신은 의심이 있습니까? 이제 실제 사용 사례에서 List <T>를 사용하지 못하게하는 다른 기술적 제약 사항을 사용해야 할 수도 있습니다. 그러나 존재하지 않아야하는 제약 조건을 추가하지 마십시오. 마이크로 소프트가 그 이유를 문서화하지 않았다면 분명히 "모범 사례"일 것입니다.


9
적절한 경우 받아 들여진 지혜에 도전 할 용기와 이니셔티브를 갖는 것이 중요하지만, 먼저 받아 들여진 지혜가 왜 도전에 착수하기 전에 먼저 받아 들여 졌는지 이해하는 것이 현명하다고 생각합니다. -1 "그 지침을 무시하십시오!" 이 가이드 라인이 존재하는 이유는 무엇입니까? 덧붙여서,이 공동체는이 경우에 "나를 비난"했을뿐만 아니라 나를 설득하는데 성공한 만족스러운 설명을 제공했다.
Superbest

3
실제로, 설명이 없을 때, 유일한 방법은 경계를 밀고 위반을 두려워하지 않고 테스트하는 것입니다. 지금. List <T>가 좋지 않은 경우에도 스스로에게 물어보십시오. 상속을 List <T>에서 Collection <T>로 바꾸는 것이 얼마나 고통 스럽습니까? 추측 : 리팩토링 도구를 사용하여 코드 길이에 관계없이 포럼에 게시하는 것보다 적은 시간. 지금은 좋은 질문이지만 실제적인 질문은 아닙니다.
Nicolas Dorier

명확히하기 위해 : 나는 내가 원하지만 원하는 것을 상속받을 수있는 SO의 축복을 기다리지 않고 있지만 내 질문의 요점은이 결정에 들어가야 할 고려 사항을 이해하고 많은 경험이 풍부한 프로그래머가 가지고있는 것처럼 보입니다. 내가 한 일의 반대를 결정했습니다. Collection<T>동일하지 않으므로 List<T>대규모 프로젝트에서 확인하는 데 약간의 작업이 필요할 수 있습니다.
Superbest

2
team.ByName("Nicolas")means "Nicolas"는 팀의 이름입니다.
Sam Leach

1
"존재해서는 안되는 제약 조건을 추가하지 마십시오"Au contraire, 노출 할만한 이유가없는 것은 노출하지 마십시오. 이것은 순수주의 나 맹목적인 열도가 아니며 최신 디자인 패션 트렌드도 아닙니다. 기본적인 객체 지향 디자인 101, 초급 레벨입니다. 이 "규칙"이 존재하는 이유는 비밀이 아니며 수십 년의 경험의 결과입니다.
sara sep

8

지침에 따르면 공개 API는 목록, 집합, 사전, 트리 등을 사용하는지 여부에 대한 내부 디자인 결정을 공개해서는 안됩니다. "팀"은 반드시 목록 일 필요는 없습니다. 목록으로 구현할 수 있지만 공개 API 사용자는 알아야 할 클래스를 사용해야합니다. 이를 통해 의사 결정을 변경하고 공용 인터페이스에 영향을주지 않고 다른 데이터 구조를 사용할 수 있습니다.


돌이켜 보면 @EricLippert와 다른 사람들에 대한 설명 후에 실제로 내 질문의 API 부분에 대한 훌륭한 답변을 얻었습니다. 내가 말한 경우 class FootballTeam : List<FootballPlayers>내 수업의 사용자는 나에게 말할 수 있습니다. 상속했습니다 List<T>, 반사를 이용하여 확인하여 List<T>축구 팀에 대한 이해가되지 않는 방법을 사용할 수있는 FootballTeamList<T>나는 것 때문에, 클라이언트에 계시 구현 세부 사항 일 (불필요).
Superbest

7

그들이 List<T>"최적화되었다"고 말할 때 나는 그들이 더 비싼 가상 방법과 같은 기능이 없다는 것을 의미한다고 생각합니다. 문제 그래서 당신이 노출되면 있다는 것입니다 List<T>귀하의 공개 API , 당신은 비즈니스 규칙을 적용 할 이상 그 기능을 사용자 정의 할 수있는 능력을 잃게. 그러나이 상속 된 클래스를 프로젝트 내에서 내부로 사용하는 경우 (수천 명의 고객 / 파트너 / 다른 팀에 API로 노출 될 가능성이 있음) 시간을 절약하고 원하는 기능입니다. 복제. 상속의 이점은 List<T>예측할 수없는 미래에 결코 사용자 정의되지 않는 많은 바보 같은 래퍼 코드를 제거한다는 것입니다. 또한 클래스가 명시 적으로 동일한 의미를 갖기를 원한다면List<T> API 수명 동안에도 괜찮을 것입니다.

FxCop 규칙 때문에 누군가의 블로그가 "나쁜"관행이라고 말하면서 많은 사람들이 많은 추가 작업을하는 것을 종종 봅니다. 여러 번, 이것은 패턴 palooza 기묘함을 디자인하기 위해 코드를 전환합니다. 많은 지침과 마찬가지로, 예외 있을 있는 지침으로 취급하십시오 .


4

이 답변의 대부분과 같이 복잡한 비교는 없지만이 상황을 처리하는 방법을 공유하고 싶습니다. 를 확장 IEnumerable<T>하면 Team클래스의 모든 메소드와 속성을 공개적으로 노출하지 않고도 클래스가 Linq 쿼리 확장을 지원할 수 있습니다 List<T>.

class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

    public Team()
    {
        playerList = new List<Player>();
    }

    public Enumerator GetEnumerator()
    {
        return playerList.GetEnumerator();
    }

    ...
}

class Player
{
    ...
}

3

클래스 사용자가 List에있는 모든 메소드와 속성이 필요한 경우 클래스에서 클래스를 파생시켜야합니다. 필요하지 않은 경우 List를 묶고 클래스 사용자에게 실제로 필요한 메소드의 래퍼를 만듭니다.

공개 API 또는 많은 사람들이 사용할 다른 코드를 작성하는 경우 이는 엄격한 규칙 입니다. 앱이 작고 개발자가 2 명 이하인 경우이 규칙을 무시해도됩니다. 시간이 절약됩니다.

작은 앱의 경우 덜 엄격한 다른 언어를 선택할 수도 있습니다. Ruby, JavaScript-적은 코드를 작성할 수있는 모든 것.


3

난 그냥 에펠을 발명하고 계약에 따라 디자인 한 버트 란드 마이어 TeamList<Player> 가 눈꺼풀을 치는 것만으로 덧붙였습니다.

그의 책 Object-Oriented Software Construction 에서 직사각형 창에 자식 창을 가질 수있는 GUI 시스템의 구현에 대해 설명합니다. 그는 단순히 한 Window모두 상속 RectangleTree<Window>구현을 재사용 할 수 있습니다.

그러나 C #은 Eiffel이 아닙니다. 후자는 다중 상속 및 기능 이름 변경을 지원 합니다 . C #에서는 하위 클래스를 만들 때 인터페이스와 구현을 모두 상속합니다. 구현을 무시할 수 있지만 호출 규칙은 수퍼 클래스에서 직접 복사됩니다. 이름을 바꿀 수 에펠 그러나, 당신은, public 메소드의 이름을 수정할 수 있습니다 AddRemoveHireFire당신을에 Team. 의 인스턴스가 (으 Team)로 다시 캐스트 List<Player>되면 호출자가 인스턴스를 사용 Add하고 Remove수정하지만 가상 메소드 HireFire호출합니다.


1
여기서 문제는 다중 상속, 믹스 인 (mixin) 등 또는 부재를 처리하는 방법이 아닙니다. 어떤 언어로든, 심지어 인간 언어로도 팀은 사람들의 목록이 아니라 사람들의 목록으로 구성됩니다. 이것은 추상적 인 개념입니다. Bertrand Meyer 또는 누군가가 List를 서브 클래 싱하여 팀을 관리하는 경우 잘못되었습니다. 보다 구체적인 종류의 목록을 원하면 List를 서브 클래 싱해야합니다. 당신이 동의하기를 바랍니다.
Mauro Sampietro

1
그것은 당신에게 어떤 상속이 있는지에 달려 있습니다. 그 자체만으로는 추상적 인 조작이며, 주류 해석이지만 두 클래스가 "특별한 종류의"관계에 있다는 것을 의미하지는 않습니다. 그러나 언어 구성이 현재이를 지원하는 대안이더라도 언어 디자인에서 지원하는 경우 상속 을 구현 재사용 메커니즘으로 취급 할 수 있습니다.
Alexey

2

나는 당신의 일반화에 동의하지 않는 것 같습니다. 팀은 단순한 플레이어 모음이 아닙니다. 팀에는 이름, 엠블럼, 관리 / 관리 직원 수집, 코칭 승무원 수집, 선수 수집 등 훨씬 더 많은 정보가 있습니다. 따라서 FootballTeam 클래스는 3 개의 컬렉션을 가져야하며 그 자체가 컬렉션이 아니어야합니다. 현실 세계를 제대로 모델링하는 것이라면

Specialized StringCollection과 같은 다른 기능 (예 : 유효성 검사 및 내부 저장소에서 개체를 추가하거나 제거하기 전에 확인)과 같은 다른 기능을 제공하는 PlayerCollection 클래스를 고려할 수 있습니다.

아마도 PlayerCollection의 개념이 선호하는 접근 방식에 더 적합합니까?

public class PlayerCollection : Collection<Player> 
{ 
}

그리고 FootballTeam은 다음과 같이 보일 수 있습니다 :

public class FootballTeam 
{ 
    public string Name { get; set; }
    public string Location { get; set; }

    public ManagementCollection Management { get; protected set; } = new ManagementCollection();

    public CoachingCollection CoachingCrew { get; protected set; } = new CoachingCollection();

    public PlayerCollection Players { get; protected set; } = new PlayerCollection();
}

2

클래스보다 인터페이스 선호

클래스는 클래스에서 파생되는 것을 피하고 필요한 최소한의 인터페이스를 구현해야합니다.

상속은 캡슐화를 깨뜨린 다

클래스에서 파생하면 캡슐화가 중단됩니다 .

  • 컬렉션 구현 방법에 대한 내부 세부 정보 노출
  • 적절하지 않은 인터페이스 (공개 함수 및 속성 세트)를 선언합니다.

무엇보다도 코드 리팩토링이 어려워집니다.

클래스는 구현 세부 사항입니다

클래스는 코드의 다른 부분에서 숨겨져 야하는 구현 세부 사항입니다. 한마디로System.List 는 현재와 미래에 적합하거나 적합하지 않을 수있는 추상 데이터 유형의 특정 구현입니다.

개념적으로 System.List데이터 유형이 "목록"이라는 사실 은 약간의 청어입니다. A System.List<T>는 요소를 추가, 삽입 및 제거하기위한 상각 된 O (1) 연산 및 요소 수를 검색하거나 색인으로 요소를 가져오고 설정하기위한 O (1) 연산을 지원하는 변경 가능한 순서 컬렉션입니다.

인터페이스가 작을수록 더 유연한 코드

데이터 구조를 설계 할 때 인터페이스가 단순할수록 코드가 더 유연 해집니다. LINQ가이를 입증하는 데 얼마나 강력한 지 살펴보십시오.

인터페이스를 선택하는 방법

"목록"을 생각할 때는 "야구 선수 컬렉션을 대표해야합니다"라고 말하면서 시작해야합니다. 클래스로 모델링하기로 결정했다고 가정 해 봅시다. 가장 먼저해야 할 일은이 클래스가 노출해야 할 최소 인터페이스 양을 결정하는 것입니다.

이 과정을 안내하는 데 도움이되는 몇 가지 질문 :

  • 카운트가 필요합니까? 구현을 고려하지 않으면IEnumerable<T>
  • 이 컬렉션은 초기화 된 후에 변경됩니까? 고려하지 않으면IReadonlyList<T> .
  • 색인으로 항목에 액세스 할 수 있어야합니까? 치다ICollection<T>
  • 컬렉션에 항목을 추가하는 순서가 중요합니까? 어쩌면 그것은ISet<T> ?
  • 실제로이 것들을 원한다면 계속해서 구현하십시오 IList<T>.

이런 식으로 코드의 다른 부분을 야구 선수 컬렉션의 구현 세부 정보에 연결하지 않고 인터페이스를 존중하는 한 구현 방법을 자유롭게 변경할 수 있습니다.

이 방법을 사용하면 코드를보다 쉽게 ​​읽고 리팩토링하고 재사용 할 수 있습니다.

보일러 플레이트 방지에 대한 참고 사항

최신 IDE에서 인터페이스를 구현하는 것은 쉬워야합니다. 마우스 오른쪽 버튼을 클릭하고 "구현 인터페이스"를 선택하십시오. 그런 다음 필요한 경우 모든 구현을 멤버 클래스로 전달하십시오.

즉, 많은 상용구를 작성하는 경우 예상보다 많은 기능을 노출하기 때문일 수 있습니다. 클래스에서 상속해서는 안되는 것과 같은 이유입니다.

또한 응용 프로그램에 적합한 더 작은 인터페이스를 설계 할 수도 있고, 필요한 다른 인터페이스에 해당 인터페이스를 매핑하기위한 몇 가지 도우미 확장 기능 일 수도 있습니다. 이것이 내가 LinqArray 라이브러리에IArray 대한 내 인터페이스 에서 취한 접근법 이다.


2

직렬화 문제

한 측면이 없습니다. List <>에서 상속 된 클래스는 XmlSerializer를 사용하여 올바르게 직렬화 할 수 없습니다 . 이 경우 DataContractSerializer 를 대신 사용하거나 자체 직렬화 구현이 필요합니다.

public class DemoList : List<Demo>
{
    // using XmlSerializer this properties won't be seralized:
    string AnyPropertyInDerivedFromList { get; set; }     
}

public class Demo
{
    // this properties will be seralized
    string AnyPropetyInDemo { get; set; }  
}

추가 읽기 : 클래스가 List <>에서 상속되면 XmlSerializer는 다른 속성을 직렬화하지 않습니다.


0

언제 받아 들일 수 있습니까?

Eric Lippert를 인용하려면 :

당신이 메커니즘을 구축 할 때 확장 List<T>메커니즘을.

예를 들어 다음과 같은 AddRange방법 이 없다는 것에 지쳤 습니다 IList<T>.

public interface IMoreConvenientListInterface<T> : IList<T>
{
    void AddRange(IEnumerable<T> collection);
}

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.