Ao planejar meus programas, geralmente começo com uma cadeia de pensamento assim:
Um time de futebol é apenas uma lista de jogadores de futebol. Portanto, eu devo representá-lo com:
var football_team = new List<FootballPlayer>();
A ordem desta lista representa a ordem em que os jogadores estão listados na lista.
Mas percebo depois que as equipes também têm outras propriedades, além da mera lista de jogadores, que devem ser registradas. Por exemplo, o total acumulado de pontuações nesta temporada, o orçamento atual, as cores uniformes, a string
representando o nome da equipe etc.
Então eu penso:
Ok, um time de futebol é como uma lista de jogadores, mas, além disso, possui um nome (a
string
) e um total de pontuações (umint
). O .NET não fornece uma aula para armazenar times de futebol, então eu vou fazer minha própria aula. A estrutura existente mais semelhante e relevante éList<FootballPlayer>
, portanto, herdarei dela:class FootballTeam : List<FootballPlayer> { public string TeamName; public int RunningTotal }
Mas acontece que uma diretriz diz que você não deve herdarList<T>
. Estou completamente confuso com esta diretriz em dois aspectos.
Por que não?
Aparentemente, de List
alguma forma, é otimizado para o desempenho . Como assim? Quais problemas de desempenho causarei se eu estender List
? O que exatamente vai quebrar?
Outro motivo que eu vi é que List
é fornecido pela Microsoft, e eu não tenho controle sobre ele, então não posso alterá-lo mais tarde, depois de expor uma "API pública" . Mas luto para entender isso. O que é uma API pública e por que devo me importar? Se meu projeto atual não tiver e provavelmente não possuir essa API pública, posso ignorar com segurança esta diretriz? Se eu herdar List
e precisar de uma API pública, que dificuldades terei?
Por que isso importa? Uma lista é uma lista. O que poderia mudar? O que eu poderia querer mudar?
E, finalmente, se a Microsoft não queria que eu herdasse List
, por que eles não fizeram parte da classe sealed
?
O que mais devo usar?
Aparentemente, para coleções personalizadas, a Microsoft forneceu uma Collection
classe que deve ser estendida em vez de List
. Mas essa classe é muito vazia e não possui muitas coisas úteis, comoAddRange
, por exemplo. A resposta de jvitor83 fornece uma lógica de desempenho para esse método específico, mas como um lento AddRange
não é melhor que não AddRange
?
Herdar de Collection
é muito mais trabalho do que herdar List
, e não vejo benefício. Certamente a Microsoft não me diria para fazer um trabalho extra sem motivo, por isso não posso deixar de sentir que estou de alguma forma entendendo algo, e herdar Collection
não é realmente a solução certa para o meu problema.
Já vi sugestões como implementar IList
. Apenas não. São dezenas de linhas de código padrão que não me valem nada.
Por fim, alguns sugerem envolver o List
item em algo:
class FootballTeam
{
public List<FootballPlayer> Players;
}
Existem dois problemas com isso:
Isso torna meu código desnecessariamente detalhado. Agora devo ligar em
my_team.Players.Count
vez de apenasmy_team.Count
. Felizmente, com o C # eu posso definir indexadores para tornar a indexação transparente e encaminhar todos os métodos do internoList
... Mas isso é muito código! O que eu ganho por todo esse trabalho?Simplesmente não faz nenhum sentido. Um time de futebol não "tem" uma lista de jogadores. Ele é a lista de jogadores. Você não diz "John McFootballer se juntou aos jogadores de SomeTeam". Você diz "John se juntou ao SomeTeam". Você não adiciona uma letra a "caracteres de uma string", adiciona uma letra a uma string. Você não adiciona um livro aos livros de uma biblioteca, adiciona um livro a uma biblioteca.
Percebo que o que acontece "sob o capô" pode ser considerado "acrescentando X à lista interna de Y", mas isso parece uma maneira muito contra-intuitiva de pensar sobre o mundo.
Minha pergunta (resumida)
Qual é a maneira C # correta de representar uma estrutura de dados, que "logicamente" (isto é, "para a mente humana") é apenas uma list
das things
poucas com alguns sinos e assobios?
A herança de List<T>
sempre é inaceitável? Quando é aceitável? Porque porque não? O que um programador deve considerar ao decidir se deve herdar List<T>
ou não?
string
é necessária para fazer tudo o que seobject
pode fazer e muito mais .Respostas:
Existem algumas boas respostas aqui. Eu acrescentaria a eles os seguintes pontos.
Peça a dez pessoas que não são programadoras de computador que estão familiarizadas com a existência do futebol para preencher o espaço em branco:
Será que alguém diga "lista de jogadores de futebol com alguns sinos e assobios", ou fizeram tudo o que "equipe de esportes" dizer ou "clube" ou "organização"? Sua noção de que um time de futebol é um tipo específico de lista de jogadores está na sua mente humana e somente na sua mente humana.
List<T>
é um mecanismo . O time de futebol é um objeto de negócios - ou seja, um objeto que representa algum conceito que está no domínio de negócios do programa. Não misture isso! Um time de futebol é um tipo de time; ele tem uma lista, uma lista é uma lista de jogadores . Uma lista não é um tipo específico de lista de jogadores . Uma lista é uma lista de jogadores. Então faça uma propriedade chamadaRoster
que é aList<Player>
. E faça issoReadOnlyList<Player>
enquanto estiver fazendo isso, a menos que você acredite que todo mundo que conhece um time de futebol possa excluir jogadores da lista.Inaceitável para quem? Eu? Não.
Quando você está construindo um mecanismo que estende o
List<T>
mecanismo .Estou construindo um mecanismo ou um objeto de negócios ?
Você gastou mais tempo digitando sua pergunta do que seria necessário escrever métodos de encaminhamento para os membros relevantes de mais de
List<T>
cinquenta vezes. Você claramente não tem medo de verbosidade, e estamos falando de uma quantidade muito pequena de código aqui; isso é alguns minutos de trabalho.ATUALIZAR
Pensei um pouco mais e há outra razão para não modelar um time de futebol como uma lista de jogadores. De fato, pode ser uma má idéia modelar um time de futebol como tendo uma lista de jogadores também. O problema com uma equipe como / ter uma lista de jogadores é que o que você tem é um instantâneo da equipe em um momento no tempo . Não sei qual é o seu argumento comercial para esta classe, mas se eu tivesse uma classe que representasse um time de futebol, eu gostaria de fazer perguntas como "quantos jogadores do Seahawks perderam jogos devido a lesão entre 2003 e 2013?" ou "Qual jogador de Denver que jogou anteriormente para outro time teve o maior aumento ano-a-ano em jardas?" ou "Os Piggers foram até o fim deste ano? "
Ou seja, um time de futebol parece-me bem modelado como uma coleção de fatos históricos , como quando um jogador foi recrutado, lesionado, aposentado, etc. Obviamente, a lista de jogadores atual é um fato importante que provavelmente deveria estar na frente centro, mas pode haver outras coisas interessantes que você deseja fazer com esse objeto que exijam uma perspectiva mais histórica.
fonte
IEnumerable<T>
- uma sequência ?Uau, sua postagem tem uma série de perguntas e pontos. A maior parte do raciocínio que você obtém da Microsoft está exatamente no ponto. Vamos começar com tudo sobre
List<T>
List<T>
é altamente otimizado. Seu principal uso é para ser usado como um membro privado de um objeto.class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }
. Agora é tão fácil quanto fazervar list = new MyList<int, string>();
.IList<T>
se você precisar que algum consumidor tenha uma lista indexada. Isso permite alterar a implementação dentro de uma classe posteriormente.Collection<T>
muito genérica porque é um conceito genérico ... o nome já diz tudo; é apenas uma coleção. Existem versões mais precisas, comoSortedCollection<T>
,ObservableCollection<T>
,ReadOnlyCollection<T>
, etc. cada um dos quais implementamIList<T>
, mas nãoList<T>
.Collection<T>
permite que os membros (por exemplo, Adicionar, Remover, etc.) sejam substituídos por serem virtuais.List<T>
não.Se eu estivesse escrevendo esse código, a classe provavelmente ficaria assim:
fonte
List<T>
é estendida como uma classe interna privada que usa genéricos para simplificar o uso e a declaração deList<T>
. Se a extensão fosse meramenteclass FootballRoster : List<FootballPlayer> { }
, então sim, eu poderia ter usado um apelido. Eu acho que vale a pena notar que você pode adicionar membros / funcionalidade adicionais, mas é limitado, pois você não pode substituir membros importantes deList<T>
.Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden
Agora, esse é um bom motivo para não herdar da Lista. As outras respostas têm filosofia demais.O código anterior significa: um monte de caras da rua jogando futebol, e eles têm um nome. Algo como:
Enfim, esse código (da minha resposta)
Significa: trata-se de um time de futebol que possui dirigentes, jogadores, administradores, etc. Algo como:
É assim que a sua lógica é apresentada nas fotos ...
fonte
private readonly List<T> _roster = new List<T>(44);
System.Linq
espaço para nome.Este é um exemplo clássico de composição versus herança .
Nesse caso específico:
O time é uma lista de jogadores com comportamento adicional
ou
O time é um objeto próprio que contém uma lista de jogadores.
Ao estender a Lista, você está se limitando de várias maneiras:
Você não pode restringir o acesso (por exemplo, impedir que as pessoas alterem a lista). Você obtém todos os métodos de lista, independentemente de precisar ou querer todos eles.
O que acontece se você quiser ter listas de outras coisas também. Por exemplo, as equipes têm treinadores, gerentes, fãs, equipamentos etc. Alguns deles podem ser listas por si só.
Você limita suas opções de herança. Por exemplo, você pode criar um objeto Team genérico e, em seguida, ter BaseballTeam, FootballTeam etc. que herdem disso. Para herdar da Lista, você precisa fazer a herança da Equipe, mas isso significa que todos os vários tipos de equipe são forçados a ter a mesma implementação dessa lista.
Composição - incluindo um objeto que fornece o comportamento que você deseja dentro do seu objeto.
Herança - seu objeto se torna uma instância do objeto com o comportamento desejado.
Ambos têm seus usos, mas este é um caso claro em que a composição é preferível.
fonte
Como todos apontaram, uma equipe de jogadores não é uma lista de jogadores. Este erro é cometido por muitas pessoas em todos os lugares, talvez em vários níveis de especialização. Muitas vezes, o problema é sutil e, ocasionalmente, muito grosseiro, como neste caso. Tais projetos são ruins porque violam o Princípio de Substituição de Liskov . A internet tem muitos bons artigos explicando esse conceito, por exemplo, http://en.wikipedia.org/wiki/Liskov_substitution_principle
Em resumo, há duas regras a serem preservadas em um relacionamento Pai / Filho entre classes:
Em outras palavras, um pai é uma definição necessária de um filho e um filho é uma definição suficiente de um pai.
Aqui está uma maneira de pensar sobre a solução e aplicar o princípio acima, que deve ajudar a evitar esse erro. Deve-se testar a hipótese verificando se todas as operações de uma classe pai são válidas para a classe derivada estrutural e semanticamente.
Como você vê, apenas a primeira característica de uma lista é aplicável a uma equipe. Portanto, uma equipe não é uma lista. Uma lista seria um detalhe de implementação de como você gerencia sua equipe, portanto, deve ser usada apenas para armazenar os objetos do jogador e ser manipulada com métodos da classe Equipe.
Neste ponto, eu gostaria de observar que uma classe de equipe não deve, na minha opinião, nem ser implementada usando uma lista; ele deve ser implementado usando uma estrutura de dados Set (HashSet, por exemplo) na maioria dos casos.
fonte
E se a
FootballTeam
equipe tiver reservas juntamente com a equipe principal?Como você modelaria isso?
O relacionamento é claramente tem um e não é um .
ou
RetiredPlayers
?Como regra geral, se você quiser herdar de uma coleção, nomeie a classe
SomethingCollection
.O seu
SomethingCollection
semanticamente faz sentido? Faça isso apenas se seu tipo for uma coleção deSomething
.No caso de
FootballTeam
isso não parece certo. ATeam
é mais do que aCollection
. ATeam
pode ter treinadores, treinadores, etc, como as outras respostas apontaram.FootballCollection
parece uma coleção de bolas de futebol ou talvez uma coleção de apetrechos de futebol.TeamCollection
, uma coleção de equipes.FootballPlayerCollection
soa como uma coleção de jogadores que seria um nome válido para uma classe herdadaList<FootballPlayer>
se você realmente quisesse fazer isso.Realmente
List<FootballPlayer>
é um tipo perfeitamente bom de se lidar. TalvezIList<FootballPlayer>
se você estiver retornando de um método.Em suma
Pergunte a si mesmo
É
X
umY
? ou temX
umY
?Os nomes das minhas turmas significam o que são?
fonte
DefensivePlayers
,OffensivePlayers
ouOtherPlayers
, pode ser legitimamente útil ter um tipo que pode ser usado pelo código que espera umList<Player>
mas os membros também incluídosDefensivePlayers
,OffsensivePlayers
ouSpecialPlayers
do tipoIList<DefensivePlayer>
,IList<OffensivePlayer>
eIList<Player>
. Pode-se usar um objeto separado para armazenar em cache as listas separadas, mas encapsulando-os dentro de um mesmo objeto como a lista principal parece mais limpo [usar a invalidação de uma lista recenseador ...Design> Implementação
Quais métodos e propriedades você expõe é uma decisão de design. Qual classe base você herda é um detalhe de implementação. Eu sinto que vale a pena dar um passo para trás.
Um objeto é uma coleção de dados e comportamento.
Portanto, suas primeiras perguntas devem ser:
Tenha em mente que herança implica um relacionamento "isa" (é a), enquanto composição implica um relacionamento "tem um" (hasa). Escolha o caminho certo para a sua situação, tendo em mente onde as coisas podem ir à medida que seu aplicativo evolui.
Considere pensar em interfaces antes de pensar em tipos concretos, pois algumas pessoas acham mais fácil colocar seu cérebro no "modo de design" dessa maneira.
Isso não é algo que todo mundo faz conscientemente nesse nível na codificação diária. Mas se você está pensando sobre esse tipo de tópico, está pisando nas águas do design. Estar ciente disso pode ser libertador.
Considere os detalhes do projeto
Dê uma olhada em List <T> e IList <T> no MSDN ou Visual Studio. Veja quais métodos e propriedades eles expõem. Todos esses métodos se parecem com algo que alguém gostaria de fazer para um time de futebol na sua opinião?
FootballTeam.Reverse () faz sentido para você? FootballTeam.ConvertAll <TOutput> () se parece com algo que você deseja?
Esta não é uma pergunta complicada; a resposta pode realmente ser "sim". Se você implementar / herdar List <Player> ou IList <Player>, ficará preso a eles; se isso é ideal para o seu modelo, faça-o.
Se você decidir sim, isso faz sentido, e você deseja que seu objeto seja tratável como uma coleção / lista de jogadores (comportamento) e, portanto, deseja implementar o ICollection ou o IList, por todos os meios. Notionally:
Se você deseja que seu objeto contenha uma coleção / lista de jogadores (dados) e, portanto, deseja que a coleção ou lista seja uma propriedade ou membro, faça isso de qualquer maneira. Notionally:
Você pode sentir que deseja que as pessoas consigam enumerar apenas o conjunto de jogadores, em vez de contá-los, adicioná-los ou removê-los. IEnumerable <Player> é uma opção perfeitamente válida a ser considerada.
Você pode achar que nenhuma dessas interfaces é útil no seu modelo. Isso é menos provável (IEnumerable <T> é útil em muitas situações), mas ainda é possível.
Qualquer pessoa que tente lhe dizer que uma delas está categoricamente e definitivamente errada em todos os casos está equivocada. Qualquer pessoa que tente lhe dizer que é categórica e definitivamente certa em todos os casos está enganada.
Vá para Implementação
Depois de decidir sobre dados e comportamento, você pode tomar uma decisão sobre a implementação. Isso inclui de quais classes concretas você depende via herança ou composição.
Isso pode não ser um grande passo, e as pessoas geralmente confundem design e implementação, já que é bem possível analisar tudo isso em sua cabeça em um ou dois segundos e começar a digitar.
Uma experiência de pensamento
Um exemplo artificial: como outros já mencionaram, uma equipe nem sempre é "apenas" uma coleção de jogadores. Você mantém uma coleção de resultados de jogos para o time? A equipe é intercambiável com o clube, no seu modelo? Nesse caso, e se seu time é uma coleção de jogadores, talvez também seja uma coleção de funcionários e / ou uma coleção de pontuações. Então você acaba com:
Não obstante o design, neste momento no C #, você não poderá implementar tudo isso herdando da Lista <T> de qualquer maneira, pois o C # "only" suporta herança única. (Se você já experimentou esse mal-estar no C ++, considere isso uma coisa boa.) A implementação de uma coleção por herança e outra por composição provavelmente parecerá suja. E propriedades como Count tornam-se confusas para os usuários, a menos que você implemente ILIst <Player> .Count e IList <StaffMember> .Count etc. explicitamente, e então são mais dolorosas do que confusas. Você pode ver para onde isso está indo; sentir a sensação de pensar nessa avenida pode muito bem dizer que é errado seguir nessa direção (e com ou sem razão, seus colegas também podem se você a implementou dessa maneira!)
A resposta curta (tarde demais)
A diretriz sobre não herdar de classes de coleção não é específica de C #, você a encontrará em muitas linguagens de programação. É sabedoria recebida, não uma lei. Uma razão é que, na prática, considera-se que a composição geralmente vence a herança em termos de compreensibilidade, implementabilidade e manutenibilidade. É mais comum que objetos do mundo real / domínio encontrem relacionamentos "hasa" úteis e consistentes do que relacionamentos "isa" úteis e consistentes, a menos que você tenha um resumo profundo, principalmente com o passar do tempo e com os dados e comportamento precisos dos objetos no código alterar. Isso não deve fazer com que você sempre exclua a herança de classes de coleção; mas pode ser sugestivo.
fonte
Primeiro de tudo, tem a ver com usabilidade. Se você usar herança, a
Team
classe irá expor comportamentos (métodos) projetados exclusivamente para manipulação de objetos. Por exemplo,AsReadOnly()
ouCopyTo(obj)
métodos não fazem sentido para o objeto de equipe. Em vez doAddRange(items)
método, você provavelmente desejaria umAddPlayers(players)
método mais descritivo .Se você deseja usar o LINQ, implementar uma interface genérica como
ICollection<T>
ouIEnumerable<T>
faria mais sentido.Como mencionado, a composição é o caminho certo a seguir. Basta implementar uma lista de jogadores como uma variável privada.
fonte
Um time de futebol não é uma lista de jogadores de futebol. Um time de futebol é composto por uma lista de jogadores de futebol!
Isso está logicamente errado:
e isso está correto:
fonte
Depende do contexto
Quando você considera seu time como uma lista de jogadores, está projetando a "idéia" de um time de bola para um aspecto: você reduz o "time" às pessoas que vê em campo. Essa projeção está correta apenas em um determinado contexto. Em um contexto diferente, isso pode estar completamente errado. Imagine que você quer se tornar um patrocinador da equipe. Então você tem que conversar com os gerentes da equipe. Nesse contexto, a equipe é projetada para a lista de seus gerentes. E essas duas listas geralmente não se sobrepõem muito. Outros contextos são os atuais e os antigos, etc.
Semântica pouco clara
Portanto, o problema de considerar uma equipe como uma lista de seus jogadores é que sua semântica depende do contexto e que não pode ser estendida quando o contexto muda. Além disso, é difícil expressar qual o contexto que você está usando.
As aulas são extensíveis
Ao usar uma classe com apenas um membro (por exemplo
IList activePlayers
), você pode usar o nome do membro (e adicionalmente seu comentário) para tornar o contexto claro. Quando há contextos adicionais, você apenas adiciona um membro adicional.As aulas são mais complexas
Em alguns casos, pode ser um exagero criar uma classe extra. Cada definição de classe deve ser carregada através do carregador de classes e será armazenada em cache pela máquina virtual. Isso custa desempenho e memória do tempo de execução. Quando você tem um contexto muito específico, pode ser bom considerar um time de futebol como uma lista de jogadores. Mas, neste caso, você realmente deve apenas usar a
IList
, não uma classe derivada dela.Conclusão / Considerações
Quando você tem um contexto muito específico, não há problema em considerar uma equipe como uma lista de jogadores. Por exemplo, dentro de um método, não há problema em escrever:
Ao usar o F #, pode ser bom criar uma abreviação de tipo:
Mas quando o contexto é mais amplo ou até pouco claro, você não deve fazer isso. Esse é especialmente o caso quando você cria uma nova classe cujo contexto no qual ela pode ser usada no futuro não é clara. Um sinal de aviso é quando você começa a adicionar atributos adicionais à sua classe (nome da equipe, treinador etc.). Este é um sinal claro de que o contexto em que a classe será usada não é fixo e mudará no futuro. Nesse caso, você não pode considerar o time como uma lista de jogadores, mas deve modelar a lista dos jogadores (atualmente ativos, não lesionados, etc.) como um atributo do time.
fonte
Deixe-me reescrever sua pergunta. para que você possa ver o assunto de uma perspectiva diferente.
Quando preciso representar um time de futebol, entendo que é basicamente um nome. Como: "The Eagles"
Mais tarde, percebi que as equipes também têm jogadores.
Por que não posso simplesmente estender o tipo de string para que ele também mantenha uma lista de players?
Seu ponto de entrada no problema é arbitrário. Tente pensar no que uma equipe possui (propriedades), não no que é .
Depois disso, você poderá ver se ele compartilha propriedades com outras classes. E pense em herança.
fonte
Só porque eu acho que as outras respostas se destacam se um time de futebol "é-a"
List<FootballPlayer>
ou "tem-a"List<FootballPlayer>
, o que realmente não responde a essa pergunta como está escrita.O OP solicita principalmente esclarecimentos sobre as diretrizes para herdar de
List<T>
:Porque
List<T>
não tem métodos virtuais. Isso é menos problemático em seu próprio código, pois geralmente você pode alternar a implementação com relativamente pouca dor - mas pode ser um negócio muito maior em uma API pública.Uma API pública é uma interface que você expõe a programadores de terceiros. Pense no código da estrutura. E lembre-se de que as diretrizes mencionadas são as " Diretrizes de Design do .NET Framework " e não as " Diretrizes de Design de Aplicativos .NET ". Há uma diferença e, de um modo geral, o design da API pública é muito mais rigoroso.
Praticamente sim. Convém considerar a lógica por trás disso para verificar se ela se aplica à sua situação, mas se você não estiver criando uma API pública, não precisará se preocupar particularmente com as preocupações da API, como a versão (das quais, este é um subconjunto).
Se você adicionar uma API pública no futuro, será necessário abstrair sua API da sua implementação (não expondo a sua
List<T>
diretamente) ou violar as diretrizes com a possível dor futura que isso implica.Depende do contexto, mas como estamos usando
FootballTeam
como exemplo - imagine que você não pode adicionar umFootballPlayer
se isso fizer com que a equipe ultrapasse o limite de salário. Uma maneira possível de adicionar isso seria algo como:Ah ... mas você não pode,
override Add
porque não évirtual
(por razões de desempenho).Se você estiver em um aplicativo (o que basicamente significa que você e todos os seus chamadores são compilados juntos), agora você pode mudar para o uso
IList<T>
e corrigir os erros de compilação:mas, se você foi exposto publicamente a terceiros, acabou de fazer uma alteração que causará erros de compilação e / ou tempo de execução.
TL; DR - as diretrizes são para APIs públicas. Para APIs privadas, faça o que quiser.
fonte
Permitir que as pessoas digam
faz algum sentido? Caso contrário, não deve ser uma lista.
fonte
myTeam.subTeam(3, 5);
subList
nem vai voltarTeam
mais.Depende do comportamento do seu objeto "equipe". Se ele se comportar como uma coleção, pode ser bom representá-la primeiro com uma Lista simples. Em seguida, você pode começar a perceber que continua duplicando o código que itera na lista; Neste ponto, você tem a opção de criar um objeto FootballTeam que envolve a lista de jogadores. A classe FootballTeam se torna o lar de todo o código que itera na lista de jogadores.
Encapsulamento. Seus clientes não precisam saber o que acontece dentro do FootballTeam. Para que todos os seus clientes saibam, isso pode ser implementado consultando a lista de jogadores em um banco de dados. Eles não precisam saber, e isso melhora o seu design.
Exatamente :) você dirá footballTeam.Add (john), não footballTeam.List.Add (john). A lista interna não estará visível.
fonte
Há muitas respostas excelentes aqui, mas quero abordar algo que não vi mencionado: o design orientado a objetos é sobre o fortalecimento de objetos .
Você deseja encapsular todas as suas regras, trabalho adicional e detalhes internos dentro de um objeto apropriado. Dessa maneira, outros objetos que interagem com esse não precisam se preocupar com tudo. Na verdade, você quer dar um passo adiante e impedir ativamente que outros objetos ignorem esses componentes internos.
Quando você herda
List
, todos os outros objetos podem vê-lo como uma lista. Eles têm acesso direto aos métodos para adicionar e remover jogadores. E você terá perdido seu controle; por exemplo:Suponha que você queira diferenciar quando um jogador sair, sabendo se ele se aposentou, renunciou ou foi demitido. Você pode implementar um
RemovePlayer
método que utiliza uma enum de entrada apropriada. No entanto, por herança deList
, você não seria capaz de impedir o acesso direto aRemove
,RemoveAll
e mesmoClear
. Como resultado, você realmente desempoderou suaFootballTeam
classe.Pensamentos adicionais sobre encapsulamento ... Você levantou a seguinte preocupação:
Você está correto, seria desnecessariamente detalhado para todos os clientes usarem sua equipe. No entanto, esse problema é muito pequeno em comparação com o fato de você ter exposto
List Players
a todos, para que eles possam mexer com sua equipe sem o seu consentimento.Você continua dizendo:
Você está errado quanto ao primeiro ponto: largue a palavra 'lista' e é óbvio que uma equipe tem jogadores.
No entanto, você bate no prego na cabeça com o segundo. Você não quer clientes ligando
ateam.Players.Add(...)
. Você quer que eles liguemateam.AddPlayer(...)
. E sua implementação (possivelmente entre outras coisas) seria chamadaPlayers.Add(...)
internamente.Espero que você possa ver a importância do encapsulamento para o objetivo de capacitar seus objetos. Você deseja permitir que cada classe faça seu trabalho bem, sem medo de interferência de outros objetos.
fonte
Lembre-se: "Todos os modelos estão errados, mas alguns são úteis". - George EP Box
Não existe um "caminho correto", apenas um caminho útil.
Escolha um que seja útil para você e / seus usuários. É isso aí.
Desenvolva economicamente, não faça engenharia demais. Quanto menos código você escrever, menos código precisará depurar.(leia as seguintes edições).- Editado
Minha melhor resposta seria ... depende. A herança de uma lista exporia os clientes desta classe a métodos que podem ser não devem ser expostos, principalmente porque o FootballTeam se parece com uma entidade comercial.
- Edição 2
Sinceramente, não me lembro do que estava me referindo no comentário "não exagere". Embora eu acredite que a mentalidade do KISS seja um bom guia, quero enfatizar que herdar uma classe de negócios da List criaria mais problemas do que resolve, devido ao vazamento de abstração .
Por outro lado, acredito que há um número limitado de casos em que simplesmente herdar da List é útil. Como escrevi na edição anterior, isso depende. A resposta para cada caso é fortemente influenciada pelo conhecimento, experiência e preferências pessoais.
Agradeço ao @kai por me ajudar a pensar com mais precisão na resposta.
fonte
List
exporia os clientes dessa classe a métodos que podem ser não devem ser expostos " é precisamente o que torna a herança deList
um absoluto não-não. Uma vez expostos, eles gradualmente são abusados e mal utilizados ao longo do tempo. Economizar tempo agora "desenvolvendo-se economicamente" pode facilmente levar a dez vezes mais economias perdidas no futuro: depuração dos abusos e, eventualmente, refatoração para corrigir a herança. O princípio YAGNI também pode ser entendido como significando: Você não precisará de todos esses métodosList
, portanto não os exponha.List
porque é rápido e fácil e, portanto, levaria a menos bugs está completamente errado, é apenas um projeto ruim e um projeto ruim leva a muito mais erros do que "engenharia excedente".Isso me lembra do "É um" versus "tem um" tradeoff. Às vezes, é mais fácil e faz mais sentido herdar diretamente de uma super classe. Outras vezes, faz mais sentido criar uma classe independente e incluir a classe da qual você teria herdado como uma variável de membro. Você ainda pode acessar a funcionalidade da classe, mas não está vinculado à interface ou a quaisquer outras restrições que possam advir da herança da classe.
Qual você faz? Tal como acontece com muitas coisas ... depende do contexto. O guia que eu usaria é que, para herdar de outra classe, realmente deveria haver um relacionamento "é um". Portanto, se você escrever uma classe chamada BMW, ela poderá herdar do Car porque um BMW é realmente um carro. Uma classe Horse pode herdar da classe Mammal porque um cavalo é realmente um mamífero na vida real e qualquer funcionalidade do Mammal deve ser relevante para o Horse. Mas você pode dizer que uma equipe é uma lista? Pelo que sei, não parece que uma equipe realmente "seja uma" lista. Portanto, nesse caso, eu teria uma lista como variável de membro.
fonte
Meu segredo sujo: eu não ligo para o que as pessoas dizem, e eu faço. O .NET Framework é espalhado com "XxxxCollection" (UIElementCollection para o topo da minha cabeça).
Então, o que me impede de dizer:
Quando eu acho melhor que
Além disso, meu PlayerCollection pode ser usado por outra classe, como "Club", sem duplicação de código.
As melhores práticas de ontem podem não ser as de amanhã. Não há razão por trás da maioria das melhores práticas, a maioria é apenas um amplo acordo entre a comunidade. Em vez de perguntar à comunidade se a culpa será sua quando você se perguntar, o que é mais legível e sustentável?
ou
Realmente. Você tem alguma dúvida? Agora talvez você precise jogar com outras restrições técnicas que o impedem de usar a Lista <T> no seu caso de uso real. Mas não adicione uma restrição que não deveria existir. Se a Microsoft não documentou o porquê, certamente é uma "prática recomendada" vinda do nada.
fonte
Collection<T>
não é o mesmo,List<T>
portanto, pode ser necessário um pouco de trabalho para verificar em um projeto de grande porte.team.ByName("Nicolas")
significa"Nicolas"
é o nome da equipe.O que as diretrizes dizem é que a API pública não deve revelar a decisão interna de design de se você está usando uma lista, um conjunto, um dicionário, uma árvore ou qualquer outra coisa. Uma "equipe" não é necessariamente uma lista. Você pode implementá-lo como uma lista, mas os usuários da sua API pública devem usar sua classe conforme a necessidade. Isso permite que você altere sua decisão e use uma estrutura de dados diferente sem afetar a interface pública.
fonte
class FootballTeam : List<FootballPlayers>
, os usuários da minha classe poderão dizer que eu ' que herdeiList<T>
usando reflexão, vendo osList<T>
métodos que não fazem sentido para um time de futebol e conseguindo usáFootballTeam
-List<T>
lo, então eu estaria revelando detalhes da implementação ao cliente (desnecessariamente).Quando eles dizem que
List<T>
é "otimizado", acho que eles querem dizer que ele não possui recursos como métodos virtuais que são um pouco mais caros. Portanto, o problema é que, depois de exporList<T>
em sua API pública , você perde a capacidade de impor regras de negócios ou personalizar sua funcionalidade posteriormente. Mas se você estiver usando essa classe herdada como interna em seu projeto (em vez de potencialmente exposta a milhares de seus clientes / parceiros / outras equipes como API), pode ser bom se economizar seu tempo e a funcionalidade que você deseja duplicado. A vantagem de herdarList<T>
é que você elimina muitos códigos de invólucros estúpidos que nunca serão personalizados em um futuro próximo. Além disso, se você quiser que sua classe tenha explicitamente a mesma semântica exata deList<T>
durante toda a vida útil de suas APIs, também pode ser bom.Costumo ver muitas pessoas fazendo toneladas de trabalho extra apenas por causa da regra da FxCop, ou o blog de alguém diz que é uma prática "ruim". Muitas vezes, isso transforma o código em design de estranheza padrão de palooza. Assim como ocorre com muitas diretrizes, trate-as como diretrizes que podem ter exceções.
fonte
Embora não tenha uma comparação complexa como a maioria dessas respostas, gostaria de compartilhar meu método para lidar com essa situação. Ao estender
IEnumerable<T>
, você pode permitir que suaTeam
classe suporte extensões de consulta Linq, sem expor publicamente todos os métodos e propriedades deList<T>
.fonte
Se os usuários de sua classe precisarem de todos os métodos e propriedades ** List possui, você deverá derivar sua classe. Se eles não precisarem deles, coloque a Lista e crie invólucros para os métodos que os usuários de sua classe realmente precisam.
Essa é uma regra estrita, se você escrever uma API pública ou qualquer outro código que será usado por muitas pessoas. Você pode ignorar esta regra se tiver um aplicativo minúsculo e não mais que 2 desenvolvedores. Isso economizará algum tempo.
Para aplicativos pequenos, considere também escolher outro idioma menos estrito. Ruby, JavaScript - qualquer coisa que permita escrever menos código.
fonte
Eu só queria acrescentar que Bertrand Meyer, o inventor de Eiffel e o design por contrato, teriam
Team
herdadoList<Player>
sem nem sequer piscar as pálpebras.Em seu livro, Construção de software orientada a objetos , ele discute a implementação de um sistema GUI em que janelas retangulares podem ter janelas filho. Ele simplesmente
Window
herdou de ambosRectangle
eTree<Window>
reutilizou a implementação.No entanto, c # não é Eiffel. O último suporta herança múltipla e renomeação de recursos . No C #, quando você subclasse, herda a interface e a implementação. Você pode substituir a implementação, mas as convenções de chamada são copiadas diretamente da superclasse. No Eiffel, no entanto, você pode modificar os nomes dos métodos públicos, para renomear
Add
eRemove
paraHire
eFire
no seuTeam
. Se uma instância deTeam
for upcast de voltaList<Player>
, o chamador irá usá-laAdd
eRemove
modificá-la, mas seus métodos virtuaisHire
eFire
serão chamados.fonte
Acho que não concordo com sua generalização. Uma equipe não é apenas uma coleção de jogadores. Uma equipe tem muito mais informações a respeito - nome, emblema, coleção de equipe administrativa / administrativa, coleção de equipe técnica e coleção de jogadores. Portanto, corretamente, sua classe FootballTeam deve ter 3 coleções e não ser uma coleção; se é para modelar adequadamente o mundo real.
Você pode considerar uma classe PlayerCollection que, como a StringCollection especializada, oferece algumas outras facilidades - como validação e verificações antes de os objetos serem adicionados ou removidos do armazenamento interno.
Talvez, a noção de apostadores PlayerCollection se adapte à sua abordagem preferida?
E então o FootballTeam pode ficar assim:
fonte
Preferir interfaces sobre classes
As classes devem evitar derivar de classes e, em vez disso, implementar as interfaces mínimas necessárias.
Herança quebra Encapsulamento
Derivando do encapsulamento de quebras de classes :
Entre outras coisas, isso dificulta a refatoração do seu código.
As classes são um detalhe de implementação
Classes são detalhes de implementação que devem ser ocultados de outras partes do seu código. Em resumo, a
System.List
é uma implementação específica de um tipo de dado abstrato, que pode ou não ser apropriado agora e no futuro.Conceitualmente, o fato de o
System.List
tipo de dados ser chamado de "lista" é um pouco complicado. ASystem.List<T>
é uma coleção ordenada mutável que suporta operações O (1) amortizadas para adicionar, inserir e remover elementos e operações O (1) para recuperar o número de elementos ou obter e definir elemento pelo índice.Quanto menor a interface, mais flexível o código
Ao projetar uma estrutura de dados, quanto mais simples a interface, mais flexível o código. Veja como o LINQ é poderoso para uma demonstração disso.
Como escolher interfaces
Quando você pensa em "lista", deve começar dizendo a si mesmo: "Preciso representar uma coleção de jogadores de beisebol". Então, digamos que você decida modelar isso com uma classe. O que você deve fazer primeiro é decidir qual a quantidade mínima de interfaces que essa classe precisará expor.
Algumas perguntas que podem ajudar a guiar esse processo:
IEnumerable<T>
IReadonlyList<T>
.ICollection<T>
ISet<T>
?IList<T>
.Dessa forma, você não acoplará outras partes do código aos detalhes de implementação da sua coleção de jogadores de beisebol e poderá alterar como ele será implementado, desde que respeite a interface.
Ao adotar essa abordagem, você descobrirá que o código se torna mais fácil de ler, refatorar e reutilizar.
Notas sobre como evitar o Boilerplate
A implementação de interfaces em um IDE moderno deve ser fácil. Clique com o botão direito e escolha "Implementar Interface". Em seguida, encaminhe todas as implementações para uma classe de membro, se necessário.
Dito isto, se você achar que está escrevendo muitos clichês, isso é potencialmente porque você está expondo mais funções do que deveria. É o mesmo motivo que você não deve herdar de uma classe.
Você também pode criar interfaces menores que façam sentido para o seu aplicativo e talvez apenas algumas funções de extensão auxiliar para mapear essas interfaces para outras que você precisar. Essa é a abordagem que adotei em minha própria
IArray
interface para a biblioteca LinqArray .fonte
Problemas com serialização
Está faltando um aspecto. Classes herdadas da Lista <> não podem ser serializadas corretamente usando XmlSerializer . Nesse caso, o DataContractSerializer deve ser usado ou uma implementação de serialização própria é necessária.
Leitura adicional: Quando uma classe é herdada da Lista <>, o XmlSerializer não serializa outros atributos
fonte
Para citar Eric Lippert:
Por exemplo, você está cansado da ausência do
AddRange
método emIList<T>
:fonte