Perché / quando dovresti usare le classi annidate in .net? O non dovresti?

Nel recente post sul blog di Kathleen Dollard , presenta un motivo interessante per utilizzare classi nidificate in .net. Tuttavia, lei cita anche che a FxCop non piacciono le classi annidate. Suppongo che le persone che scrivono le regole di FxCop non siano stupide, quindi ci deve essere un ragionamento dietro quella posizione, ma non sono stato in grado di trovarlo.

Usa una class annidata quando la class che stai nidificando è utile solo alla class che la include. Ad esempio, le classi nidificate ti permettono di scrivere qualcosa come (semplificato):

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

Puoi creare una definizione completa della tua class in un posto, non devi passare attraverso nessun cerchio PIMPL per definire come funziona la tua class, e il mondo esterno non ha bisogno di vedere nulla della tua implementazione.

Se la class TreeNode era esterna, dovresti rendere pubblici tutti i campi o fare un sacco di metodi get/set per usarlo. Il mondo esterno avrebbe un’altra class che inquina la loro intelligenza.

Dal tutorial Java di Sun:

Perché usare le classi annidate? Ci sono diversi motivi validi per l’utilizzo di classi annidate, tra cui:

  • È un modo per raggruppare logicamente le classi che vengono utilizzate solo in un posto.
  • Aumenta l’incapsulamento.
  • Le classi annidate possono portare a un codice più leggibile e gestibile.

Raggruppamento logico di classi: se una class è utile solo per un’altra class, è logico incorporarla in quella class e mantenerla insieme. Annidare queste “classi di supporto” rende il loro pacchetto più snello.

Incremento aumentato: prendi in considerazione due classi di primo livello, A e B, in cui B ha bisogno di accedere ai membri di A che altrimenti sarebbero dichiarati privati. Nascondendo la class B all’interno della class A, i membri di A possono essere dichiarati privati ​​e B può accedervi. Inoltre, la stessa B può essere nascosta dal mondo esterno. < - Questo non si applica all'implementazione di classi nidificate di C #, questo si applica solo a Java.

Codice più leggibile e gestibile: l’inserimento di classi di piccole dimensioni all’interno delle classi di primo livello posiziona il codice più vicino a dove viene utilizzato.

Modello singleton completamente pigro e sicuro per i thread

 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(); } } 

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

Dipende dall’uso. Io raramente userei mai una class nidificata pubblica, ma uso sempre classi nidificate private tutte le volte. Una class nidificata privata può essere utilizzata per un object secondario che deve essere utilizzato solo all’interno del genitore. Un esempio potrebbe essere se una class HashTable contiene un object Entry privato per memorizzare solo i dati internamente.

Se la class è pensata per essere utilizzata dal chiamante (esternamente), generalmente mi piace renderla una class autonoma separata.

Oltre agli altri motivi sopra elencati, c’è un altro motivo per cui posso pensare non solo a usare classi nidificate, ma in effetti classi nidificate pubbliche. Per coloro che lavorano con più classi generiche che condividono gli stessi parametri di tipo generico, la possibilità di dichiarare uno spazio dei nomi generico sarebbe estremamente utile. Sfortunatamente, .Net (o almeno C #) non supporta l’idea di spazi dei nomi generici. Quindi, per raggiungere lo stesso objective, possiamo utilizzare le classi generiche per raggiungere lo stesso objective. Prendi le seguenti classi di esempio relative a un’ quadro logica:

 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 { } 

Possiamo semplificare le firme di queste classi utilizzando uno spazio dei nomi generico (implementato tramite classi nidificate):

 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 {} } 

Quindi, attraverso l’uso di classi parziali come suggerito da Erik van Brakel in un commento precedente, è ansible separare le classi in file nidificati separati. Raccomando di utilizzare un’estensione di Visual Studio come NestIn per supportare l’annidamento dei file di classi parziali. Ciò consente di utilizzare i file di class “namespace” anche per organizzare i file di class nidificati in un modo simile alla cartella.

Per esempio:

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); } } 

I file nell’esploratore di soluzioni di Visual Studio sarebbero quindi organizzati come tali:

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

E implementeresti lo spazio dei nomi generico come il seguente:

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 {} } 

E i file sarebbero organizzati in Solution Explorer come segue:

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

