¿Por qué / cuándo debería usar clases anidadas en .net? ¿O no deberías?

En la publicación de blog reciente de Kathleen Dollard , presenta una razón interesante para utilizar clases anidadas en .net. Sin embargo, ella también menciona que a FxCop no le gustan las clases anidadas. Supongo que las personas que escriben reglas FxCop no son estúpidas, por lo que debe haber un razonamiento detrás de esa posición, pero no he podido encontrarlo.

Utilice una clase anidada cuando la clase que está anidando solo sea útil para la clase adjunta. Por ejemplo, las clases anidadas le permiten escribir algo como (simplificado):

public class SortedMap { private class TreeNode { TreeNode left; TreeNode right; } } 

Puedes hacer una definición completa de tu clase en un solo lugar, no tienes que saltar a través de los aros de PIMPL para definir cómo funciona tu clase, y el mundo exterior no necesita ver nada de tu implementación.

Si la clase TreeNode era externa, tendrías que hacer public todos los campos o crear un get/set métodos get/set para usarla. El mundo exterior tendría otra clase contaminando su intellisense.

Del tutorial de Java de Sun:

¿Por qué usar clases anidadas? Hay varias razones convincentes para usar clases anidadas, entre ellas:

  • Es una forma de agrupar lógicamente clases que solo se usan en un solo lugar.
  • Aumenta la encapsulación.
  • Las clases anidadas pueden conducir a un código más legible y mantenible.

Agrupación lógica de clases: si una clase es útil solo para otra clase, entonces es lógico incluirla en esa clase y mantener las dos juntas. Anidar tales “clases de ayuda” hace que su paquete sea más eficiente.

Encapsulación incrementada: Considere dos clases de nivel superior, A y B, donde B necesita acceso a los miembros de A que, de lo contrario, serían declarados privados. Al ocultar la clase B dentro de la clase A, los miembros de A pueden declararse privados y B puede acceder a ellos. Además, B puede ocultarse del mundo exterior. <- Esto no se aplica a la implementación de clases anidadas de C #, esto solo se aplica a Java.

Código más legible y fácil de mantener: las clases pequeñas de jerarquización dentro de las clases de nivel superior colocan el código más cerca de donde se usa.

