¿Por qué no heredar de la Lista <T>?


1400

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, stringel 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 (a int). .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 es List<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 Listalguna 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 Listes 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 Collectionclase 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 AddRangeno es mejor que no AddRange?

Heredar de Collectiones 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 Collectionno 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 Listen algo:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Hay dos problemas con esto:

  1. Hace que mi código sea innecesariamente detallado. Ahora debo llamar en my_team.Players.Countlugar de solo my_team.Count. Afortunadamente, con C # puedo definir indexadores para hacer que la indexación sea transparente y reenviar todos los métodos internos List... ¡Pero eso es mucho código! ¿Qué obtengo por todo ese trabajo?

  2. 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 listde las thingspocas 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?


90
Entonces, ¿es su pregunta realmente cuándo usar la herencia (un cuadrado es un rectángulo porque siempre es razonable proporcionar un cuadrado cuando se solicitó un rectángulo genérico) y cuándo usar la composición (un equipo de fútbol tiene una lista de jugadores de fútbol, ​​además a otras propiedades que son tan "fundamentales" para él, como un nombre)? ¿Se confundirían otros programadores si en otro lugar del código les pasaras un FootballTeam cuando esperaban una Lista simple?
Vuelo Odyssey

77
¿Qué sucede si le digo que un equipo de fútbol es una empresa comercial que posee una lista de contratos de jugadores en lugar de una lista de jugadores con algunas propiedades adicionales?
STT LCU

75
Es realmente bastante simple. ¿Es un equipo de fútbol una lista de jugadores? Obviamente no, porque como usted dice, hay otras propiedades relevantes. ¿Un equipo de fútbol tiene una lista de jugadores? Sí, por lo que debe contener una lista. Aparte de eso, la composición es preferible a la herencia en general, porque es más fácil de modificar más adelante. Si encuentra la herencia y la composición lógicas al mismo tiempo, vaya con la composición.
Tobberoth

45
@FlightOdyssey: Supongamos que tiene un método SquashRectangle que toma un rectángulo, reduce a la mitad su altura y duplica su ancho. Ahora pasa en un rectángulo que pasa a ser de 4x4, pero es de tipo rectángulo y un cuadrado que es 4x4. ¿Cuáles son las dimensiones del objeto después de llamar a SquashRectangle? Para el rectángulo claramente es 2x8. Si el cuadrado es 2x8, ya no es un cuadrado; Si no es 2x8, la misma operación en la misma forma produce un resultado diferente. La conclusión es que un cuadrado mutable no es una especie de rectángulo.
Eric Lippert

29
@Gangnus: Su declaración es simplemente falsa; Se requiere una subclase real para tener funcionalidad que es un superconjunto de la superclase. Se stringrequiere A para hacer todo lo que se objectpuede hacer y más .
Eric Lippert

Respuestas:


1567

Hay algunas buenas respuestas aquí. Les agregaría los siguientes puntos.

¿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 lista de cosas con algunas campanas y silbatos?

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:

Un equipo de fútbol es un tipo particular de _____

¿ 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 llamada Rosterque es a List<Player>. Y hágalo ReadOnlyList<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.

¿Heredar de List<T>siempre es inaceptable?

Inaceptable para quien? ¿Yo? No.

¿Cuándo es aceptable?

Cuando estás construyendo un mecanismo que extiende el List<T>mecanismo .

¿Qué debe tener en cuenta un programador al decidir si heredar List<T>o no?

¿Estoy construyendo un mecanismo o un objeto comercial ?

¡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 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.


84
Aunque las otras respuestas han sido muy útiles, creo que esta aborda mis preocupaciones más directamente. En cuanto a exagerar la cantidad de código, tiene razón en que al final no es mucho trabajo, pero me confundo muy fácilmente cuando incluyo algún código en mi programa sin entender por qué lo estoy incluyendo.
Excelente

128
@ Superbest: ¡Me alegro de poder ayudar! Tienes razón al escuchar esas dudas; si no comprende por qué está escribiendo algún código, descúbralo o escriba un código diferente que sí entienda.
Eric Lippert

48
@Mehrdad Pero parece olvidar las reglas de oro de la optimización: 1) no lo hagas, 2) no lo hagas todavía, y 3) no lo hagas sin primero hacer perfiles de rendimiento para mostrar lo que hay que optimizar.
AJMansfield

