Pourquoi/quand devriez-vous utiliser des classes imbriquées dans .net ?Ou ne devriez-vous pas ?

StackOverflow https://stackoverflow.com/questions/48872

  •  09-06-2019
  •  | 
  •  

Question

Dans Article de blog récent de Kathleen Dollard, elle présente une raison intéressante d'utiliser des classes imbriquées dans .net.Cependant, elle mentionne également que FxCop n'aime pas les classes imbriquées.Je suppose que les personnes qui écrivent les règles de FxCop ne sont pas stupides, il doit donc y avoir un raisonnement derrière cette position, mais je n'ai pas pu le trouver.

Était-ce utile?

La solution

Utilisez une classe imbriquée lorsque la classe que vous imbriquez n’est utile qu’à la classe englobante.Par exemple, les classes imbriquées vous permettent d'écrire quelque chose comme (simplifié) :

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

Vous pouvez créer une définition complète de votre classe en un seul endroit, vous n'avez pas besoin de franchir des étapes PIMPL pour définir le fonctionnement de votre classe, et le monde extérieur n'a pas besoin de voir quoi que ce soit de votre implémentation.

Si la classe TreeNode était externe, vous devrez soit créer tous les champs public ou faites un tas de get/set méthodes pour l'utiliser.Le monde extérieur aurait une autre classe qui polluerait son intellisense.

Autres conseils

Extrait du didacticiel Java de Sun :

Pourquoi utiliser des classes imbriquées ?Il existe plusieurs raisons impérieuses d’utiliser des classes imbriquées, parmi lesquelles :

  • C'est une manière de regrouper logiquement des classes qui ne sont utilisées qu'à un seul endroit.
  • Cela augmente l’encapsulation.
  • Les classes imbriquées peuvent conduire à un code plus lisible et maintenable.

Regroupement logique de classes : si une classe n'est utile qu'à une seule autre classe, il est alors logique de l'intégrer dans cette classe et de conserver les deux ensemble.L'imbrication de ces "classes d'assistance" rend leur package plus rationalisé.

Encapsulation accrue : considérez deux classes de niveau supérieur, A et B, où B a besoin d'accéder aux membres de A qui seraient autrement déclarés privés.En cachant la classe B dans la classe A, les membres de A peuvent être déclarés privés et B peut y accéder.De plus, B lui-même peut être caché du monde extérieur. <- Cela ne s'applique pas à l'implémentation des classes imbriquées en C#, cela s'applique uniquement à Java.

Code plus lisible et maintenable : l'imbrication de petites classes dans des classes de niveau supérieur rapproche le code de l'endroit où il est utilisé.

Modèle singleton entièrement paresseux et thread-safe

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

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

Cela dépend de l'utilisation.J'utilise rarement une classe imbriquée publique, mais j'utilise tout le temps des classes imbriquées privées.Une classe privée imbriquée peut être utilisée pour un sous-objet destiné à être utilisé uniquement à l'intérieur du parent.Un exemple de ceci serait si une classe HashTable contient un objet Entry privé pour stocker les données en interne uniquement.

Si la classe est destinée à être utilisée par l’appelant (en externe), j’aime généralement en faire une classe autonome distincte.

En plus des autres raisons énumérées ci-dessus, il existe une autre raison à laquelle je peux penser non seulement pour utiliser des classes imbriquées, mais en fait des classes imbriquées publiques.Pour ceux qui travaillent avec plusieurs classes génériques partageant les mêmes paramètres de type générique, la possibilité de déclarer un espace de noms générique serait extrêmement utile.Malheureusement, .Net (ou du moins C#) ne prend pas en charge l'idée d'espaces de noms génériques.Ainsi, afin d'atteindre le même objectif, nous pouvons utiliser des classes génériques pour atteindre le même objectif.Prenons l'exemple de classes suivant lié à une entité logique :

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

Nous pouvons simplifier les signatures de ces classes en utilisant un espace de noms générique (implémenté via des classes imbriquées) :

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

}

Ensuite, grâce à l'utilisation de classes partielles comme suggéré par Erik van Brakel dans un commentaire précédent, vous pouvez séparer les classes en fichiers imbriqués distincts.Je recommande d'utiliser une extension Visual Studio comme NestIn pour prendre en charge l'imbrication des fichiers de classe partiels.Cela permet aux fichiers de classe « espace de noms » d'être également utilisés pour organiser les fichiers de classe imbriqués dans un dossier.

Par exemple:

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

Entité.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;
        }

    }

}

Entity.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;
        }

    }

}

Entité.IBaseBusiness.cs

partial class   Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{

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

}

Entité.IBaseDataAccess.cs

partial class   Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{

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

}

Les fichiers de l'explorateur de solutions Visual Studio seraient alors organisés comme tel :

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

Et vous implémenteriez l'espace de noms générique comme suit :

Utilisateur.cs

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

Utilisateur.DataObject.cs

partial class   User
{

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

}

Utilisateur.DataObjectList.cs

partial class   User
{

    public  class   DataObjectList : BaseDataObjectList {}

}

Utilisateur.IBusiness.cs

partial class   User
{

    public  interface   IBusiness : IBaseBusiness {}

}

Utilisateur.IDataAccess.cs

