c# - studio - ¿Por qué no heredar de la lista<T>?
visual studio c# herencia (26)
Cuando planifico mis programas, a menudo comienzo con una cadena de pensamiento como esta:
Un equipo de fútbol es sólo 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 que se enumeran los jugadores 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 de esta temporada, el presupuesto actual, los colores uniformes, una string
representa el nombre del equipo, etc.
Así que entonces pienso:
De acuerdo, un equipo de fútbol es como una lista de jugadores, pero además, tiene un nombre (una
string
) y un total acumulado de puntajes (unint
). .NET no proporciona una clase para almacenar equipos de fútbol, así que haré mi propia clase. La estructura existente más similar y relevante es laList<FootballPlayer>
, por lo que heredaré de ella:
class FootballTeam : List<FootballPlayer> { public string TeamName; public int RunningTotal }
Pero resulta que una directriz dice que no debe heredar de la List<T>
. Estoy completamente confundido por esta guía en dos aspectos.
Por qué no?
Al parecer, la List
está optimizada de alguna manera para el rendimiento . ¿Cómo es eso? ¿Qué problemas de rendimiento causaré si extiendo la List
? ¿Qué se romperá exactamente?
Otra razón que he visto es que Microsoft proporciona la List
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 lo tiene y es probable que nunca tenga esta API pública, ¿puedo ignorar esta guía de forma segura? Si heredo de List
y resulta que necesito una API pública, ¿qué dificultades tendré?
¿Por qué importa? Una lista es una lista. ¿Qué podría cambiar? ¿Qué podría querer cambiar?
Y, por último, si Microsoft no quería que heredara de List
, ¿por qué no sealed
la clase?
¿Qué más se supone que debo usar?
Al parecer, para colecciones personalizadas, Microsoft ha proporcionado una clase de Collection
que debería extenderse en lugar de List
. Pero esta clase es muy simple y no tiene muchas cosas útiles, como AddRange
, por ejemplo. La respuesta de jvitor83 proporciona una justificación de rendimiento para ese método en particular, pero ¿cómo un AddRange
lento no es mejor que ningún AddRange
?
Heredar de Collection
es mucho más trabajo que heredar de List
, y no veo ningún beneficio. Seguramente Microsoft no me dirá que haga un trabajo extra sin ninguna razón, por lo que no puedo evitar sentirme como si de alguna manera estoy malinterpretando algo, y heredar Collection
es realmente la solución adecuada para mi problema.
He visto sugerencias como implementar IList
. Simplemente no. Esto es docenas de líneas de código repetitivo que no me gana nada.
Por último, algunos sugieren envolver la List
en algo:
class FootballTeam
{
public List<FootballPlayer> Players;
}
Hay dos problemas con esto:
Hace mi código innecesariamente detallado. Ahora debo llamar a
my_team.Players.Count
lugar de amy_team.Count
. Afortunadamente, con C # puedo definir los indexadores para hacer que la indexación sea transparente y reenviar todos los métodos de laList
interna ... ¡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. Es la lista de jugadores. No dices "John McFootballer se ha unido a los jugadores de SomeTeam". Usted dice "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 lo que sucede "bajo el capó" puede decirse que es "agregar X a la lista interna de Y", pero esto parece ser una forma muy poco intuitiva de pensar sobre el mundo.
Mi pregunta (resumida)
¿Cuál es la forma correcta en C # de representar una estructura de datos que, "lógicamente" (es decir, "para la mente humana") es solo una list
de things
con algunas campanas y silbidos?
¿Es hereditario de la List<T>
siempre inaceptable? ¿Cuándo es aceptable? ¿Por qué por qué no? ¿Qué debe considerar un programador, al decidir si heredar de la List<T>
o no?
Por último, algunos sugieren envolver la Lista en algo:
Esa es la forma correcta. "Sin palabras" es una mala manera de ver esto. Tiene un significado explícito cuando escribes my_team.Players.Count
. Quieres contar los jugadores.
my_team.Count
..no significa nada. Cuenta que?
Un equipo no es una lista, consiste en más que una lista de jugadores. Un equipo posee jugadores, por lo que los jugadores deben ser parte de él (un miembro).
Si realmente te preocupa que sea demasiado detallado, siempre puedes exponer las propiedades del equipo:
public int PlayerCount {
get {
return Players.Count;
}
}
..que se convierte en:
my_team.PlayerCount
Esto tiene un significado ahora y se adhiere a la ley de Demeter .
También debe considerar adherirse al principio de reutilización del material compuesto . Al heredar de la List<T>
, estás diciendo que un equipo es una lista de jugadores y expone métodos innecesarios de ella. Esto es incorrecto: como usted dijo, un equipo es más que una lista de jugadores: tiene un nombre, gerentes, miembros de la junta, entrenadores, personal médico, topes salariales, etc. Al hacer que su clase de equipo contenga una lista de jugadores, usted estamos diciendo que "un equipo tiene una lista de jugadores", pero también puede tener otras cosas.
¿Cuál es la forma correcta en C # de representar una estructura de datos ...
Recuerde: "Todos los modelos están mal, pero algunos son útiles". - George EP Box
No hay una "forma correcta", solo una útil.
Elija uno que sea útil para usted y / o sus usuarios. Eso es. Desarrollar económicamente, no sobre ingeniar. Cuanto menos código escriba, menos código necesitará depurar. (lea las siguientes ediciones).
- Editado
Mi mejor respuesta sería ... depende. La herencia de una lista expondría a los clientes de esta clase a métodos que pueden no estar expuestos, principalmente porque FootballTeam se parece a una entidad comercial.
- Edición 2
Sinceramente, no recuerdo a qué me refería en el comentario de "no hacer ingeniería excesiva". Si bien creo que la mentalidad de KISS es una buena guía, quiero enfatizar que heredar una clase de negocios de la Lista crearía más problemas de los que resuelve, debido a la pérdida de abstracción .
Por otro lado, creo que hay un número limitado de casos en los que simplemente heredar de la Lista es útil. 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 más precisamente sobre la respuesta.
Prefiere Interfaces sobre Clases
Las clases deben evitar derivarse de clases y, en su lugar, implementar las interfaces mínimas necesarias.
Herencia rompe la encapsulación
Derivando de las clases se rompe la encapsulación :
- expone detalles internos sobre cómo se implementa su colección
- declara una interfaz (conjunto de funciones y propiedades públicas) que puede no ser apropiada
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, 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 engañoso. A System.List<T>
es una colección ordenada mutable que admite operaciones amortizadas O (1) para agregar, insertar y eliminar elementos, y operaciones O (1) para recuperar la cantidad de elementos o obtener y configurar elementos por índice.
Cuanto más pequeña sea la interfaz, más flexible será el código.
Al diseñar una estructura de datos, cuanto más simple sea la interfaz, más flexible será el código. Solo mire cuán poderoso es LINQ para una demostración de esto.
Cómo elegir interfaces
Cuando pienses en "lista", debes comenzar diciéndote a ti mismo: "Necesito representar una colección de jugadores de béisbol". Así que digamos que decides modelar esto con una clase. Lo primero que debe hacer es decidir qué cantidad mínima de interfaces tendrá que exponer esta clase.
Algunas preguntas que pueden ayudar a guiar este proceso:
- ¿Necesito tener el conde? Si no considera implementar
IEnumerable<T>
- ¿Esta colección va a cambiar después de que se haya inicializado? Si no lo consideramos
IReadonlyList<T>
. - ¿Es importante que pueda acceder a los artículos por índice? Considerar
ICollection<T>
- ¿Es importante el orden en el que agrego artículos a la colección? Tal vez es un
ISet<T>
? - Si realmente quieres esto, entonces adelante e implementa
IList<T>
.
De esta manera, no unirá otras partes del código a los detalles de implementación de su colección de jugadores de béisbol y será libre de 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 caldera
Implementar interfaces en un IDE moderno debería ser fácil. Haga clic derecho y elija "Implementar interfaz". Luego reenvíe todas las implementaciones a una clase miembro si es necesario.
Dicho esto, si descubre que está escribiendo un montón de repetitivo, es potencialmente porque está exponiendo más funciones de las que debería. Es la misma razón por la que no debe 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 auxiliares para asignar esas interfaces a las que necesite. Este es el enfoque que tomé en mi propia IArray
interfaz para la biblioteca LinqArray .
depende del contexto
Cuando considera a su equipo como una lista de jugadores, está proyectando la "idea" de un equipo de foot ball a 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 ser completamente erróneo. Imagina que quieres ser patrocinador del equipo. Así que tienes que hablar con los directivos del equipo. En este contexto, el equipo se proyecta a la lista de sus gerentes. Y estas dos listas usualmente no se superponen mucho. Otros contextos son los actuales contra los jugadores anteriores, etc.
Semántica poco clara
Entonces, el problema al considerar a un equipo como una lista de sus jugadores es que su semántica depende del contexto y que no puede extenderse cuando el contexto cambia. Además, es difícil expresar qué contexto está utilizando.
Las clases son extensibles
Cuando utiliza una clase con un solo miembro (por ejemplo IList activePlayers
), puede usar el nombre del miembro (y además su comentario) para aclarar el contexto. Cuando hay contextos adicionales, simplemente agrega un miembro adicional.
Las clases son más complejas.
En algunos casos, podría ser una exageración crear una clase extra. 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 tiempo de ejecución y memoria. Cuando tenga un contexto muy específico, podría estar bien considerar un equipo de fútbol como una lista de jugadores. Pero en este caso, deberías usar IList
una clase, no una clase derivada de ella.
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 es completamente correcto escribir
IList<Player> footballTeam = ...
Cuando se usa F #, incluso puede estar bien crear una abreviatura de tipo
type FootballTeam = IList<Player>
Pero cuando el contexto es más amplio o incluso poco claro, no debes hacer esto. Este es especialmente el caso, cuando creas una nueva clase, donde no está claro en qué contexto se puede usar en el futuro. Una señal de advertencia es cuando comienzas a agregar atributos adicionales a tu 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 los jugadores (actualmente activos, no lesionados, etc.) como un atributo del equipo.
Diseño> Implementación
Qué métodos y propiedades expone es una decisión de diseño. La clase base de la que hereda es un detalle de implementación. Creo que vale la pena dar un paso atrás hacia la primera.
Un objeto es una colección de datos y comportamiento.
Así que tus primeras preguntas deben ser:
- ¿Qué datos comprende este objeto en el modelo que estoy creando?
- ¿Qué comportamiento exhibe este objeto en ese modelo?
- ¿Cómo podría esto cambiar en el futuro?
Tenga en cuenta que la herencia implica una relación "isa" (es a), mientras que la composición implica una relación "tiene una" (hasa). Elija la correcta para su situación en su vista, 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 hacen conscientemente a este nivel en la codificación del día a día. Pero si está reflexionando sobre este tipo de tema, está pisando aguas de diseño. Ser consciente de ello puede ser liberador.
Considerar detalles de diseño
Eche un vistazo a List <T> e IList <T> en MSDN o Visual Studio. Ver qué métodos y propiedades exponen. ¿Todos estos métodos parecen algo que alguien querría hacerle a un Equipo de Fútbol en su opinión?
¿Tiene sentido para ti footballTeam.Reverse ()? ¿FootballTeam.ConvertAll <TOutput> () se parece a algo que quieres?
Esta no es una pregunta con trampa; la respuesta podría ser realmente "sí". Si implementas / heredas la Lista <Player> o IList <Player>, te quedas con ellos; Si eso es ideal para tu modelo, hazlo.
Si decides que sí, eso tiene sentido, y quieres que tu objeto sea tratable como una colección / lista de jugadores (comportamiento), y por lo tanto quieres implementar ICollection o IList, por todos los medios. Nocionalmente
class FootballTeam : ... ICollection<Player>
{
...
}
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
class FootballTeam ...
{
public ICollection<Player> Players { get { ... } }
}
Puede sentir que quiere 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 para considerar.
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 decirle que uno de estos está categóricamente y definitivamente equivocado en todos los casos está equivocado. Cualquier persona que intente decirle que es correcta categóricamente y definitivamente en todos los casos 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 a través de la herencia o la composición.
Puede que este no sea un gran paso, y la gente a menudo confunde el diseño y la implementación, ya que es muy posible repasarlo todo en uno o dos segundos y comenzar a escribir.
Un experimento de pensamiento
Un ejemplo artificial: como han mencionado otros, un equipo no siempre es "solo" una colección de jugadores. ¿Mantienes una colección de resultados de partidos para el equipo? ¿Es el equipo intercambiable con el club, en tu 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:
class FootballTeam : ... ICollection<Player>,
ICollection<StaffMember>,
ICollection<Score>
{
....
}
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 la herencia única. (Si has probado este Malarky en C ++, puede considerar esto una buena cosa.) La aplicación de una colección a través de la herencia y una a través de la composición es probable que sentir sucia. Y las propiedades como Count se vuelven confusas para los usuarios a menos que implemente ILIst <Player> .Count e IList <StaffMember> .Count, etc. de manera explícita, y luego son dolorosas en lugar de confusas. Puedes ver a dónde va esto; Es posible que, al pensar en esta avenida, se sienta la sensación de que es un error dirigirse en esta dirección (y, con razón o sin ella, ¡sus colegas también podrían hacerlo si lo implementaron de esta manera!)
La respuesta corta (demasiado tarde)
La guía 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 gana sobre la herencia en términos de comprensibilidad, implementabilidad y mantenimiento. Es más común que los objetos del mundo real / dominio encuentren relaciones "hasa" útiles y consistentes que las relaciones "isa" útiles y consistentes, a menos que esté en el resumen, especialmente a medida que pasa el tiempo y los datos y el comportamiento precisos de los objetos en el código cambios Esto no debería hacer que siempre descarte la herencia de las clases de colección; pero puede ser sugerente.
¿Qué pasa si el Equipo de FootballTeam
tiene un equipo de reservas junto con el equipo principal?
class FootballTeam
{
List<FootballPlayer> Players { get; set; }
List<FootballPlayer> ReservePlayers { get; set; }
}
¿Cómo modelarías eso con?
class FootballTeam : List<FootballPlayer>
{
public string TeamName;
public int RunningTotal
}
La relación es claramente tiene una y no es una .
o RetiredPlayers
?
class FootballTeam
{
List<FootballPlayer> Players { get; set; }
List<FootballPlayer> ReservePlayers { get; set; }
List<FootballPlayer> RetiredPlayers { get; set; }
}
Como regla general, si alguna vez desea heredar de una colección, nombre la clase SomethingCollection
.
¿Su SomethingCollection
semánticamente tiene sentido? Solo haz esto si tu tipo es una colección de Something
.
En el caso de FootballTeam
no suena bien. Un Team
es más que una Collection
. Un Team
puede tener entrenadores, entrenadores, etc., como han señalado las otras respuestas.
FootballCollection
suena como una colección de balones 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 hereda de la List<FootballPlayer>
si realmente quisieras hacerlo.
Really List<FootballPlayer>
es un tipo perfectamente bueno para tratar. Tal vez IList<FootballPlayer>
si lo está devolviendo desde un método.
En resumen
Pregúntese
¿Es
X
unaY
? o tieneX
aY
?¿Mis nombres de clase significan lo que son?
Como todos han señalado, un equipo de jugadores no es una lista de jugadores. Muchas personas en todo el mundo cometen este error, tal vez con varios niveles de experiencia. A menudo, el problema es sutil y en ocasiones muy grave, como en este caso. Tales diseños son malos porque violan el principio de sustitución de Liskov . Internet tiene muchos artículos buenos que explican este concepto, por ejemplo, http://en.wikipedia.org/wiki/Liskov_substitution_principle
En resumen, hay dos reglas que deben conservarse en una relación padre / hijo entre las clases:
- un niño no debe requerir ninguna característica menor a la que define completamente al padre.
- un padre no debe requerir ninguna característica además de lo que define completamente al niño.
En otras palabras, un padre es una definición necesaria de un hijo, y un hijo es una definición suficiente de un padre.
Aquí hay una manera de pensar en la solución y aplicar el principio anterior que debería ayudar a evitar tal error. Uno debe probar la hipótesis de uno al verificar si todas las operaciones de una clase padre son válidas para la clase derivada tanto estructural como semánticamente.
- ¿Es un equipo de fútbol una lista de jugadores de fútbol? (¿Todas las propiedades de una lista se aplican a un equipo en el mismo significado)
- ¿Es un equipo una colección de entidades homogéneas? Sí, el equipo es una colección de jugadores.
- ¿El orden de inclusión de los jugadores es descriptivo del estado del equipo y el equipo garantiza que la secuencia se conserve a menos que se cambie explícitamente? No y no
- ¿Se espera que los jugadores sean incluidos / eliminados en función de su posición secuencial en el equipo? No
Como ve, solo la primera característica de una lista es aplicable a un equipo. De ahí que un equipo no sea una lista. Una lista sería un detalle de la implementación de cómo administras tu 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 señalar que una clase de Equipo no debería, en mi opinión, implementarse utilizando una Lista; debe implementarse utilizando una estructura de datos de conjuntos (HashSet, por ejemplo) en la mayoría de los casos.
En primer lugar, tiene que ver con la usabilidad. Si utiliza la herencia, la clase Team
expondrá el comportamiento (métodos) diseñados exclusivamente para la manipulación de objetos. Por ejemplo, los AsReadOnly()
o CopyTo(obj)
no tienen sentido para el objeto de equipo. En lugar del método AddRange(items)
, probablemente desearía un método AddPlayers(players)
más descriptivo.
Si desea utilizar LINQ, la implementación de una interfaz genérica como ICollection<T>
o IEnumerable<T>
tendría más sentido.
Como se mencionó, la composición es la forma correcta de hacerlo. Solo implementa una lista de jugadores como una variable privada.
Este es un ejemplo clásico de composition vs inheritance .
En este caso específico:
¿Es el equipo una lista de jugadores con comportamiento añadido?
o
¿Es el equipo un objeto propio que contiene una lista de jugadores?
Al extender la Lista, te estás limitando de varias maneras:
No puede restringir el acceso (por ejemplo, detener a las personas que cambian la lista). Usted obtiene todos los métodos de la lista, ya sea que los necesite o quiera todos o no.
Qué pasa 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 de equipo genérico y luego tener a BaseballTeam, FootballTeam, etc. que se hereden de eso. Para heredar de la Lista, debe hacer la herencia del Equipo, pero eso significa que todos los diferentes tipos de equipo se ven 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.
Hay algunas buenas respuestas aquí. Les añadiría los siguientes puntos.
¿Cuál es la forma correcta en C # de representar una estructura de datos que, "lógicamente" (es decir, "para la mente humana") es solo una lista de cosas con algunas campanas y silbidos?
Pídale a diez personas que no estén programadas por computadora y que estén familiarizadas con la existencia del fútbol que completen el espacio en blanco:
A football team is a particular kind of _____
¿ Alguien dijo "lista de jugadores de fútbol con algunas campanas y silbidos", o todos dijeron "equipo deportivo" o "club" u "organización"? Tu idea de que un equipo de fútbol es un tipo particular de lista de jugadores está en tu mente humana y solo en tu mente humana.
List<T>
es un mecanismo . El equipo de fútbol es un objeto de negocios , es decir, un objeto que representa algún concepto que está en el dominio de negocios del programa. ¡No mezcles esos! Un equipo de fútbol es una especie de equipo; tiene una plantilla, 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. Así que haz una propiedad llamada Roster
que es una List<Player>
. Y haz que sea ReadOnlyList<Player>
mientras estás en ello, a menos que creas que todos los que saben sobre un equipo de fútbol pueden eliminar jugadores de la lista.
¿Es hereditario de la
List<T>
siempre inaceptable?
¿Inaceptable para quién? ¿Yo? No.
¿Cuándo es aceptable?
Cuando estás construyendo un mecanismo que extiende el mecanismo de List<T>
.
¿Qué debe considerar un programador, al decidir si heredar de la
List<T>
o no?
¿Estoy construyendo un mecanismo o un objeto de negocio ?
¡Pero eso es mucho código! ¿Qué obtengo por todo ese trabajo?
Pasó más tiempo escribiendo su pregunta que le habría llevado escribir métodos de reenvío para los miembros relevantes de la 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
Lo 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 que también tenga una lista de jugadores. El problema con un equipo como / con 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 faltaron a partidos debido a una lesión entre 2003 y 2013?" o "¿Qué jugador de Denver que jugó anteriormente para otro equipo tuvo el mayor incremento año tras año en yardas?" o " ¿Los Piggers llegaron todo el camino este año? "
Es decir, un equipo de fútbol me parece estar bien modelado como una recopilació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. Centro, pero puede haber otras cosas interesantes que desee hacer con este objeto que requieran una perspectiva más histórica.
Wow, tu publicación tiene un montón de preguntas y puntos. La mayor parte del razonamiento que obtiene de Microsoft está exactamente en el punto. Comencemos con todo sobre la List<T>
-
List<T>
está altamente optimizada. Su uso principal es para ser utilizado como miembro privado de un objeto. - Microsoft no lo selló porque a veces es posible que desee crear una clase que tenga un nombre más amigable:
class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }
. Ahora es tan fácil como hacervar list = new MyList<int, string>();
. - CA1002: No exponga listas genéricas : Básicamente, incluso si planea usar esta aplicación como el único desarrollador, vale la pena desarrollarla con buenas prácticas de codificación, de modo que se inculquen en usted y sean de segunda naturaleza. Aún puede exponer la lista como un
IList<T>
si necesita que un consumidor tenga una lista indexada. Esto te permite cambiar la implementación dentro de una clase más adelante. - Microsoft hizo la
Collection<T>
muy genérica porque es un concepto genérico ... el nombre lo dice todo; Es sólo una colección. Existen versiones más precisas comoSortedCollection<T>
,ObservableCollection<T>
,ReadOnlyCollection<T>
, etc., cada una de las cuales implementaIList<T>
pero noList<T>
. -
Collection<T>
permite que los miembros (por ejemplo, Agregar, Eliminar, etc.) se sobrescriban porque son virtuales.List<T>
no lo hace. - La última parte de tu pregunta es acertada. Un equipo de fútbol es más que una lista de jugadores, por lo que debe ser una clase que contenga esa lista de jugadores. Piensa en la composición vs herencia . Un equipo de fútbol tiene una lista de jugadores (una lista), no es una lista de jugadores.
Si estuviera escribiendo este código, la clase probablemente se vería así:
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.
}
Permite que la gente diga
myTeam.subList(3, 5);
¿Tiene algún sentido en absoluto? Si no, entonces no debería ser una lista.
Aquí hay muchas respuestas excelentes, pero quiero referirme a algo que no vi mencionado: el diseño orientado a objetos se trata de potenciar los 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 ello. De hecho, usted quiere ir un paso más allá y evitar activamente que otros objetos pasen por alto estas partes internas.
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:
Supongamos que quieres diferenciar cuando un jugador se va sabiendo si se retiraron, renunciaron o fueron despedidos. Podría implementar un RemovePlayer
método que tome una enumeración de entrada apropiada. Sin embargo, al heredar de List
, no podrá impedir el acceso directo a Remove
, RemoveAll
e incluso Clear
. Como resultado, has desempoderado a tu FootballTeam
clase.
Pensamientos adicionales sobre la encapsulación ... Usted planteó la siguiente preocupación:
Hace mi código innecesariamente detallado. Ahora debo llamar a my_team.Players.Count en lugar de a my_team.Count.
Tiene razón, eso sería innecesariamente detallado para que todos los clientes usen su equipo. Sin embargo, ese problema es muy pequeño en comparación con el hecho de que ha estado expuesto List Players
a todos para que puedan jugar con su equipo sin su consentimiento.
Usted va a decir:
Simplemente no tiene ningún sentido. Un equipo de fútbol no "tiene" una lista de jugadores. Es la lista de jugadores. No dices "John McFootballer se ha unido a los jugadores de SomeTeam". Usted dice "John se ha unido a SomeTeam".
Te equivocas en 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(...)
. Quieres que te llamen ateam.AddPlayer(...)
. Y su implementación llamaría (posiblemente entre otras cosas) Players.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 su trabajo bien sin temor a interferencias de otros objetos.
Aunque 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 extender IEnumerable<T>
, puede permitir que su Team
clase admita extensiones de consulta Linq, sin exponer públicamente todos los métodos y propiedades de 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
{
...
}
Creo que no estoy de acuerdo con su 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 de administración / administración, equipo de entrenamiento, y luego colección de jugadores. Así que correctamente, su clase de 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, como Specialized StringCollection, ofrece otras facilidades, como la validación y las comprobaciones antes de agregar o eliminar objetos de la tienda interna.
Tal vez, ¿la noción de mejor PlayerCollection se adapta a su enfoque preferido?
public class PlayerCollection : Collection<Player>
{
}
Y luego el equipo de fútbol puede verse así:
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();
}
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 caros. Entonces, el problema es que una vez que expones List<T>
en tu API pública , pierdes la capacidad de hacer cumplir las reglas comerciales o personalizar su funcionalidad más adelante. Pero si está utilizando esta clase heredada como interna en su proyecto (en lugar de estar expuesta a miles de sus clientes / socios / otros equipos como API), entonces puede estar bien si ahorra tiempo y es la funcionalidad que desea. duplicar. La ventaja de heredar List<T>
es que elimina gran cantidad de código de envoltura tonta que nunca se personalizará en un futuro previsible. Además, si desea que su clase tenga explícitamente la misma semántica queList<T>
Para la vida de sus API, entonces también puede estar bien.
A menudo veo a mucha gente haciendo un montón 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 del patrón palooza rareza. Al igual que con muchas pautas, trátelas como pautas que pueden tener excepciones.
Déjame reescribir tu pregunta. así que puedes 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"
string team = new string();
Luego más tarde me di cuenta que los equipos también tienen jugadores.
¿Por qué no puedo simplemente extender el tipo de cadena para que también tenga una lista de jugadores?
Su punto de entrada en el problema es arbitrario. Trate de pensar lo que lo hace un equipo tiene (propiedades), no lo que es .
Después de hacer eso, podría ver si comparte propiedades con otras clases. Y pensar en la herencia.
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 se repite en la lista; en este punto, tiene la opción de crear un objeto FootballTeam que envuelva la lista de jugadores. La clase FootballTeam se convierte en el hogar de todos los códigos que se repiten en la lista de jugadores.
Hace mi código innecesariamente detallado. Ahora debo llamar a my_team.Players.Count en lugar de a my_team.Count. Afortunadamente, con C # puedo definir los indexadores para hacer que la indexación sea transparente y reenviar todos los métodos de la Lista interna ... ¡Pero eso es mucho código! ¿Qué obtengo por todo ese trabajo?
Encapsulacion Sus clientes no necesitan saber qué sucede dentro de FootballTeam. Para que todos sus clientes sepan, podría implementarse buscando la lista de jugadores en una base de datos. No necesitan saberlo, y esto mejora su diseño.
Simplemente no tiene ningún sentido. Un equipo de fútbol no "tiene" una lista de jugadores. Es la lista de jugadores. No dices "John McFootballer se ha unido a los jugadores de SomeTeam". Usted dice "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.
Exactamente :) dirás footballTeam.Add (john), no footballTeam.List.Add (john). La lista interna no será visible.
Esto me recuerda a "Is a" versus "tiene a" trade-off. A veces es más fácil y tiene más sentido heredar directamente de una súper clase. Otras veces tiene más sentido crear una clase independiente e incluir la clase que habría heredado como una variable miembro. Aún 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". Por lo tanto, si escribe una clase llamada BMW, podría heredarse de un automóvil porque un BMW realmente es un automóvil. Una clase de Caballo puede heredar de la clase de Mamíferos porque un caballo en realidad es un mamífero en la vida real y cualquier funcionalidad de Mamíferos debe ser relevante para Caballos. ¿Pero puedes decir que un equipo es una lista? Por lo que puedo decir, no parece realmente un Equipo "es una" Lista. Entonces, en este caso, tendría una Lista como variable miembro.
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.
Mi secreto sucio: no me importa lo que diga la gente, y lo hago. .NET Framework se propaga con "XxxxCollection" (UIElementCollection para el ejemplo superior de mi cabeza).
Entonces, ¿qué me impide decir:
team.Players.ByName("Nicolas")
Cuando lo encuentro mejor que
team.ByName("Nicolas")
Además, mi PlayerCollection podría ser utilizada por otra clase, como "Club" sin ninguna duplicación de código.
club.Players.ByName("Nicolas")
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, ¿qué es más legible y fácil de mantener?
team.Players.ByName("Nicolas")
o
team.ByName("Nicolas")
De Verdad. ¿Tienes alguna duda? Tal vez necesite jugar con otras restricciones técnicas que le impiden utilizar la Lista <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 es seguramente una "mejor práctica" que viene de la nada.
Si los usuarios de su clase necesitan todos los métodos y propiedades que tiene la lista **, debe derivar su clase de ella. Si no los necesitan, encierre la Lista y haga envoltorios para los métodos que realmente necesitan los usuarios de su clase.
Esta es una regla estricta, si escribe una API pública , o cualquier otro código que será usado 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 te permita escribir menos código.
Simplemente porque creo que las otras respuestas prácticamente se van en una tangente de si un equipo de fútbol "es-un" List<FootballPlayer>
o "tiene-un" List<FootballPlayer>
, lo que realmente no responde a esta pregunta como está escrito.
El OP solicita principalmente aclaraciones sobre las pautas para heredar de List<T>
:
Una guía dice que no debes heredar de
List<T>
. Por qué no?
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 un acuerdo mucho más grande en una API pública.
¿Qué es una API pública y por qué debería importarme?
Una API pública es una interfaz que usted 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 general, el diseño público de la API es mucho más estricto.
Si mi proyecto actual no lo tiene y es probable que nunca tenga esta API pública, ¿puedo ignorar esta guía de forma segura? Si heredo de List y resulta que necesito una API pública, ¿qué dificultades tendré?
Bastante, sí. Es posible que desee considerar la razó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 tiene que preocuparse particularmente por cuestiones de API como las versiones (de las cuales, esta es una subconjunto).
Si agrega una API pública en el futuro, tendrá que abstraer su API de su implementación (al no exponerla List<T>
directamente) o violar las pautas con el posible dolor futuro que ello conlleva.
¿Por qué importa? Una lista es una lista. ¿Qué podría cambiar? ¿Qué podría querer cambiar?
Depende del contexto, pero ya que estamos usando FootballTeam
como ejemplo, imagina que no puedes agregar un FootballPlayer
si eso causaría que el equipo supere el límite salarial. Una posible forma de agregar eso sería algo como:
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!");
}
}
}
Ah ... pero no puedes override Add
porque no lo es virtual
(por razones de rendimiento).
Si está en una aplicación (lo que, básicamente, significa que usted y todas las personas que llaman están compiladas juntas), ahora puede cambiar y usar IList<T>
cualquier error de compilación:
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 */
}
pero, si ha expuesto públicamente a un tercero, acaba de hacer un cambio importante que causará errores de compilación y / o tiempo de ejecución.
TL; DR: las directrices son para las API públicas. Para APIs privadas, haz lo que quieras.
Solo quería agregar que Bertrand Meyer, el inventor de Eiffel y el diseño por contrato, habrían Team
heredado de List<Player>
tanto como de un pestillo.
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 ambos Rectangle
y Tree<Window>
reutilizar la implementación.
Sin embargo, C # no es Eiffel. Este último admite herencia múltiple y cambio de nombre de características . En C #, cuando realiza una subclase, hereda tanto la interfaz como la implementación. Puede anular la implementación, pero las convenciones de llamada se copian directamente desde la superclase. En Eiffel, sin embargo, puede modificar los nombres de los métodos públicos, para que pueda cambiar el nombre Add
y Remove
para Hire
y Fire
en su Team
. Si una instancia de Team
es upcast de nuevo a List<Player>
la persona que llama se utilice Add
y Remove
para modificarlo, pero sus métodos virtuales Hire
y Fire
será llamado.
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:
class FootballTeam : List<FootballPlayer>
{
public string TeamName;
public int RunningTotal
}
y esto es correcto:
class FootballTeam
{
public List<FootballPlayer> players
public string TeamName;
public int RunningTotal
}
class FootballTeam : List<FootballPlayer>
{
public string TeamName;
public int RunningTotal;
}
El código anterior significa: un grupo de muchachos de la calle que juegan al fútbol y que tienen un nombre. Algo como:
De todos modos, este código (de mi respuesta)
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; }
}
public int PlayerCount
{
get { return _roster.Count(); }
}
// Any additional members you want to expose/wrap.
}
Medios: este es un equipo de fútbol que tiene directivos, jugadores, administradores, etc. Algo así como:
Así es como se presenta tu lógica en imágenes ...