107
@Mehrdad: Honestamente, si su aplicación requiere que se preocupe por la carga de rendimiento de, por ejemplo, los métodos virtuales, cualquier lenguaje moderno (C #, Java, etc.) no es el idioma para usted. Tengo una computadora portátil aquí que podría ejecutar una simulación de cada juego de arcade que jugaba de niño simultáneamente . Esto me permite no preocuparme por un nivel de indirección aquí y allá.
Eric Lippert

44
@Superbest: ahora estamos definitivamente en el extremo del espectro "composición no herencia". Un cohete se compone de partes de cohetes . ¡Un cohete no es "un tipo especial de lista de piezas"! Te sugeriría que lo recortes más; ¿Por qué una lista, en lugar de una IEnumerable<T>- una secuencia ?
Eric Lippert

161

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 sobreList<T>

  • List<T> Está altamente optimizado. Su uso principal es ser utilizado como miembro privado de un objeto.
  • Microsoft no cerraba porque a veces es posible que desee crear una clase que tiene un nombre más sencillo: class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. Ahora es tan fácil como hacerlo var 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, para que se inculquen en usted y en la segunda naturaleza. Aún puede exponer la lista como 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.
  • Microsoft lo hizo 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 como SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>, etc. cada uno de los cuales implementan IList<T>pero no List<T> .
  • Collection<T>permite que los miembros (es decir, Agregar, Eliminar, etc.) se anulen porque son virtuales. List<T>no.
  • La última parte de su 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. Piense 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.
}

66
" tu publicación tiene una gran cantidad de preguntas " - Bueno, dije que estaba completamente confundido. Gracias por vincularme a la pregunta de @ Readonly, ahora veo que una gran parte de mi pregunta es un caso especial del problema más general de Composición vs. Herencia.
Superbest

3
@Brian: excepto que no se puede crear un genérico con alias, todos los tipos deben ser concretos En mi ejemplo, List<T>se extiende como una clase interna privada que usa genéricos para simplificar el uso y la declaración de List<T>. Si la extensión fuera simplemente class 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 de List<T>.
Mi

66
Si esto fuera Programmers.SE, estaría de acuerdo con el rango actual de votos de respuesta, pero, como se trata de una publicación SO, esta respuesta al menos intenta responder a los problemas específicos de C # (.NET), a pesar de que hay aspectos generales definidos. problemas de diseño.
Mark Hurd

77
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overriddenAhora esa es una buena razón para no heredar de la Lista. Las otras respuestas tienen demasiada filosofía.
Navin

10
@my: Sospecho que te refieres a "gran cantidad de preguntas", ya que un detective es un detective.
Batty

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

El código anterior significa: un grupo de chicos de la calle jugando al fútbol, ​​y tienen un nombre. Algo como:

Gente jugando futbol

De todos modos, este código (de la respuesta de mi)

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

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

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

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

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

Significa: este es un equipo de fútbol que tiene directivos, jugadores, administradores, etc. Algo así como:

Imagen que muestra los miembros (y otros atributos) del equipo Manchester United

Así es como se presenta su lógica en imágenes ...


99
Esperaría que su segundo ejemplo sea un club de fútbol, ​​no un equipo de fútbol. Un club de fútbol tiene gerentes y administradores, etc. De esta fuente : "Un equipo de fútbol es el nombre colectivo que se le da a un grupo de jugadores ... Tales equipos podrían seleccionarse para jugar en un partido contra un equipo contrario, para representar a un club de fútbol ... ". Entonces, es mi opinión que un equipo es una lista de jugadores (o quizás más exactamente una colección de jugadores).
Ben

3
Más optimizadoprivate readonly List<T> _roster = new List<T>(44);
SiD

2
Es necesario importar el System.Linqespacio de nombres.
Davide Cannizzo

1
Para imágenes: +1
V.7

115

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:

  1. 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.

  2. ¿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.

  3. 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.


1
Ampliando el # 2, no tiene sentido que una Lista tenga una Lista.
Cruncher

Primero, la pregunta no tiene nada que ver con la composición versus la herencia. La respuesta es que OP no quiere implementar un tipo de lista más específico, por lo que no debe extender List <>. Estoy retorcido porque tienes puntajes muy altos en stackoverflow y debes saber esto claramente y la gente confía en lo que dices, así que ahora un mínimo de 55 personas que votaron y cuya idea está confundida creen que de una manera u otra está bien construir un sistema pero claramente no lo es! # no-rage
Mauro Sampietro