partial class   User
{

    public  interface   IDataAccess : IBaseDataAccess {}

}

Et les fichiers seraient organisés dans l'explorateur de solutions comme suit :

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

Ce qui précède est un exemple simple d'utilisation d'une classe externe comme espace de noms générique.J'ai déjà construit des "espaces de noms génériques" contenant 9 paramètres de type ou plus.Devoir synchroniser ces paramètres de type sur les neuf types qui devaient tous connaître les paramètres de type était fastidieux, en particulier lors de l'ajout d'un nouveau paramètre.L'utilisation d'espaces de noms génériques rend ce code beaucoup plus gérable et lisible.

Si je comprends bien l'article de Katheleen, elle propose d'utiliser une classe imbriquée pour pouvoir écrire SomeEntity.Collection au lieu de EntityCollection< SomeEntity>.À mon avis, c'est une manière controversée de vous épargner du temps de frappe.Je suis presque sûr que dans le monde réel, les collections d'applications auront des différences dans les implémentations, vous devrez donc de toute façon créer une classe distincte.Je pense qu'utiliser le nom de classe pour limiter la portée des autres classes n'est pas une bonne idée.Cela pollue l’intellisense et renforce les dépendances entre les classes.L'utilisation d'espaces de noms est un moyen standard de contrôler la portée des classes.Cependant, je trouve que l'utilisation de classes imbriquées comme dans le commentaire @hazzen est acceptable, sauf si vous avez des tonnes de classes imbriquées, ce qui est le signe d'une mauvaise conception.

Une autre utilisation non encore mentionnée des classes imbriquées est la ségrégation des types génériques.Par exemple, supposons que l'on souhaite disposer de familles génériques de classes statiques pouvant prendre des méthodes avec différents nombres de paramètres, ainsi que des valeurs pour certains de ces paramètres, et générer des délégués avec moins de paramètres.Par exemple, on souhaite avoir une méthode statique qui puisse prendre une Action<string, int, double> et donne un String<string, int> qui appellera l'action fournie passant 3.5 comme double;on peut aussi souhaiter avoir une méthode statique qui peut prendre un Action<string, int, double> et donne un Action<string>, qui passe 7 comme le int et 5.3 comme le double.En utilisant des classes imbriquées génériques, on peut faire en sorte que les invocations de méthodes ressemblent à ceci :

MakeDelegate<string,int>.WithParams<double>(theDelegate, 3.5);
MakeDelegate<string>.WithParams<int,double>(theDelegate, 7, 5.3);

ou, parce que les derniers types dans chaque expression peuvent être déduits même si les premiers ne le peuvent pas :

MakeDelegate<string,int>.WithParams(theDelegate, 3.5);
MakeDelegate<string>.WithParams(theDelegate, 7, 5.3);

L'utilisation des types génériques imbriqués permet de déterminer quels délégués sont applicables à quelles parties de la description globale du type.

Les classes imbriquées peuvent être utilisées pour les besoins suivants :

  1. Classement des données
  2. Lorsque la logique de la classe principale est compliquée et que vous avez l'impression d'avoir besoin d'objets subordonnés pour gérer la classe
  3. Lorsque vous pensez que l'état et l'existence de la classe dépendent entièrement de la classe englobante

J'utilise souvent des classes imbriquées pour masquer les détails de l'implémentation. Un exemple tiré de la réponse d'Eric Lippert ici :

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

Ce modèle s’améliore encore avec l’utilisation de génériques. Voir cette question pour deux exemples sympas.Alors je finis par écrire

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

au lieu de

new EqualityComparer<Person, int>(p => p.Id);

Je peux aussi avoir une liste générique de Equality<Person> mais non EqualityComparer<Person, int>

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

alors que

var l = new List<EqualityComparer<Person, ??>>> 
        { 
         new EqualityComparer<Person, int>>(p => p.Id),
         new EqualityComparer<Person, string>>(p => p.Name) 
        }

n'est pas possible.C'est l'avantage d'une classe imbriquée héritant de la classe parent.

Un autre cas (de même nature - implémentation masquée) est celui où vous souhaitez rendre les membres d'une classe (champs, propriétés, etc.) accessibles uniquement pour une seule 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
}

Comme nawfal implémentation mentionnée du modèle Abstract Factory, ce code peut être étendu pour atteindre Modèle de clusters de classes qui est basé sur le modèle Abstract Factory.

J'aime imbriquer des exceptions uniques à une seule classe, c'est-à-dire.ceux qui ne sont jamais jetés d’ailleurs.

Par exemple:

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

Cela permet de garder vos fichiers de projet bien rangés et non remplis d'une centaine de petites classes d'exception trapues.

Gardez à l’esprit que vous devrez tester la classe imbriquée.S'il est privé, vous ne pourrez pas le tester de manière isolée.

Vous pourriez cependant le rendre interne, en conjonction avec le InternalsVisibleTo attribut.Cependant, cela reviendrait à créer un champ privé interne uniquement à des fins de test, ce que je considère comme une mauvaise auto-documentation.

Ainsi, vous souhaiterez peut-être implémenter uniquement des classes privées imbriquées impliquant une faible complexité.

oui pour ce cas :

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);
        }
    }
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top