Patrón de singleton totalmente vago y seguro para subprocesos

 public sealed class Singleton { Singleton() { } public static Singleton Instance { get { return Nested.instance; } } class Nested { // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Nested() { } internal static readonly Singleton instance = new Singleton(); } } 

fuente: http://www.yoda.arachsys.com/csharp/singleton.html

Depende del uso. Raramente usaría una clase anidada pública pero usaría clases anidadas privadas todo el tiempo. Una clase anidada privada se puede usar para un subobjeto que se pretende usar solo dentro del padre. Un ejemplo de esto sería si una clase HashTable contiene un objeto de entrada privado para almacenar datos internamente solamente.

Si la clase está destinada a ser utilizada por la persona que llama (externamente), generalmente me gusta convertirla en una clase independiente separada.

Además de los otros motivos enumerados anteriormente, hay una razón más por la que puedo pensar no solo en el uso de clases anidadas, sino también en clases anidadas públicas. Para aquellos que trabajan con múltiples clases genéricas que comparten los mismos parámetros de tipo genérico, la capacidad de declarar un espacio de nombres genérico sería extremadamente útil. Desafortunadamente, .Net (o al menos C #) no admite la idea de espacios de nombres generics. Entonces, para lograr el mismo objective, podemos usar clases genéricas para cumplir el mismo objective. Tome las siguientes clases de ejemplo relacionadas con una entidad lógica:

 public class BaseDataObject < tDataObject, tDataObjectList, tBusiness, tDataAccess > where tDataObject : BaseDataObject where tDataObjectList : BaseDataObjectList, new() where tBusiness : IBaseBusiness where tDataAccess : IBaseDataAccess { } public class BaseDataObjectList < tDataObject, tDataObjectList, tBusiness, tDataAccess > : CollectionBase where tDataObject : BaseDataObject where tDataObjectList : BaseDataObjectList, new() where tBusiness : IBaseBusiness where tDataAccess : IBaseDataAccess { } public interface IBaseBusiness < tDataObject, tDataObjectList, tBusiness, tDataAccess > where tDataObject : BaseDataObject where tDataObjectList : BaseDataObjectList, new() where tBusiness : IBaseBusiness where tDataAccess : IBaseDataAccess { } public interface IBaseDataAccess < tDataObject, tDataObjectList, tBusiness, tDataAccess > where tDataObject : BaseDataObject where tDataObjectList : BaseDataObjectList, new() where tBusiness : IBaseBusiness where tDataAccess : IBaseDataAccess { } 

Podemos simplificar las firmas de estas clases mediante el uso de un espacio de nombres genérico (implementado a través de clases anidadas):

 public partial class Entity < tDataObject, tDataObjectList, tBusiness, tDataAccess > where tDataObject : Entity.BaseDataObject where tDataObjectList : Entity.BaseDataObjectList, new() where tBusiness : Entity.IBaseBusiness where tDataAccess : Entity.IBaseDataAccess { public class BaseDataObject {} public class BaseDataObjectList : CollectionBase {} public interface IBaseBusiness {} public interface IBaseDataAccess {} } 

Luego, mediante el uso de clases parciales como lo sugirió Erik van Brakel en un comentario anterior, puede separar las clases en archivos separados nesteds. Recomiendo usar una extensión de Visual Studio como NestIn para admitir el anidamiento de los archivos de clase parciales. Esto permite que los archivos de clase de “espacio de nombres” también se utilicen para organizar los archivos de clase nesteds en una carpeta de forma similar.

Por ejemplo:

Entity.cs

 public partial class Entity < tDataObject, tDataObjectList, tBusiness, tDataAccess > where tDataObject : Entity.BaseDataObject where tDataObjectList : Entity.BaseDataObjectList, new() where tBusiness : Entity.IBaseBusiness where tDataAccess : Entity.IBaseDataAccess { } 

Entity.BaseDataObject.cs

 partial class Entity { public class BaseDataObject { public DataTimeOffset CreatedDateTime { get; set; } public Guid CreatedById { get; set; } public Guid Id { get; set; } public DataTimeOffset LastUpdateDateTime { get; set; } public Guid LastUpdatedById { get; set; } public static implicit operator tDataObjectList(DataObject dataObject) { var returnList = new tDataObjectList(); returnList.Add((tDataObject) this); return returnList; } } } 

Entity.BaseDataObjectList.cs

 partial class Entity { public class BaseDataObjectList : CollectionBase { public tDataObjectList ShallowClone() { var returnList = new tDataObjectList(); returnList.AddRange(this); return returnList; } } } 

Entity.IBaseBusiness.cs

 partial class Entity { public interface IBaseBusiness { tDataObjectList Load(); void Delete(); void Save(tDataObjectList data); } } 

Entity.IBaseDataAccess.cs

 partial class Entity { public interface IBaseDataAccess { tDataObjectList Load(); void Delete(); void Save(tDataObjectList data); } } 

Los archivos en el explorador de soluciones de Visual Studio se organizarían como tales:

 Entity.cs + Entity.BaseDataObject.cs + Entity.BaseDataObjectList.cs + Entity.IBaseBusiness.cs + Entity.IBaseDataAccess.cs 

Y usted implementaría el espacio de nombre genérico de la siguiente manera:

User.cs

 public partial class User : Entity < User.DataObject, User.DataObjectList, User.IBusiness, User.IDataAccess > { } 

User.DataObject.cs

 partial class User { public class DataObject : BaseDataObject { public string UserName { get; set; } public byte[] PasswordHash { get; set; } public bool AccountIsEnabled { get; set; } } } 

User.DataObjectList.cs

 partial class User { public class DataObjectList : BaseDataObjectList {} } 

User.IBusiness.cs

 partial class User { public interface IBusiness : IBaseBusiness {} } 

User.IDataAccess.cs

 partial class User { public interface IDataAccess : IBaseDataAccess {} } 

Y los archivos se organizarían en el explorador de soluciones de la siguiente manera:

 User.cs + User.DataObject.cs + User.DataObjectList.cs + User.IBusiness.cs + User.IDataAccess.cs 

Lo anterior es un ejemplo simple de usar una clase externa como un espacio de nombres genérico. Creé “espacios de nombres generics” que contienen 9 o más parámetros de tipo en el pasado. Tener que mantener esos parámetros de tipo sincronizados en los nueve tipos que todos necesitaban conocer los parámetros de tipo era tedioso, especialmente al agregar un nuevo parámetro. El uso de espacios de nombres generics hace que ese código sea mucho más manejable y legible.

Si entiendo bien el artículo de Katheleen, ella propone usar una clase anidada para poder escribir SomeEntity.Collection en lugar de EntityCollection . En mi opinión, es una forma controvertida de ahorrar algo de tipeo. Estoy bastante seguro de que en las colecciones de aplicaciones del mundo real habrá alguna diferencia en las implementaciones, por lo que tendrás que crear clases separadas de todos modos. Creo que usar el nombre de la clase para limitar el scope de otra clase no es una buena idea. Contamina inteligentemente y fortalece las dependencias entre clases. El uso de espacios de nombres es una forma estándar de controlar el scope de las clases. Sin embargo, considero que el uso de clases anidadas como en el comentario de @hazzen es aceptable a menos que tenga toneladas de clases anidadas, lo que es un signo de mal diseño.

Otro uso aún no mencionado para clases anidadas es la segregación de tipos generics. Por ejemplo, supongamos que uno quiere tener algunas familias genéricas de clases estáticas que puedan tomar métodos con varios números de parámetros, junto con valores para algunos de esos parámetros, y generar delegates con menos parámetros. Por ejemplo, uno desea tener un método estático que puede tomar una Action y producir una String que llamará a la acción suministrada pasando 3.5 como el double ; uno también puede desear tener un método estático que puede tomar una Action y producir una Action , pasando 7 como el int y 5.3 como el double . Usando clases genéricas anidadas, uno puede hacer arreglos para que las invocaciones de métodos sean algo así como:

 MakeDelegate.WithParams(theDelegate, 3.5); MakeDelegate.WithParams(theDelegate, 7, 5.3); 

o, porque los últimos tipos en cada expresión se pueden inferir aunque los anteriores no puedan:

 MakeDelegate.WithParams(theDelegate, 3.5); MakeDelegate.WithParams(theDelegate, 7, 5.3); 

El uso de los tipos generics nesteds permite saber qué delegates son aplicables a qué partes de la descripción general del tipo.

Las clases anidadas se pueden usar para las siguientes necesidades:

  1. Clasificación de los datos
  2. Cuando la lógica de la clase principal es complicada y sientes que necesitas objetos subordinados para gestionar la clase
  3. Cuando sepas que el estado y la existencia de la clase dependen completamente de la clase que los incluye

A menudo uso clases anidadas para ocultar detalles de implementación. Un ejemplo de la respuesta de Eric Lippert aquí:

 abstract public class BankAccount { private BankAccount() { } // Now no one else can extend BankAccount because a derived class // must be able to call a constructor, but all the constructors are // private! private sealed class ChequingAccount : BankAccount { ... } public static BankAccount MakeChequingAccount() { return new ChequingAccount(); } private sealed class SavingsAccount : BankAccount { ... } } 

Este patrón se vuelve aún mejor con el uso de generics. Vea esta pregunta para dos ejemplos geniales. Así que termino escribiendo

 Equality.CreateComparer(p => p.Id); 

en lugar de

 new EqualityComparer(p => p.Id); 

También puedo tener una lista genérica de Equality pero no EqualityComparer

 var l = new List> { Equality.CreateComparer(p => p.Id), Equality.CreateComparer(p => p.Name) } 

donde como

 var l = new List>> { new EqualityComparer>(p => p.Id), new EqualityComparer>(p => p.Name) } 

no es posible. Ese es el beneficio de la clase anidada que hereda de la clase principal.

Otro caso (de la misma naturaleza – implementación oculta) es cuando desea hacer que los miembros de una clase (campos, propiedades, etc.) sean accesibles solo para una sola clase:

 public class Outer { class Inner //private class { public int Field; //public field } static inner = new Inner { Field = -1 }; // Field is accessible here, but in no other class } 

Como nawfal mencionó la implementación del patrón Abstract Factory, ese código puede ser desviado para lograr un patrón de Class Clusters basado en el patrón Abstract Factory.

Me gusta anidar excepciones que son exclusivas de una sola clase, es decir. unos que nunca se arrojan desde ningún otro lugar.

Por ejemplo:

 public class MyClass { void DoStuff() { if (!someArbitraryCondition) { // This is the only class from which OhNoException is thrown throw new OhNoException( "Oh no! Some arbitrary condition was not satisfied!"); } // Do other stuff } public class OhNoException : Exception { // Constructors calling base() } } 

Esto ayuda a mantener ordenados los archivos de su proyecto y no están llenos de cientos de pequeñas clases de excepción.

Tenga en cuenta que deberá probar la clase anidada. Si es privado, no podrás probarlo de forma aislada.

Sin embargo, podría hacerlo interno junto con el atributo InternalsVisibleTo . Sin embargo, esto sería lo mismo que hacer un campo privado interno solo para fines de prueba, lo cual considero una mala auto documentación.

Por lo tanto, es posible que desee implementar solo clases privadas anidadas de baja complejidad.

sí para este caso:

 class Join_Operator { class Departamento { public int idDepto { get; set; } public string nombreDepto { get; set; } } class Empleado { public int idDepto { get; set; } public string nombreEmpleado { get; set; } } public void JoinTables() { List departamentos = new List(); departamentos.Add(new Departamento { idDepto = 1, nombreDepto = "Arquitectura" }); departamentos.Add(new Departamento { idDepto = 2, nombreDepto = "Progtwigción" }); List empleados = new List(); empleados.Add(new Empleado { idDepto = 1, nombreEmpleado = "John Doe." }); empleados.Add(new Empleado { idDepto = 2, nombreEmpleado = "Jim Bell" }); var joinList = (from e in empleados join d in departamentos on e.idDepto equals d.idDepto select new { nombreEmpleado = e.nombreEmpleado, nombreDepto = d.nombreDepto }); foreach (var dato in joinList) { Console.WriteLine("{0} es empleado del departamento de {1}", dato.nombreEmpleado, dato.nombreDepto); } } }