66
@sam Quiere el comportamiento de la lista. Tiene dos opciones, puede extender la lista (herencia) o puede incluir una lista dentro de su objeto (composición). ¿Quizás ha entendido mal parte de la pregunta o la respuesta en lugar de que 55 personas estén equivocadas y usted tiene razón? :)
Tim B

44
Si. Es un caso degenerado con un solo super y sub pero estoy dando una explicación más general para su pregunta específica. Es posible que solo tenga un equipo y que siempre tenga una lista, pero la opción sigue siendo heredar la lista o incluirla como un objeto interno. Una vez que comienzas a incluir varios tipos de equipo (fútbol, ​​cricket, etc.) y comienzan a ser más que una simple lista de jugadores, ves cómo tienes la pregunta de composición completa frente a herencia. Mirar el panorama general es importante en casos como este para evitar elecciones equivocadas temprano que luego significan una gran refactorización posterior.
Tim B

3
El OP no solicitó Composición, pero el caso de uso es un claro ejemplo del problema X: Y, de los cuales uno de los 4 principios de programación orientados a objetos está roto, mal utilizado o no se comprende completamente. La respuesta más clara es escribir una colección especializada como una Pila o una Cola (que no parece ajustarse al caso de uso) o comprender la composición. Un equipo de fútbol NO es una lista de jugadores de fútbol. Si el OP lo solicitó explícitamente o no es irrelevante, sin comprender la abstracción, la encapsulación, la herencia y el polimorfismo, no entenderán la respuesta, ergo, X: Y amok
Julia McGuigan

67

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:

  • un niño no debe requerir ninguna característica inferior 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 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.

  • ¿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 se asegura de que la secuencia se mantenga 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 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.


8
Buena captura en la lista versus conjunto. Eso parece ser un error que se comete con demasiada frecuencia. Cuando solo puede haber una instancia de un elemento en una colección, algún tipo de conjunto o diccionario debe ser un candidato preferido para la implementación. Está bien tener un equipo con dos jugadores con el mismo nombre. No está bien tener un jugador incluido dos veces al mismo tiempo.
Fred Mitchell

1
+1 para algunos puntos positivos, aunque es más importante preguntar "¿puede una estructura de datos admitir razonablemente todo lo útil (idealmente a corto y largo plazo)", en lugar de "la estructura de datos hace más de lo que es útil"?
Tony Delroy

1
@TonyD Bueno, los puntos que estoy planteando no es que uno debería verificar "si la estructura de datos hace más de lo que es útil". Es para marcar "Si la estructura de datos del padre hace algo irrelevante, sin sentido o contra-intuitivo al comportamiento que la clase del niño podría implicar".
Satyan Raina

1
@TonyD En realidad, existe un problema con las características irrelevantes derivadas de un padre, ya que en muchos casos fallarían las pruebas negativas. Un programador podría extender Human {eat (); correr(); write ();} desde un gorila {eat (); correr(); swing ();} pensando que no hay nada de malo en un humano con una característica adicional de poder hacer swing. Y luego, en un mundo de juegos, tu humano de repente comienza a sortear todos los supuestos obstáculos de la tierra simplemente balanceándose sobre los árboles. A menos que se especifique explícitamente, un humano práctico no debería ser capaz de balancearse. Tal diseño deja la API muy abierta al abuso y la confusión
Satyan Raina

1
@TonyD No estoy sugiriendo que la clase Player debería derivarse de HashSet tampoco. Estoy sugiriendo que la clase Player debería 'en la mayoría de los casos' implementarse usando un HashSet a través de Composition y eso es totalmente un detalle de nivel de implementación, no un nivel de diseño uno (es por eso que lo mencioné como una nota al margen de mi respuesta). Podría muy bien implementarse usando una lista si existe una justificación válida para tal implementación. Entonces, para responder a su pregunta, ¿es necesario tener O (1) búsqueda por clave? No. Por lo tanto, NO se debe extender un reproductor desde un HashSet también.
Satyan Raina

50

¿Qué pasa si el 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?

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

La relación es claramente tiene un y no es un .

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, asigne un nombre a la clase SomethingCollection.

¿Tiene SomethingCollectionsentido semánticamente? Solo haga esto si su tipo es una colección de Something.

En el caso de FootballTeamque no suene bien. A Teames más que a Collection. A Teampuede tener entrenadores, entrenadores, etc., como han señalado las otras respuestas.

FootballCollectionSuena 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.

FootballPlayerCollectionsuena como una colección de jugadores que sería un nombre válido para una clase que hereda List<FootballPlayer>si realmente quisieras hacer eso.

