Por que/quando você deve usar classes aninhadas em .net?Ou não deveria?
Pergunta
Em Postagem recente no blog de Kathleen Dollard, ela apresenta um motivo interessante para usar classes aninhadas em .net.No entanto, ela também menciona que o FxCop não gosta de classes aninhadas.Presumo que as pessoas que escrevem as regras do FxCop não sejam estúpidas, então deve haver um raciocínio por trás dessa posição, mas não consegui encontrá-lo.
Solução
Use uma classe aninhada quando a classe que você está aninhando for útil apenas para a classe envolvente.Por exemplo, classes aninhadas permitem escrever algo como (simplificado):
public class SortedMap {
private class TreeNode {
TreeNode left;
TreeNode right;
}
}
Você pode fazer uma definição completa de sua classe em um só lugar, não precisa passar por nenhum obstáculo do PIMPL para definir como sua classe funciona e o mundo externo não precisa ver nada de sua implementação.
Se a classe TreeNode fosse externa, você teria que criar todos os campos public
ou fazer um monte de get/set
métodos para usá-lo.O mundo exterior teria outra classe poluindo seu intellisense.
Outras dicas
Por que usar classes aninhadas?Existem vários motivos convincentes para usar classes aninhadas, entre eles:
- É uma forma de agrupar logicamente classes que são utilizadas apenas em um local.
- Aumenta o encapsulamento.
- Classes aninhadas podem levar a um código mais legível e de fácil manutenção.
Agrupamento lógico de classes — Se uma classe é útil apenas para uma outra classe, então é lógico incorporá-la nessa classe e manter as duas juntas.Aninhar essas "classes auxiliares" torna seu pacote mais simplificado.
Maior encapsulamento – Considere duas classes de nível superior, A e B, onde B precisa de acesso a membros de A que de outra forma seriam declarados privados.Ao ocultar a classe B dentro da classe A, os membros de A podem ser declarados privados e B pode acessá-los.Além disso, o próprio B pode ser escondido do mundo exterior. <- Isso não se aplica à implementação de classes aninhadas em C#, apenas se aplica a Java.
Código mais legível e de fácil manutenção: aninhar classes pequenas em classes de nível superior coloca o código mais próximo de onde ele é usado.
Padrão singleton totalmente preguiçoso e seguro para threads
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();
}
}
Depende do uso.Eu raramente usaria uma classe aninhada pública, mas usaria classes aninhadas privadas o tempo todo.Uma classe aninhada privada pode ser usada para um subobjeto que se destina a ser usado apenas dentro do pai.Um exemplo disso seria se uma classe HashTable contivesse um objeto Entry privado para armazenar dados apenas internamente.
Se a classe for usada pelo chamador (externamente), geralmente gosto de torná-la uma classe independente separada.
Além dos outros motivos listados acima, há mais um motivo em que consigo pensar não apenas para usar classes aninhadas, mas de fato classes aninhadas públicas.Para aqueles que trabalham com múltiplas classes genéricas que compartilham os mesmos parâmetros de tipo genérico, a capacidade de declarar um namespace genérico seria extremamente útil.Infelizmente, .Net (ou pelo menos C#) não suporta a ideia de namespaces genéricos.Portanto, para atingir o mesmo objetivo, podemos usar classes genéricas para cumprir o mesmo objetivo.Tomemos os seguintes exemplos de classes relacionadas a uma entidade lógica:
public class BaseDataObject
<
tDataObject,
tDataObjectList,
tBusiness,
tDataAccess
>
where tDataObject : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
where tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
where tBusiness : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
where tDataAccess : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}
public class BaseDataObjectList
<
tDataObject,
tDataObjectList,
tBusiness,
tDataAccess
>
:
CollectionBase<tDataObject>
where tDataObject : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
where tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
where tBusiness : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
where tDataAccess : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}
public interface IBaseBusiness
<
tDataObject,
tDataObjectList,
tBusiness,
tDataAccess
>
where tDataObject : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
where tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
where tBusiness : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
where tDataAccess : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}
public interface IBaseDataAccess
<
tDataObject,
tDataObjectList,
tBusiness,
tDataAccess
>
where tDataObject : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
where tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
where tBusiness : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
where tDataAccess : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}
Podemos simplificar as assinaturas dessas classes usando um namespace genérico (implementado por meio de classes aninhadas):
public
partial class Entity
<
tDataObject,
tDataObjectList,
tBusiness,
tDataAccess
>
where tDataObject : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObject
where tDataObjectList : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObjectList, new()
where tBusiness : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseBusiness
where tDataAccess : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseDataAccess
{
public class BaseDataObject {}
public class BaseDataObjectList : CollectionBase<tDataObject> {}
public interface IBaseBusiness {}
public interface IBaseDataAccess {}
}
Então, através do uso de classes parciais, conforme sugerido por Erik van Brakel em um comentário anterior, você pode separar as classes em arquivos aninhados separados.Eu recomendo usar uma extensão do Visual Studio como NestIn para oferecer suporte ao aninhamento de arquivos de classe parciais.Isso permite que os arquivos de classe "namespace" também sejam usados para organizar os arquivos de classe aninhados em uma pasta.
Por exemplo:
Entidade.cs
public
partial class Entity
<
tDataObject,
tDataObjectList,
tBusiness,
tDataAccess
>
where tDataObject : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObject
where tDataObjectList : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObjectList, new()
where tBusiness : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseBusiness
where tDataAccess : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseDataAccess
{
}
Entidade.BaseDataObject.cs
partial class Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
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;
}
}
}
Entidade.BaseDataObjectList.cs
partial class Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
public class BaseDataObjectList : CollectionBase<tDataObject>
{
public tDataObjectList ShallowClone()
{
var returnList = new tDataObjectList();
returnList.AddRange(this);
return returnList;
}
}
}
Entidade.IBaseBusiness.cs
partial class Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
public interface IBaseBusiness
{
tDataObjectList Load();
void Delete();
void Save(tDataObjectList data);
}
}
Entidade.IBaseDataAccess.cs
partial class Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
public interface IBaseDataAccess
{
tDataObjectList Load();
void Delete();
void Save(tDataObjectList data);
}
}
Os arquivos no explorador de soluções do visual studio seriam então organizados da seguinte forma:
Entity.cs
+ Entity.BaseDataObject.cs
+ Entity.BaseDataObjectList.cs
+ Entity.IBaseBusiness.cs
+ Entity.IBaseDataAccess.cs
E você implementaria o namespace genérico da seguinte forma:
Usuário.cs
public
partial class User
:
Entity
<
User.DataObject,
User.DataObjectList,
User.IBusiness,
User.IDataAccess
>
{
}
Usuário.DataObject.cs
partial class User
{
public class DataObject : BaseDataObject
{
public string UserName { get; set; }
public byte[] PasswordHash { get; set; }
public bool AccountIsEnabled { get; set; }
}
}
Usuário.DataObjectList.cs
partial class User
{
public class DataObjectList : BaseDataObjectList {}
}
Usuário.IBusiness.cs
partial class User
{
public interface IBusiness : IBaseBusiness {}
}
Usuário.IDataAccess.cs
partial class User
{
public interface IDataAccess : IBaseDataAccess {}
}
E os arquivos seriam organizados no explorador de soluções da seguinte forma:
User.cs
+ User.DataObject.cs
+ User.DataObjectList.cs
+ User.IBusiness.cs
+ User.IDataAccess.cs
O exemplo acima é um exemplo simples de uso de uma classe externa como um namespace genérico.Eu construí "namespaces genéricos" contendo 9 ou mais parâmetros de tipo no passado.Ter que manter esses parâmetros de tipo sincronizados entre os nove tipos que todos precisavam para saber os parâmetros de tipo era entediante, especialmente ao adicionar um novo parâmetro.O uso de namespaces genéricos torna esse código muito mais gerenciável e legível.
Se bem entendi o artigo de Katheleen, ela propõe usar uma classe aninhada para poder escrever SomeEntity.Collection em vez de EntityCollection<SomeEntity>.Na minha opinião, é uma maneira controversa de economizar digitação.Tenho certeza de que no mundo real as coleções de aplicativos terão alguma diferença nas implementações, então você precisará criar uma classe separada de qualquer maneira.Acho que usar o nome da classe para limitar o escopo de outra classe não é uma boa ideia.Polui o intellisense e fortalece as dependências entre as classes.Usar namespaces é uma maneira padrão de controlar o escopo das classes.No entanto, acho que o uso de classes aninhadas como no comentário @hazzen é aceitável, a menos que você tenha toneladas de classes aninhadas, o que é um sinal de design ruim.
Outro uso ainda não mencionado para classes aninhadas é a segregação de tipos genéricos.Por exemplo, suponha que alguém queira ter algumas famílias genéricas de classes estáticas que possam receber métodos com vários números de parâmetros, juntamente com valores para alguns desses parâmetros, e gerar delegados com menos parâmetros.Por exemplo, deseja-se ter um método estático que possa assumir um Action<string, int, double>
e render um String<string, int>
que chamará a ação fornecida passando 3,5 como o double
;pode-se também desejar ter um método estático que possa assumir um Action<string, int, double>
e render um Action<string>
, passando 7
Enquanto o int
e 5.3
Enquanto o double
.Usando classes aninhadas genéricas, pode-se fazer com que as invocações de método sejam algo como:
MakeDelegate<string,int>.WithParams<double>(theDelegate, 3.5);
MakeDelegate<string>.WithParams<int,double>(theDelegate, 7, 5.3);
ou, porque os últimos tipos em cada expressão podem ser inferidos mesmo que os primeiros não possam:
MakeDelegate<string,int>.WithParams(theDelegate, 3.5);
MakeDelegate<string>.WithParams(theDelegate, 7, 5.3);
O uso de tipos genéricos aninhados possibilita saber quais delegados são aplicáveis a quais partes da descrição geral do tipo.
As classes aninhadas podem ser usadas para as seguintes necessidades:
- Classificação dos dados
- Quando a lógica da classe principal é complicada e você sente que precisa de objetos subordinados para gerenciar a classe
- Quando você percebe que o estado e a existência da classe dependem totalmente da classe envolvente
Costumo usar classes aninhadas para ocultar detalhes de implementação. Um exemplo da resposta de Eric Lippert aqui:
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 { ... }
}
Esse padrão fica ainda melhor com o uso de genéricos. Ver essa questão para dois exemplos legais.Então acabo escrevendo
Equality<Person>.CreateComparer(p => p.Id);
em vez de
new EqualityComparer<Person, int>(p => p.Id);
Também posso ter uma lista genérica de Equality<Person>
mas não EqualityComparer<Person, int>
var l = new List<Equality<Person>>
{
Equality<Person>.CreateComparer(p => p.Id),
Equality<Person>.CreateComparer(p => p.Name)
}
enquanto
var l = new List<EqualityComparer<Person, ??>>>
{
new EqualityComparer<Person, int>>(p => p.Id),
new EqualityComparer<Person, string>>(p => p.Name)
}
não é possível.Esse é o benefício da classe aninhada herdar da classe pai.
Outro caso (da mesma natureza - ocultando a implementação) é quando você deseja tornar os membros de uma classe (campos, propriedades etc.) acessíveis apenas para uma única classe:
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 mencionada implementação do padrão Abstract Factory, esse código pode ser estendido para alcançar Padrão de clusters de classe que é baseado no padrão Abstract Factory.
Gosto de aninhar exceções exclusivas de uma única classe, ou seja.aqueles que nunca são jogados de qualquer outro lugar.
Por exemplo:
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()
}
}
Isso ajuda a manter os arquivos do seu projeto organizados e não cheios de centenas de pequenas classes de exceção.
Lembre-se de que você precisará testar a classe aninhada.Se for privado, você não poderá testá-lo isoladamente.
Você poderia torná-lo interno, no entanto, Em conjunto com o InternalsVisibleTo
atributo.Porém, isso seria o mesmo que tornar interno um campo privado apenas para fins de teste, o que considero uma autodocumentação ruim.
Portanto, você pode querer implementar apenas classes aninhadas privadas que envolvam baixa complexidade.
sim 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<Departamento> departamentos = new List<Departamento>();
departamentos.Add(new Departamento { idDepto = 1, nombreDepto = "Arquitectura" });
departamentos.Add(new Departamento { idDepto = 2, nombreDepto = "Programación" });
List<Empleado> empleados = new List<Empleado>();
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);
}
}
}