Quanto sopra è un semplice esempio di utilizzo di una class esterna come spazio dei nomi generico. Ho costruito “spazi dei nomi generici” contenenti 9 o più parametri di tipo nel passato. Dovendo mantenere sincronizzati quei parametri di tipo tra i nove tipi che tutti dovevano conoscere i parametri del tipo era noioso, specialmente quando si aggiungeva un nuovo parametro. L’uso di spazi dei nomi generici rende questo codice molto più gestibile e leggibile.

Se capisco bene l’articolo di Katheleen, propone di usare la class nidificata per poter scrivere SomeEntity.Collection invece di EntityCollection . Secondo me è controverso il modo di risparmiarti un po ‘di digitazione. Sono abbastanza sicuro che nel mondo reale le raccolte di applicazioni avranno alcune differenze nelle implementazioni, quindi dovrai comunque creare classi separate. Penso che usare il nome della class per limitare l’ambito di un’altra class non sia una buona idea. Inquina l’intelligenza e rafforza le dipendenze tra le classi. L’uso degli spazi dei nomi è un modo standard per controllare l’ambito delle classi. Tuttavia, trovo che l’uso di classi annidate come nel commento @hazzen sia accettabile a meno che non si abbiano tonnellate di classi annidate che sono un segno di progettazione errata.

Un altro uso non ancora menzionato per le classi annidate è la segregazione di tipi generici. Ad esempio, si supponga di voler avere alcune famiglie generiche di classi statiche che possono utilizzare metodi con vari numeri di parametri, insieme a valori per alcuni di questi parametri e generare delegati con un numero inferiore di parametri. Ad esempio, si desidera avere un metodo statico che può prendere Action e fornire una String che chiamerà l’azione fornita che passa a 3.5 come double ; si potrebbe anche desiderare di avere un metodo statico che può prendere Action e produrre Action , passando 7 come int e 5.3 come double . Usando classi nidificate generiche, è ansible fare in modo che le invocazioni del metodo siano qualcosa come:

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

oppure, poiché gli ultimi tipi in ogni espressione possono essere dedotti anche se i precedenti non possono:

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

L’uso dei tipi generici nidificati consente di stabilire quali delegati sono applicabili a quali parti della descrizione generale del tipo.

Le classi nidificate possono essere utilizzate per le seguenti esigenze:

  1. Classificazione dei dati
  2. Quando la logica della class principale è complicata e ti senti come se avessi bisogno di oggetti subordinati per gestire la class
  3. Quando tu che lo stato e l’esistenza della class dipendono completamente dalla class che li racchiude

Uso spesso classi annidate per hide i dettagli di implementazione. Un esempio della risposta di Eric Lippert qui:

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

Questo modello diventa ancora migliore con l’uso di farmaci generici. Vedi questa domanda per due fantastici esempi. Così finisco per scrivere

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

invece di

 new EqualityComparer(p => p.Id); 

Inoltre posso avere una lista generica di Equality ma non EqualityComparer

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

mentre

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

non è ansible. Questo è il vantaggio della class nidificata che eredita dalla class genitore.

Un altro caso (della stessa natura – l’implementazione nascosta) è quando si desidera rendere i membri di una class (campi, proprietà ecc.) Accessibili solo per una singola class:

 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 } 

Dato che nawfal ha menzionato l’implementazione del pattern Abstract Factory, quel codice può essere esteso per ottenere un pattern di Cluster di Classe basato sul pattern Abstract Factory.

Mi piace annidare eccezioni che sono uniche per una singola class, vale a dire. quelli che non vengono mai lanciati da nessun altro luogo.

Per esempio:

 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() } } 

Ciò aiuta a mantenere ordinati i file del progetto e non è pieno di cento piccole classi di eccezioni.

Ricorda che dovrai testare la class annidata. Se è privato, non sarai in grado di testarlo da solo.

È ansible renderlo interno, tuttavia, in combinazione con l’attributo InternalsVisibleTo . Tuttavia, ciò equivarrebbe a rendere interno un campo privato solo a scopo di test, che considero una scarsa autocomposizione.

Quindi, potresti voler implementare solo classi nidificate private che implicano una bassa complessità.

si per questo 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 = "Programació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); } } }