Realmente List<FootballPlayer>es un tipo perfectamente bueno para tratar. Tal vez IList<FootballPlayer>si lo devuelve desde un método.

En resumen

Pregúntese

  1. Es X un Y? o tiene X un Y?

  2. ¿Los nombres de mis clases significan lo que son?


Si el tipo de cada jugador podría clasificar como perteneciente a cualquiera de los dos DefensivePlayers, OffensivePlayerso OtherPlayers, podría ser legítimamente útil tener un tipo que podría ser utilizado por un código que espera List<Player>, sino también los miembros incluidos DefensivePlayers, OffsensivePlayerso SpecialPlayersdel tipo IList<DefensivePlayer>, IList<OffensivePlayer>y IList<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 ...
supercat

... como una señal del hecho de que la lista principal ha cambiado y las sublistas deberán regenerarse la próxima vez que se acceda a ellas].
supercat

2
Si bien estoy de acuerdo con el punto en cuestión, realmente me destroza el alma ver a alguien dar consejos de diseño y sugerir exponer una Lista concreta <T> con un captador y colocador público en un objeto comercial :(
sara

38

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:

  • ¿Qué datos contiene este objeto en el modelo que estoy creando?
  • ¿Qué comportamiento exhibe este objeto en ese modelo?
  • ¿Cómo podría cambiar esto en el futuro?

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:

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 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:

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 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.


34

En primer lugar, tiene que ver con la usabilidad. Si usa la herencia, la Teamclase expondrá comportamientos (métodos) diseñados exclusivamente para la manipulación de objetos. Por ejemplo, AsReadOnly()o los CopyTo(obj)métodos no tienen sentido para el objeto del equipo. En lugar del AddRange(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.


30

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 
}

19
Esto no explica por qué . El OP sabe que la mayoría de los otros programadores se sienten así, simplemente no entiende por qué es importante. Esto realmente solo proporciona información ya en la pregunta.
Servy

2
Esto es lo primero que pensé también, cómo sus datos se modelaron incorrectamente. Entonces, la razón es que su modelo de datos es incorrecto.
rball

3
¿Por qué no heredar de la lista? Porque no quiere un tipo de lista más específico.
Mauro Sampietro

2
No creo que el segundo ejemplo sea correcto. Está exponiendo el estado y, por lo tanto, está rompiendo la encapsulación. El estado debe estar oculto / interno al objeto y la forma de mutar este estado debe ser a través de métodos públicos. Exactamente qué métodos es algo que se determinará a partir de los requisitos del negocio, pero debe estar en una base de "necesito esto ahora", no "podría ser útil en algún momento no lo sé". Mantenga su API apretada y se ahorrará tiempo y esfuerzo.
sara

1
La encapsulación no es el punto de la pregunta. Me concentro en no extender un tipo si no desea que ese tipo se comporte de una manera más específica. Esos campos deben ser autoproperties en C # y métodos get / set en otros idiomas, pero aquí en este contexto es totalmente superfluo
Mauro Sampietro

28

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:

IList<Player> footballTeam = ...

Al usar 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 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.


27

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"

string team = new string();

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.


1
Ese es un buen punto: uno podría pensar que el equipo es solo un nombre. Sin embargo, si mi aplicación tiene como objetivo trabajar con las acciones de los jugadores, entonces ese pensamiento es un poco menos obvio. De todos modos, parece que el problema se reduce a la composición frente a la herencia al final.
Excelente

66
Esa es una forma meritoria de verlo. Como nota al margen, considere esto: un hombre rico posee varios equipos de fútbol, ​​le regala uno a un amigo, el amigo cambia el nombre del equipo, despide al entrenador, y reemplaza a todos los jugadores. El equipo de amigos se encuentra con el equipo masculino en el green y cuando el equipo masculino pierde, dice "¡No puedo creer que me estés ganando con el equipo que te di!" es el hombre correcto? ¿Cómo comprobarías esto?
user1852503

20

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>:

Una directriz 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 mucho más importante 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 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.

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é?

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.

¿Por qué es tan importante? Una lista es una lista. ¿Qué podría cambiar? ¿Qué podría querer cambiar?

Depende del contexto, pero dado que lo estamos usando FootballTeamcomo ejemplo, imagine que no puede agregar un FootballPlayersi 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 Addporque 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:

 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 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.


18

¿Permitir que la gente diga

myTeam.subList(3, 5);

tiene sentido? Si no, entonces no debería ser una Lista.


3
Podría hacerlo si lo llamarasmyTeam.subTeam(3, 5);
Sam Leach

3
@SamLeach Suponiendo que eso sea cierto, aún necesitará composición, no herencia. Como subListya ni siquiera devolverá un Team.
Cruncher

1
Esto me convenció más que cualquier otra respuesta. +1
FireCubez

16

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.

Hace que mi código sea innecesariamente detallado. Ahora debo llamar a my_team.Players.Count en lugar de solo my_team.Count. Afortunadamente, con C # puedo definir 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?

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.

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". 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.

Exactamente :) dirás footballTeam.Add (john), no footballTeam.List.Add (john). La lista interna no será visible.


