Cuando planifico mis programas, a menudo empiezo con una cadena de pensamiento como esta:
Un equipo de fútbol es solo una lista de jugadores de fútbol. Por lo tanto, debería representarlo con:
var football_team = new List<FootballPlayer>();
El orden de esta lista representa el orden en el que los jugadores figuran en la lista.
Pero luego me doy cuenta de que los equipos también tienen otras propiedades, además de la mera lista de jugadores, que deben registrarse. Por ejemplo, el total acumulado de puntajes esta temporada, el presupuesto actual, los colores uniformes, string
el nombre del equipo, etc.
Entonces pienso:
De acuerdo, un equipo de fútbol es como una lista de jugadores, pero además tiene un nombre (a
string
) y un total de puntajes (aint
). .NET no proporciona una clase para almacenar equipos de fútbol, por lo que haré mi propia clase. La estructura existente más similar y relevante esList<FootballPlayer>
, por lo que heredaré de ella:class FootballTeam : List<FootballPlayer> { public string TeamName; public int RunningTotal }
Pero resulta que una guía dice que no debes heredar deList<T>
. Estoy completamente confundido por esta directriz en dos aspectos.
Por qué no?
Al parecer, de List
alguna manera está optimizado para el rendimiento . ¿Cómo es eso? ¿Qué problemas de rendimiento causaré si extiendo List
? ¿Qué se romperá exactamente?
Otra razón que he visto es que List
es proporcionada por Microsoft, y no tengo control sobre ella, por lo que no puedo cambiarla más tarde, después de exponer una "API pública" . Pero me cuesta entender esto. ¿Qué es una API pública y por qué debería importarme? Si mi proyecto actual no tiene y es probable que nunca tenga esta API pública, ¿puedo ignorar esta guía de manera segura? Si heredo de List
y resulta que necesito una API pública, ¿qué dificultades tendré?
¿Por qué es tan importante? Una lista es una lista. ¿Qué podría cambiar? ¿Qué podría querer cambiar?
Y, por último, si Microsoft no quería que heredara List
, ¿por qué no llegaron a la clase sealed
?
¿Qué más se supone que debo usar?
Aparentemente, para colecciones personalizadas, Microsoft ha proporcionado una Collection
clase que debería ampliarse en lugar de List
. Pero esta clase es muy básica y no tiene muchas cosas útiles, comoAddRange
por ejemplo. La respuesta de jvitor83 proporciona una lógica de rendimiento para ese método en particular, pero ¿cómo es que un lento AddRange
no es mejor que no AddRange
?
Heredar de Collection
es mucho más trabajo que heredar de List
, y no veo ningún beneficio. Seguramente Microsoft no me diría que hiciera un trabajo extra sin ninguna razón, por lo que no puedo evitar sentir que de alguna manera estoy malinterpretando algo, y que heredar Collection
no es la solución correcta para mi problema.
He visto sugerencias como la implementación IList
. Simplemente no. Estas son docenas de líneas de código repetitivo que no me dan nada.
Por último, algunos sugieren envolverlo List
en algo:
class FootballTeam
{
public List<FootballPlayer> Players;
}
Hay dos problemas con esto:
Hace que mi código sea innecesariamente detallado. Ahora debo llamar en
my_team.Players.Count
lugar de solomy_team.Count
. Afortunadamente, con C # puedo definir indexadores para hacer que la indexación sea transparente y reenviar todos los métodos internosList
... ¡Pero eso es mucho código! ¿Qué obtengo por todo ese trabajo?Simplemente no tiene ningún sentido. Un equipo de fútbol no "tiene" una lista de jugadores. Que es la lista de jugadores. No dices "John McFootballer se ha unido a los jugadores de SomeTeam". Dices "John se ha unido a SomeTeam". No agrega una letra a "los caracteres de una cadena", agrega una letra a una cadena. No agrega un libro a los libros de una biblioteca, agrega un libro a una biblioteca.
Me doy cuenta de que se puede decir que lo que sucede "bajo el capó" es "agregar X a la lista interna de Y", pero esto parece una forma muy intuitiva de pensar sobre el mundo.
Mi pregunta (resumida)
¿Cuál es la forma correcta de C # de representar una estructura de datos, que, "lógicamente" (es decir, "para la mente humana") es solo una list
de las things
pocas campanas y silbatos?
¿Heredar de List<T>
siempre es inaceptable? ¿Cuándo es aceptable? ¿Por qué por qué no? ¿Qué debe tener en cuenta un programador al decidir si heredar List<T>
o no?
string
requiere A para hacer todo lo que seobject
puede hacer y más .Respuestas:
Hay algunas buenas respuestas aquí. Les agregaría los siguientes puntos.
Pida a diez personas que no sean programadores de computadoras que estén familiarizadas con la existencia del fútbol que completen el espacio en blanco:
¿ Alguien dijo "lista de jugadores de fútbol con algunas campanas y silbatos", o todos dijeron "equipo deportivo" o "club" u "organización"? Su idea de que un equipo de fútbol es un tipo particular de lista de jugadores está en su mente humana y solo en su mente humana.
List<T>
Es un mecanismo . El equipo de fútbol es un objeto comercial , es decir, un objeto que representa un concepto que se encuentra en el dominio comercial del programa. ¡No mezcles esos! Un equipo de fútbol es una especie de equipo; que tiene una lista, una lista es una lista de jugadores . Una lista no es un tipo particular de lista de jugadores . Una lista es una lista de jugadores. Entonces haga una propiedad llamadaRoster
que es aList<Player>
. Y hágaloReadOnlyList<Player>
mientras lo hace, a menos que crea que todos los que conocen un equipo de fútbol eliminan a los jugadores de la lista.Inaceptable para quien? ¿Yo? No.
Cuando estás construyendo un mecanismo que extiende el
List<T>
mecanismo .¿Estoy construyendo un mecanismo o un objeto comercial ?
Pasó más tiempo escribiendo su pregunta que le habría llevado escribir métodos de reenvío para los miembros relevantes de
List<T>
cincuenta veces. Claramente no tienes miedo de la verbosidad, y estamos hablando de una cantidad muy pequeña de código aquí; Esto es unos minutos de trabajo.ACTUALIZAR
Pensé un poco más y hay otra razón para no modelar un equipo de fútbol como una lista de jugadores. De hecho, podría ser una mala idea modelar un equipo de fútbol con una lista de jugadores también. El problema con un equipo como / tener una lista de jugadores es que lo que tienes es una instantánea del equipo en un momento dado . No sé cuál es su caso de negocios para esta clase, pero si tuviera una clase que representara a un equipo de fútbol, me gustaría hacerle preguntas como "¿cuántos jugadores de los Seahawks se perdieron los juegos debido a una lesión entre 2003 y 2013?" o "¿Qué jugador de Denver que jugó anteriormente para otro equipo tuvo el mayor aumento de yardas corridas año tras año?" o " ¿Los Piggers fueron todo este año? "
Es decir, me parece que un equipo de fútbol está bien modelado como una colección de hechos históricos , como cuando un jugador fue reclutado, lesionado, retirado, etc. Obviamente, la lista actual de jugadores es un hecho importante que probablemente debería estar al frente y ... centro, pero puede haber otras cosas interesantes que desee hacer con este objeto que requieran una perspectiva más histórica.
fuente
IEnumerable<T>
- una secuencia ?Wow, tu publicación tiene una gran cantidad de preguntas y puntos. La mayor parte del razonamiento que obtiene de Microsoft es exactamente el punto. Comencemos con todo sobre
List<T>
List<T>
Está altamente optimizado. Su uso principal es ser utilizado como miembro privado de un objeto.class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }
. Ahora es tan fácil como hacerlovar list = new MyList<int, string>();
.IList<T>
si necesita que algún consumidor tenga una lista indexada. Esto le permite cambiar la implementación dentro de una clase más adelante.Collection<T>
muy genérico porque es un concepto genérico ... el nombre lo dice todo; Es solo una colección. Hay versiones más precisas, tales comoSortedCollection<T>
,ObservableCollection<T>
,ReadOnlyCollection<T>
, etc. cada uno de los cuales implementanIList<T>
pero noList<T>
.Collection<T>
permite que los miembros (es decir, Agregar, Eliminar, etc.) se anulen porque son virtuales.List<T>
no.Si estuviera escribiendo este código, la clase probablemente se vería así:
fuente
List<T>
se extiende como una clase interna privada que usa genéricos para simplificar el uso y la declaración deList<T>
. Si la extensión fuera simplementeclass FootballRoster : List<FootballPlayer> { }
, entonces sí, podría haber usado un alias en su lugar. Supongo que vale la pena señalar que podría agregar miembros / funcionalidad adicionales, pero es limitado ya que no puede anular miembros importantes deList<T>
.Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden
Ahora esa es una buena razón para no heredar de la Lista. Las otras respuestas tienen demasiada filosofía.El código anterior significa: un grupo de chicos de la calle jugando al fútbol, y tienen un nombre. Algo como:
De todos modos, este código (de la respuesta de mi)
Significa: este es un equipo de fútbol que tiene directivos, jugadores, administradores, etc. Algo así como:
Así es como se presenta su lógica en imágenes ...
fuente
private readonly List<T> _roster = new List<T>(44);
System.Linq
espacio de nombres.Este es un ejemplo clásico de composición vs herencia .
En este caso específico:
¿Es el equipo una lista de jugadores con comportamiento adicional?
o
¿Es el equipo un objeto en sí mismo que contiene una lista de jugadores?
Al extender la Lista, te estás limitando de varias maneras:
No puede restringir el acceso (por ejemplo, evitar que las personas cambien la lista). Obtiene todos los métodos de Lista, ya sea que los necesite / quiera todos o no.
¿Qué sucede si quieres tener listas de otras cosas también? Por ejemplo, los equipos tienen entrenadores, gerentes, fanáticos, equipos, etc. Algunos de ellos podrían ser listas por derecho propio.
Limitas tus opciones de herencia. Por ejemplo, es posible que desee crear un objeto Team genérico y luego tener BaseballTeam, FootballTeam, etc., que hereden de eso. Para heredar de la Lista, debe hacer la herencia del Equipo, pero eso significa que todos los diversos tipos de equipo están obligados a tener la misma implementación de esa lista.
Composición: incluye un objeto que proporciona el comportamiento que desea dentro de su objeto.
Herencia: su objeto se convierte en una instancia del objeto que tiene el comportamiento que desea.
Ambos tienen sus usos, pero este es un caso claro donde la composición es preferible.
fuente
Como todos han señalado, un equipo de jugadores no es una lista de jugadores. Este error es cometido por muchas personas en todas partes, tal vez en varios niveles de experiencia. A menudo, el problema es sutil y ocasionalmente muy grave, como en este caso. Tales diseños son malos porque violan el Principio de sustitución de Liskov . Internet tiene muchos buenos artículos que explican este concepto, por ejemplo, http://en.wikipedia.org/wiki/Liskov_substitution_principle
En resumen, hay dos reglas que deben preservarse en una relación padre / hijo entre clases:
En otras palabras, un padre es una definición necesaria de un niño, y un niño es una definición suficiente de un padre.
Aquí hay una manera de pensar a través de las soluciones y aplicar el principio anterior que debería ayudarlo a evitar tal error. Uno debería probar la hipótesis de uno verificando si todas las operaciones de una clase padre son válidas para la clase derivada tanto estructural como semánticamente.
Como puede ver, solo la primera característica de una lista es aplicable a un equipo. Por lo tanto, un equipo no es una lista. Una lista sería un detalle de implementación de cómo administra su equipo, por lo que solo debe usarse para almacenar los objetos del jugador y manipularse con métodos de clase de equipo.
En este punto, me gustaría comentar que, en mi opinión, una clase de equipo ni siquiera debería implementarse utilizando una lista; debe implementarse utilizando una estructura de datos Set (HashSet, por ejemplo) en la mayoría de los casos.
fuente
¿Qué pasa si el
FootballTeam
tiene un equipo de reservas junto con el equipo principal?¿Cómo modelarías eso?
La relación es claramente tiene un y no es un .
o
RetiredPlayers
?Como regla general, si alguna vez desea heredar de una colección, asigne un nombre a la clase
SomethingCollection
.¿Tiene
SomethingCollection
sentido semánticamente? Solo haga esto si su tipo es una colección deSomething
.En el caso de
FootballTeam
que no suene bien. ATeam
es más que aCollection
. ATeam
puede tener entrenadores, entrenadores, etc., como han señalado las otras respuestas.FootballCollection
Suena como una colección de pelotas de fútbol o tal vez una colección de parafernalia de fútbol.TeamCollection
, una colección de equipos.FootballPlayerCollection
suena como una colección de jugadores que sería un nombre válido para una clase que heredaList<FootballPlayer>
si realmente quisieras hacer eso.Realmente
List<FootballPlayer>
es un tipo perfectamente bueno para tratar. Tal vezIList<FootballPlayer>
si lo devuelve desde un método.En resumen
Pregúntese
Es
X
unY
? o tieneX
unY
?¿Los nombres de mis clases significan lo que son?
fuente
DefensivePlayers
,OffensivePlayers
oOtherPlayers
, podría ser legítimamente útil tener un tipo que podría ser utilizado por un código que esperaList<Player>
, sino también los miembros incluidosDefensivePlayers
,OffsensivePlayers
oSpecialPlayers
del tipoIList<DefensivePlayer>
,IList<OffensivePlayer>
yIList<Player>
. Uno podría usar un objeto separado para almacenar en caché las listas separadas, pero encapsularlas dentro del mismo objeto que la lista principal parecería más limpio [use la invalidación de un enumerador de listas ...Diseño> Implementación
Qué métodos y propiedades expones es una decisión de diseño. De qué clase base hereda es un detalle de implementación. Siento que vale la pena dar un paso atrás al primero.
Un objeto es una colección de datos y comportamiento.
Entonces sus primeras preguntas deberían ser:
Tenga en cuenta que la herencia implica una relación "isa" (es una), mientras que la composición implica una relación "tiene una" (hasa). Desde su punto de vista, elija el adecuado para su situación, teniendo en cuenta dónde podrían ir las cosas a medida que evoluciona su aplicación.
Considere pensar en interfaces antes de pensar en tipos concretos, ya que a algunas personas les resulta más fácil poner su cerebro en "modo de diseño" de esa manera.
Esto no es algo que todos hagan conscientemente a este nivel en la codificación diaria. Pero si estás reflexionando sobre este tipo de tema, estás pisando en aguas de diseño. Ser consciente de ello puede ser liberador.
Considere los detalles del diseño
Eche un vistazo a Lista <T> e ILista <T> en MSDN o Visual Studio. Vea qué métodos y propiedades exponen. ¿Todos estos métodos se parecen a algo que alguien quisiera hacer con un equipo de fútbol en tu opinión?
¿Tiene sentido footballTeam.Reverse () para ti? ¿FootballTeam.ConvertAll <TOutput> () se parece a algo que quieres?
Esta no es una pregunta capciosa; la respuesta podría ser realmente "sí". Si implementa / hereda List <Player> o IList <Player>, está atascado con ellos; si eso es ideal para tu modelo, hazlo.
Si decide que sí, tiene sentido, y desea que su objeto sea tratable como una colección / lista de jugadores (comportamiento), y por lo tanto desea implementar ICollection o IList, hágalo de todas maneras. Nocionalmente:
Si desea que su objeto contenga una colección / lista de jugadores (datos) y, por lo tanto, desea que la colección o lista sea una propiedad o un miembro, hágalo por todos los medios. Nocionalmente:
Puede sentir que desea que las personas solo puedan enumerar el conjunto de jugadores, en lugar de contarlos, agregarlos o eliminarlos. IEnumerable <Player> es una opción perfectamente válida a tener en cuenta.
Puede sentir que ninguna de estas interfaces es útil en su modelo. Esto es menos probable (IEnumerable <T> es útil en muchas situaciones) pero aún es posible.
Cualquiera que intente decirte que uno de estos es categórica y definitivamente incorrecto en todos los casos está equivocado. Cualquiera que intente decirte que es categórica y definitivamente correcto en cada caso está equivocado.
Pasar a la implementación
Una vez que haya decidido los datos y el comportamiento, puede tomar una decisión sobre la implementación. Esto incluye las clases concretas de las que depende por herencia o composición.
Esto puede no ser un gran paso, y las personas a menudo combinan diseño e implementación, ya que es muy posible ejecutarlo todo en su cabeza en un segundo o dos y comenzar a escribir.
Un experimento mental
Un ejemplo artificial: como otros han mencionado, un equipo no siempre es "solo" una colección de jugadores. ¿Mantiene una colección de puntajes de partidos para el equipo? ¿El equipo es intercambiable con el club, en su modelo? Si es así, y si su equipo es una colección de jugadores, quizás también sea una colección de personal y / o una colección de puntajes. Entonces terminas con:
A pesar del diseño, en este punto en C # no podrá implementar todo esto heredando de la Lista <T> de todos modos, ya que C # "solo" admite herencia única. (Si ha probado esta malarky en C ++, puede considerar esto como una buena cosa). Implementar una colección por herencia y otra por composición es probable que se sienta sucio. Y propiedades como Count se vuelven confusas para los usuarios a menos que implemente ILIst <Player> .Count e IList <StaffMember> .Count etc. explícitamente, y luego son dolorosas en lugar de confusas. Puedes ver a dónde va esto; El presentimiento al pensar en esta avenida bien puede decirle que se siente mal ir en esta dirección (y con razón o sin razón, ¡sus colegas también podrían hacerlo si lo implementa de esta manera!)
La respuesta corta (demasiado tarde)
La directriz sobre no heredar de las clases de colección no es específica de C #, la encontrará en muchos lenguajes de programación. Se recibe sabiduría, no una ley. Una razón es que, en la práctica, se considera que la composición a menudo prevalece sobre la herencia en términos de comprensión, implementabilidad y mantenibilidad. Es más común con los objetos del mundo real / dominio encontrar relaciones "hasa" útiles y consistentes que las relaciones "isa" útiles y consistentes a menos que sea profundo en lo abstracto, especialmente a medida que pasa el tiempo y los datos precisos y el comportamiento de los objetos en el código cambios Esto no debería hacer que siempre descarte heredar de las clases de colección; Pero puede ser sugerente.
fuente
En primer lugar, tiene que ver con la usabilidad. Si usa la herencia, la
Team
clase expondrá comportamientos (métodos) diseñados exclusivamente para la manipulación de objetos. Por ejemplo,AsReadOnly()
o losCopyTo(obj)
métodos no tienen sentido para el objeto del equipo. En lugar delAddRange(items)
método, es probable que desee un método más descriptivo.AddPlayers(players)
método .Si desea utilizar LINQ, implemente una interfaz genérica como
ICollection<T>
oIEnumerable<T>
tendría más sentido.Como se mencionó, la composición es la forma correcta de hacerlo. Simplemente implemente una lista de jugadores como una variable privada.
fuente
Un equipo de fútbol no es una lista de jugadores de fútbol. Un equipo de fútbol está compuesto por una lista de jugadores de fútbol!
Esto es lógicamente incorrecto:
y esto es correcto:
fuente
depende del contexto
Cuando considera a su equipo como una lista de jugadores, está proyectando la "idea" de un equipo de balón de fútbol en un aspecto: reduce el "equipo" a las personas que ve en el campo. Esta proyección solo es correcta en un determinado contexto. En un contexto diferente, esto podría estar completamente equivocado. Imagina que quieres convertirte en patrocinador del equipo. Entonces tienes que hablar con los gerentes del equipo. En este contexto, el equipo se proyecta a la lista de sus gerentes. Y estas dos listas generalmente no se superponen demasiado. Otros contextos son los jugadores actuales frente a los anteriores, etc.
Semántica poco clara
Entonces, el problema de considerar a un equipo como una lista de sus jugadores es que su semántica depende del contexto y que no se puede extender cuando el contexto cambia. Además, es difícil expresar qué contexto está utilizando.
Las clases son extensibles.
Cuando usa una clase con un solo miembro (p. Ej.
IList activePlayers
), puede usar el nombre del miembro (y adicionalmente su comentario) para aclarar el contexto. Cuando hay contextos adicionales, simplemente agrega un miembro adicional.Las clases son mas complejas
En algunos casos, puede ser excesivo crear una clase adicional. Cada definición de clase debe cargarse a través del cargador de clases y la máquina virtual la almacenará en caché. Esto le cuesta rendimiento en tiempo de ejecución y memoria. Cuando tienes un contexto muy específico, podría estar bien considerar a un equipo de fútbol como una lista de jugadores. Pero en este caso, deberías usar un
IList
, no una clase derivada de él.Conclusión / Consideraciones
Cuando tienes un contexto muy específico, está bien considerar a un equipo como una lista de jugadores. Por ejemplo, dentro de un método, está completamente bien escribir:
Al usar F #, incluso puede estar bien crear una abreviatura de tipo:
Pero cuando el contexto es más amplio o incluso poco claro, no debe hacer esto. Este es especialmente el caso cuando crea una nueva clase cuyo contexto en el que se puede utilizar en el futuro no está claro. Una señal de advertencia es cuando comienza a agregar atributos adicionales a su clase (nombre del equipo, entrenador, etc.). Esta es una señal clara de que el contexto donde se usará la clase no es fijo y cambiará en el futuro. En este caso, no puede considerar al equipo como una lista de jugadores, pero debe modelar la lista de jugadores (actualmente activos, no lesionados, etc.) como un atributo del equipo.
fuente
Déjame reescribir tu pregunta. entonces puede ver el tema desde una perspectiva diferente.
Cuando necesito representar a un equipo de fútbol, entiendo que es básicamente un nombre. Como: "Las águilas"
Luego, me di cuenta de que los equipos también tienen jugadores.
¿Por qué no puedo simplemente extender el tipo de cadena para que también contenga una lista de jugadores?
Su punto de entrada al problema es arbitrario. Intenta pensar qué hace un equipo tiene (propiedades), no lo que es .
Después de hacer eso, podría ver si comparte propiedades con otras clases. Y piensa en la herencia.
fuente
Solo porque creo que las otras respuestas se disparan por una tangente de si un equipo de fútbol "es-a"
List<FootballPlayer>
o "tiene-a"List<FootballPlayer>
, lo que realmente no responde a esta pregunta tal como está escrita.El OP principalmente solicita aclaraciones sobre las pautas para heredar de
List<T>
:Porque
List<T>
no tiene métodos virtuales. Este es un problema menor en su propio código, ya que generalmente puede cambiar la implementación con relativamente poco dolor, pero puede ser mucho más importante en una API pública.Una API pública es una interfaz que expone a programadores de terceros. Piensa en el código marco. Y recuerde que las pautas a las que se hace referencia son las " Pautas de diseño de .NET Framework " y no las " Pautas de diseño de aplicaciones .NET ". Hay una diferencia y, en términos generales, el diseño de API pública es mucho más estricto.
Bastante sí. Es posible que desee considerar la justificación detrás de esto para ver si se aplica a su situación de todos modos, pero si no está creando una API pública, entonces no necesita preocuparse particularmente por las preocupaciones de la API como el control de versiones (de las cuales, esta es una subconjunto).
Si agrega una API pública en el futuro, deberá abstraer su API de su implementación (al no exponerla
List<T>
directamente) o violar las pautas con el posible dolor futuro que conlleva.Depende del contexto, pero dado que lo estamos usando
FootballTeam
como ejemplo, imagine que no puede agregar unFootballPlayer
si causaría que el equipo supere el límite salarial. Una posible forma de agregar eso sería algo como:Ah ... pero no puedes
override Add
porque no esvirtual
(por razones de rendimiento).Si está en una aplicación (que, básicamente, significa que usted y todas sus personas que llaman están compiladas juntas), ahora puede cambiar a usar
IList<T>
y corregir cualquier error de compilación:pero, si se ha expuesto públicamente a un tercero, acaba de realizar un cambio importante que provocará errores de compilación y / o tiempo de ejecución.
TL; DR: las pautas son para API públicas. Para API privadas, haga lo que quiera.
fuente
¿Permitir que la gente diga
tiene sentido? Si no, entonces no debería ser una Lista.
fuente
myTeam.subTeam(3, 5);
subList
ya ni siquiera devolverá unTeam
.Depende del comportamiento de su objeto "equipo". Si se comporta como una colección, podría estar bien representarlo primero con una Lista simple. Entonces puede comenzar a notar que sigue duplicando el código que itera en la lista; en este punto tienes la opción de crear un objeto FootballTeam que envuelva la lista de jugadores. La clase FootballTeam se convierte en el hogar de todo el código que itera en la lista de jugadores.
Encapsulación Sus clientes no necesitan saber qué sucede dentro de FootballTeam. Para que todos sus clientes lo sepan, podría implementarse buscando la lista de jugadores en una base de datos. No necesitan saberlo, y esto mejora su diseño.
Exactamente :) dirás footballTeam.Add (john), no footballTeam.List.Add (john). La lista interna no será visible.
fuente
Aquí hay muchas respuestas excelentes, pero quiero tocar algo que no vi mencionado: el diseño orientado a objetos se trata de potenciar objetos .
Desea encapsular todas sus reglas, trabajo adicional y detalles internos dentro de un objeto apropiado. De esta manera, otros objetos que interactúan con este no tienen que preocuparse por todo. De hecho, desea ir un paso más allá y evitar activamente que otros objetos pasen por alto estos elementos internos.
Cuando hereda de
List
, todos los demás objetos pueden verlo como una Lista. Tienen acceso directo a los métodos para agregar y eliminar jugadores. Y habrás perdido tu control; por ejemplo:Suponga que desea diferenciar cuando un jugador se va sabiendo si se retiró, renunció o fue despedido. Podría implementar un
RemovePlayer
método que tome una enumeración de entrada adecuada. Sin embargo, al heredar deList
, no podrá evitar el acceso directo aRemove
,RemoveAll
e inclusoClear
. Como resultado, en realidad has quitado el poder a tuFootballTeam
clase.Pensamientos adicionales sobre la encapsulación ... Usted planteó la siguiente preocupación:
Tienes razón, eso sería innecesariamente detallado para que todos los clientes utilicen tu equipo. Sin embargo, ese problema es muy pequeño en comparación con el hecho de que te has expuesto
List Players
a todos y cada uno para que puedan jugar con tu equipo sin tu consentimiento.Sigues diciendo:
Estás equivocado sobre el primer bit: suelta la palabra 'lista', y en realidad es obvio que un equipo tiene jugadores.
Sin embargo, golpeas el clavo en la cabeza con el segundo. No quieres que los clientes llamen
ateam.Players.Add(...)
. Los quieres llamandoateam.AddPlayer(...)
. Y su implementación podría (posiblemente entre otras cosas) llamarPlayers.Add(...)
internamente.Esperemos que pueda ver cuán importante es la encapsulación para el objetivo de potenciar sus objetos. Desea permitir que cada clase haga bien su trabajo sin temor a la interferencia de otros objetos.
fuente
Recuerde: "Todos los modelos están equivocados, pero algunos son útiles". - George EP Box
No existe un "camino correcto", solo uno útil.
Elija uno que sea útil para usted y / o sus usuarios. Eso es.
Desarrollar económicamente, no sobre-ingeniero. Cuanto menos código escriba, menos código tendrá que depurar.(lea las siguientes ediciones).- Editado
Mi mejor respuesta sería ... depende. Heredar de una Lista expondría a los clientes de esta clase a métodos que podrían no estar expuestos, principalmente porque FootballTeam parece una entidad comercial.
- Edición 2
Sinceramente, no recuerdo a qué me refería en el comentario "no sobre-ingeniero". Si bien creo que la mentalidad de KISS es una buena guía, quiero enfatizar que heredar una clase de negocios de List crearía más problemas de los que resuelve, debido a la fuga de abstracción .
Por otro lado, creo que hay un número limitado de casos en los que simplemente es útil heredar de List. Como escribí en la edición anterior, depende. La respuesta a cada caso está fuertemente influenciada por el conocimiento, la experiencia y las preferencias personales.
Gracias a @kai por ayudarme a pensar con mayor precisión sobre la respuesta.
fuente
List
expondría a los clientes de esta clase a métodos que podrían no exponerse " es precisamente lo que hace que heredar deList
un absoluto no-no. Una vez expuestos, gradualmente son maltratados y mal utilizados con el tiempo. Ahorrar tiempo ahora "desarrollándose económicamente" puede conducir fácilmente a diez veces el ahorro perdido en el futuro: depurar los abusos y eventualmente refactorizar para reparar la herencia. El principio YAGNI también se puede considerar como significado: no necesitarás todos esos métodosList
, así que no los expongas.List
porque es rápido y fácil y, por lo tanto, conduciría a menos errores es simplemente incorrecto, es solo un mal diseño y un mal diseño conduce a muchos más errores que "sobre ingeniería".Esto me recuerda que "Es un" versus "tiene un" compromiso. A veces es más fácil y tiene más sentido heredar directamente de una superclase. Otras veces tiene más sentido crear una clase independiente e incluir la clase de la que habría heredado como variable miembro. Todavía puede acceder a la funcionalidad de la clase, pero no está vinculado a la interfaz ni a ninguna otra restricción que pueda derivarse de la herencia de la clase.
Que haces Como con muchas cosas ... depende del contexto. La guía que usaría es que para heredar de otra clase realmente debería haber una relación "es una". Entonces, si escribe una clase llamada BMW, podría heredar de Car porque un BMW realmente es un automóvil. Una clase Horse puede heredar de la clase Mammal porque un caballo en realidad es un mamífero en la vida real y cualquier funcionalidad de Mammal debería ser relevante para Horse. Pero, ¿puedes decir que un equipo es una lista? Por lo que puedo decir, no parece que un Equipo realmente "sea una" Lista. Entonces, en este caso, tendría una Lista como una variable miembro.
fuente
Mi secreto sucio: no me importa lo que diga la gente, y lo hago. .NET Framework se distribuye con "XxxxCollection" (ejemplo UIElementCollection).
Entonces, ¿qué me impide decir:
Cuando lo encuentro mejor que
Además, mi PlayerCollection puede ser utilizada por otra clase, como "Club" sin duplicación de código.
Las mejores prácticas de ayer, podrían no ser las de mañana. No hay ninguna razón detrás de la mayoría de las mejores prácticas, la mayoría son solo un amplio acuerdo entre la comunidad. En lugar de preguntarle a la comunidad si te culpará cuando hagas eso, pregúntate a ti mismo, ¿qué es más fácil de leer y mantener?
o
De Verdad. ¿Tienes alguna duda? Ahora quizás necesite jugar con otras restricciones técnicas que le impiden usar List <T> en su caso de uso real. Pero no agregue una restricción que no debería existir. Si Microsoft no documentó el por qué, entonces seguramente es una "mejor práctica" que viene de la nada.
fuente
Collection<T>
no es lo mismo, porList<T>
lo que puede requerir bastante trabajo verificar en un proyecto de gran tamaño.team.ByName("Nicolas")
significa"Nicolas"
es el nombre del equipo.Lo que dicen las pautas es que la API pública no debe revelar la decisión de diseño interno de si está utilizando una lista, un conjunto, un diccionario, un árbol o lo que sea. Un "equipo" no es necesariamente una lista. Puede implementarlo como una lista, pero los usuarios de su API pública deben usar su clase según sea necesario. Esto le permite cambiar su decisión y usar una estructura de datos diferente sin afectar la interfaz pública.
fuente
class FootballTeam : List<FootballPlayers>
, los usuarios de mi clase podrán decirme ' he heredado deList<T>
mediante el uso de la reflexión, al ver losList<T>
métodos que no tienen sentido para un equipo de fútbol, y ser capaz de utilizarFootballTeam
enList<T>
, por lo que me gustaría ser reveladores detalles de implementación para el cliente (innecesariamente).Cuando dicen que
List<T>
está "optimizado", creo que quieren decir que no tiene características como los métodos virtuales, que son un poco más caras. Entonces, el problema es que una vez que exponeList<T>
en su API pública , pierde la capacidad de hacer cumplir las reglas comerciales o personalizar su funcionalidad más adelante. Pero si está utilizando esta clase heredada como interna dentro de su proyecto (en lugar de estar potencialmente expuesta a miles de sus clientes / socios / otros equipos como API), entonces puede estar bien si le ahorra tiempo y es la funcionalidad que desea. duplicar. La ventaja de heredar de por vida de sus API también puede estar bien.List<T>
es que elimina mucho código de envoltorio tonto que nunca se personalizará en un futuro previsible. Además, si desea que su clase tenga explícitamente la misma semántica exacta queList<T>
A menudo veo a muchas personas haciendo toneladas de trabajo extra solo porque la regla de FxCop lo dice o el blog de alguien dice que es una práctica "mala". Muchas veces, esto convierte el código en el diseño de la rareza del patrón palooza. Al igual que con muchas pautas, trátelas como pautas que pueden tener excepciones.
fuente
Si bien no tengo una comparación compleja como la mayoría de estas respuestas, me gustaría compartir mi método para manejar esta situación. Al ampliar
IEnumerable<T>
, puede permitir que suTeam
clase admita extensiones de consulta de Linq, sin exponer públicamente todos los métodos y propiedades deList<T>
.fuente
Si los usuarios de su clase necesitan todos los métodos y propiedades que tiene ** List, debe derivar su clase de ella. Si no los necesitan, incluya la Lista y cree envoltorios para los métodos que los usuarios de su clase realmente necesitan.
Esta es una regla estricta, si escribe una API pública , o cualquier otro código que será utilizado por muchas personas. Puede ignorar esta regla si tiene una aplicación pequeña y no más de 2 desarrolladores. Esto te ahorrará algo de tiempo.
Para aplicaciones pequeñas, también puede considerar elegir otro lenguaje menos estricto. Ruby, JavaScript: cualquier cosa que le permita escribir menos código.
fuente
Solo quería agregar que Bertrand Meyer, el inventor de Eiffel y el diseño por contrato, habría
Team
heredadoList<Player>
sin tan siquiera pestañear.En su libro, Construcción de software orientado a objetos , analiza la implementación de un sistema GUI donde las ventanas rectangulares pueden tener ventanas secundarias. Simplemente ha
Window
heredado de ambosRectangle
yTree<Window>
para reutilizar la implementación.Sin embargo, C # no es Eiffel. Este último admite herencia múltiple y renombrar características . En C #, cuando subclases, heredas tanto la interfaz como la implementación. Puede anular la implementación, pero las convenciones de llamada se copian directamente de la superclase. En Eiffel, sin embargo, puede modificar los nombres de los métodos públicos, por lo que puede cambiar el nombre
Add
yRemove
enHire
yFire
en suTeam
. Si una instancia deTeam
está de vuelta a upcastList<Player>
, el llamante usarAdd
yRemove
modificar, pero sus métodos virtualesHire
yFire
será llamado.fuente
Creo que no estoy de acuerdo con tu generalización. Un equipo no es solo una colección de jugadores. Un equipo tiene mucha más información al respecto: nombre, emblema, colección de personal administrativo / administrativo, colección de entrenadores y luego colección de jugadores. Por lo tanto, su clase FootballTeam debería tener 3 colecciones y no ser una colección en sí misma; si es para modelar adecuadamente el mundo real.
Podría considerar una clase PlayerCollection que, al igual que Specialized StringCollection, ofrece otras funciones, como la validación y las comprobaciones antes de agregar o quitar objetos del almacén interno.
Tal vez, ¿la noción de un jugador que apuesta mejor se adapta a su enfoque preferido?
Y luego el FootballTeam puede verse así:
fuente
Prefiere interfaces sobre clases
Las clases deben evitar derivar de las clases y en su lugar implementar las interfaces mínimas necesarias.
La herencia rompe la encapsulación
Derivar de clases rompe la encapsulación :
Entre otras cosas, esto hace que sea más difícil refactorizar su código.
Las clases son un detalle de implementación
Las clases son un detalle de implementación que debe ocultarse de otras partes de su código. En resumen, a
System.List
es una implementación específica de un tipo de datos abstractos, que puede o no ser apropiado ahora y en el futuro.Conceptualmente, el hecho de que el
System.List
tipo de datos se llame "lista" es un poco raro. ASystem.List<T>
es una colección ordenada mutable que admite operaciones O (1) amortizadas para agregar, insertar y eliminar elementos, y operaciones O (1) para recuperar el número de elementos u obtener y establecer elementos por índice.Cuanto más pequeña es la interfaz, más flexible es el código
Al diseñar una estructura de datos, cuanto más simple es la interfaz, más flexible es el código. Solo mire cuán poderoso es LINQ para una demostración de esto.
Cómo elegir interfaces
Cuando piense en "lista", debe comenzar diciéndose a sí mismo: "Necesito representar una colección de jugadores de béisbol". Entonces, digamos que decides modelar esto con una clase. Lo que debe hacer primero es decidir cuál es la cantidad mínima de interfaces que esta clase necesitará exponer.
Algunas preguntas que pueden ayudar a guiar este proceso:
IEnumerable<T>
IReadonlyList<T>
.ICollection<T>
ISet<T>
?IList<T>
.De esta manera, no vinculará otras partes del código con los detalles de implementación de su colección de jugadores de béisbol y podrá cambiar la forma en que se implementa siempre que respete la interfaz.
Al adoptar este enfoque, encontrará que el código se vuelve más fácil de leer, refactorizar y reutilizar.
Notas sobre cómo evitar la repetitiva
Implementar interfaces en un IDE moderno debería ser fácil. Haga clic derecho y seleccione "Implementar interfaz". Luego, reenvíe todas las implementaciones a una clase miembro si es necesario.
Dicho esto, si descubres que estás escribiendo mucho repetitivo, es potencialmente porque estás exponiendo más funciones de las que deberías. Es la misma razón por la que no deberías heredar de una clase.
También puede diseñar interfaces más pequeñas que tengan sentido para su aplicación, y tal vez solo un par de funciones de extensión auxiliar para asignar esas interfaces a cualquier otra que necesite. Este es el enfoque que tomé en mi propia
IArray
interfaz para la biblioteca LinqArray .fuente
Problemas con la serialización
Falta un aspecto. Las clases que heredan de List <> no se pueden serializar correctamente usando XmlSerializer . En ese caso, se debe utilizar DataContractSerializer o se necesita una implementación de serialización propia.
Lectura adicional: cuando una clase se hereda de List <>, XmlSerializer no serializa otros atributos
fuente
Para citar a Eric Lippert:
Por ejemplo, estás cansado de la ausencia del
AddRange
método enIList<T>
:fuente