1
OP quiere entender cómo MODELAR LA REALIDAD, pero luego define un equipo de fútbol como una lista de jugadores de fútbol (lo cual es incorrecto conceptualmente). Este es el problema. Cualquier otro argumento es engañoso para él en mi opinión.
Mauro Sampietro

3
No estoy de acuerdo con que la lista de jugadores esté equivocada conceptualmente. No necesariamente. Como alguien más escribió en esta página, "Todos los modelos están mal, pero algunos son útiles"
xpmatteo

Los modelos correctos son difíciles de obtener, una vez terminados son útiles. Si un modelo puede ser mal utilizado, es incorrecto conceptualmente. stackoverflow.com/a/21706411/711061
Mauro Sampietro

15

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 RemovePlayermétodo que tome una enumeración de entrada adecuada. Sin embargo, al heredar de List, no podrá evitar el acceso directo a Remove, RemoveAlle incluso Clear. Como resultado, en realidad has quitado el poder a tu FootballTeamclase.


Pensamientos adicionales sobre la encapsulación ... Usted planteó la siguiente preocupación:

Hace que mi código sea innecesariamente detallado. Ahora debo llamar a my_team.Players.Count en lugar de solo my_team.Count.

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 Playersa todos y cada uno para que puedan jugar con tu equipo sin tu consentimiento.

Sigues diciendo:

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". Dices "John se ha unido a SomeTeam".

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 llamando ateam.AddPlayer(...). Y su implementación podría (posiblemente entre otras cosas) llamar 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 bien su trabajo sin temor a la interferencia de otros objetos.


12

¿Cuál es la forma correcta de C # de representar una estructura de datos ...

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.


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. No sé si debería editar esta respuesta o publicar otra, ¿alguna idea?
ArturoTena

2
La parte de " heredar de un Listexpondría a los clientes de esta clase a métodos que podrían no exponerse " es precisamente lo que hace que heredar de Listun 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étodos List, así que no los expongas.
Desilusionado

3
"No sobre-ingenie. Cuanto menos código escriba, menos código tendrá que depurar". <- Creo que esto es engañoso. La encapsulación y la composición sobre la herencia NO es una ingeniería excesiva y no causa más necesidad de depuración. Al encapsular, está LIMITANDO la cantidad de formas en que los clientes pueden usar (y abusar) de su clase y, por lo tanto, tiene menos puntos de entrada que necesitan pruebas y validación de entrada. Heredar de Listporque 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".
sara

@kai Estoy de acuerdo contigo en todos los puntos. Sinceramente, no recuerdo a qué me refería en el comentario "no sobre-ingeniero". OTOH, creo que hay un número limitado de casos en los que simplemente es útil heredar de List. Como escribí en la edición posterior, depende. La respuesta a cada caso está fuertemente influenciada por el conocimiento, la experiencia y las preferencias personales. Como todo en la vida. ;-)
ArturoTena

11

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.


9

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:

team.Players.ByName("Nicolas")

Cuando lo encuentro mejor que

team.ByName("Nicolas")

Además, mi PlayerCollection puede ser utilizada por otra clase, como "Club" sin 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 a ti mismo, ¿qué es más fácil de leer y mantener?

team.Players.ByName("Nicolas") 

o

team.ByName("Nicolas")

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.


99
Si bien es importante tener el coraje y la iniciativa para desafiar la sabiduría aceptada cuando sea apropiado, creo que es prudente entender primero por qué la sabiduría aceptada se ha aceptado en primer lugar, antes de embarcarse para desafiarla. -1 porque "¡Ignora esa directriz!" no es una buena respuesta a "¿Por qué existe esta directriz?" Por cierto, la comunidad no solo "me culpó" en este caso, sino que me dio una explicación satisfactoria que logró persuadirme.
Excelente

3
En realidad, cuando no se hace una explicación, su única forma es empujar el límite y probar sin tener miedo a la infracción. Ahora. Pregúntese, incluso si la Lista <T> no fue una buena idea. ¿Cuánto dolor sería cambiar la herencia de la Lista <T> a la Colección <T>? Una suposición: menos tiempo que publicar en el foro, independientemente de la longitud de su código con herramientas de refactorización. Ahora es una buena pregunta, pero no práctica.
Nicolas Dorier

Para aclarar: ciertamente no estoy esperando la bendición de SO para heredar de lo que quiera como quiera, pero el punto de mi pregunta era comprender las consideraciones que deberían incluirse en esta decisión, y por qué tantos programadores experimentados parecen tener Decidí lo contrario de lo que hice. Collection<T>no es lo mismo, por List<T>lo que puede requerir bastante trabajo verificar en un proyecto de gran tamaño.
Excelente

2
team.ByName("Nicolas")significa "Nicolas"es el nombre del equipo.
Sam Leach

1
"no agregue una restricción que no debería existir" Au contraire, no exponga nada que no tenga una buena razón para exponer. Esto no es purismo o fanatismo ciego, ni es la última tendencia de diseño de moda. Es un diseño básico orientado a objetos 101, nivel principiante. No es un secreto por qué existe esta "regla", es el resultado de varias décadas de experiencia.
sara

8

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.


En retrospectiva, después de la explicación de @EricLippert y otros, realmente ha dado una gran respuesta para la parte API de mi pregunta, además de lo que dijo, si lo hago class FootballTeam : List<FootballPlayers>, los usuarios de mi clase podrán decirme ' he heredado de List<T>mediante el uso de la reflexión, al ver los List<T>métodos que no tienen sentido para un equipo de fútbol, y ser capaz de utilizar FootballTeamen List<T>, por lo que me gustaría ser reveladores detalles de implementación para el cliente (innecesariamente).
Excelente

7

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 expone List<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.


4

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 su Teamclase admita extensiones de consulta de 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
{
    ...
}

3

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.


3

Solo quería agregar que Bertrand Meyer, el inventor de Eiffel y el diseño por contrato, habría Teamheredado List<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 Windowheredado de ambos Rectangley Tree<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 Addy Removeen Hirey Fireen su Team. Si una instancia de Teamestá de vuelta a upcast List<Player>, el llamante usar Addy Removemodificar, pero sus métodos virtuales Hirey Fireserá llamado.


1
Aquí el problema no es la herencia múltiple, mixins (etc.) o cómo lidiar con su ausencia. En cualquier idioma, incluso en lenguaje humano, un equipo no es una lista de personas, sino que se compone de una lista de personas. Este es un concepto abstracto. Si Bertrand Meyer o alguien maneja un equipo subclasificando, List está haciendo mal. Debería subclasificar una Lista si desea un tipo de lista más específico. Espero que estés de acuerdo.
Mauro Sampietro

1
Eso depende de lo que la herencia sea para ti. En sí mismo, es una operación abstracta, no significa que dos clases estén en una relación de "es un tipo especial de", aunque sea la interpretación principal. Sin embargo, la herencia puede tratarse como un mecanismo para la reutilización de la implementación si el diseño del lenguaje lo admite, a pesar de que la composición es la alternativa preferida actualmente.
Alexey

2

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?

public class PlayerCollection : Collection<Player> 
{ 
}

Y luego el FootballTeam 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();
}

2

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 :

  • 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, a System.Listes 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.Listtipo de datos se llame "lista" es un poco raro. A System.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:

  • ¿Necesito contar? Si no, considere implementarIEnumerable<T>
  • ¿Esta colección cambiará después de que se haya inicializado? Si no lo consideras IReadonlyList<T>.
  • ¿Es importante que pueda acceder a los elementos por índice? ConsiderarICollection<T>
  • ¿Es importante el orden en el que agrego elementos a la colección? Tal vez es un ISet<T>?
  • Si realmente desea estas cosas, continúe e implemente 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 IArrayinterfaz para la biblioteca LinqArray .


2

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.

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

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

Lectura adicional: cuando una clase se hereda de List <>, XmlSerializer no serializa otros atributos


0

¿Cuándo es aceptable?

Para citar a Eric Lippert:

Cuando estás construyendo un mecanismo que extiende el List<T>mecanismo.

Por ejemplo, estás cansado de la ausencia del AddRangemétodo en IList<T>:

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

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.