web-dev-qa-db-fra.com

Validation des paramètres du constructeur en C # - Meilleures pratiques

Quelle est la meilleure pratique pour la validation des paramètres constructeur?

Supposons un simple bit de C #:

public class MyClass
{
    public MyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            throw new ArgumentException("Text cannot be empty");

        // continue with normal construction
    }
}

Serait-il acceptable de lever une exception?

L'alternative que j'ai rencontrée était la pré-validation, avant d'instancier:

public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
        {
            MessageBox.Show("Text cannot be empty");
            return null;
        }
        else
        {
            return new MyClass(text);
        }
    }
}
35
MPelletier

J'ai tendance à effectuer toute ma validation dans le constructeur. C'est un must car je crée presque toujours des objets immuables. Pour votre cas spécifique, je pense que cela est acceptable.

if (string.IsNullOrEmpty(text))
    throw new ArgumentException("message", "text");

Si vous utilisez .NET 4, vous pouvez le faire. Bien sûr, cela dépend si vous considérez qu'une chaîne qui ne contient que des espaces blancs n'est pas valide.

if (string.IsNullOrWhiteSpace(text))
    throw new ArgumentException("message", "text");
27
ChaosPandion

Beaucoup de gens déclarent que les constructeurs ne devraient pas lever d'exceptions. KyleG sur cette page , par exemple, fait exactement cela. Honnêtement, je ne vois pas pourquoi.

En C++, lever une exception à partir d'un constructeur est une mauvaise idée, car cela vous laisse une mémoire allouée contenant un objet non initialisé auquel vous n'avez aucune référence (c'est-à-dire qu'il s'agit d'une fuite de mémoire classique). C'est probablement de là que vient la stigmatisation - un groupe de développeurs C++ old-school a à demi créé leur apprentissage C # et a simplement appliqué ce qu'ils savaient du C++. En revanche, dans Objective-C Apple a séparé l'étape d'allocation de l'étape d'initialisation, donc les constructeurs de ce langage peuvent lever des exceptions.

C # ne peut pas fuir la mémoire d'un appel infructueux à un constructeur. Même certaines classes du framework .NET lèveront des exceptions dans leurs constructeurs.

23
Ant

Lance une exception IFF, la classe ne peut pas être mise dans un état cohérent en ce qui concerne son utilisation sémantique. Sinon, non. Ne laissez JAMAIS un objet exister dans un état incohérent. Cela inclut de ne pas fournir de constructeurs complets (comme avoir un constructeur vide + initialize () avant que votre objet ne soit réellement complètement construit) ... DITES JUSTE NON!

À la rigueur, tout le monde le fait. Je l'ai fait l'autre jour pour un objet très étroitement utilisé dans une portée étroite. Un jour sur la route, moi ou quelqu'un d'autre paiera probablement le prix de ce glissement dans la pratique.

Je dois noter que par "constructeur", je veux dire la chose que le client appelle pour construire l'objet. Cela pourrait tout aussi bien être autre chose qu'une construction réelle qui porte le nom de "constructeur". Par exemple, quelque chose comme ça en C++ ne violerait pas le principe IMNSHO:

struct funky_object
{
  ...
private:
  funky_object();
  bool initialize(std::string);

  friend boost::optional<funky_object> build_funky(std::string);
};
boost::optional<funky_object> build_funky(std::string str)
{
  funky_object fo;
  if (fo.initialize(str)) return fo;
  return boost::optional<funky_object>();
}

Étant donné que la seule façon de créer un funky_object est en appelant build_funky le principe de ne jamais laisser un objet invalide exister reste intact même si le "constructeur" réel ne termine pas le travail.

C'est beaucoup de travail supplémentaire, mais pour un gain discutable (peut-être même une perte). Je préférerais toujours la voie d'exception.

13
Edward Strange

Dans ce cas, j'utiliserais la méthode d'usine. Fondamentalement, définissez votre classe n'a que des constructeurs privés et une méthode d'usine qui renvoie une instance de votre objet. Si les paramètres initiaux ne sont pas valides, renvoyez simplement null et demandez au code appelant de décider quoi faire.

public class MyClass
{
    private MyClass(string text)
    {
        //normal construction
    }

    public static MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            return null;
        else
            return new MyClass(text);
    }
}
public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        var cls = MyClass.MakeMyClass(text);
        if(cls == null)
             //show messagebox or throw exception
        return cls;
    }
}

Ne jetez jamais d'exceptions à moins que les conditions soient exceptionnelles. Je pense que dans ce cas, une valeur vide peut être transmise facilement. Si tel est le cas, l'utilisation de ce modèle éviterait les exceptions et les pénalités de performance qu'ils encourent tout en conservant l'état MyClass valide.

9
Donn Relacion
  • Un constructeur ne devrait pas avoir d'effets secondaires.
    • Rien de plus que l'initialisation de champ privé doit être considéré comme un effet secondaire.
    • Un constructeur avec des effets secondaires rompt le principe de responsabilité unique (SRP) et va à l'encontre de l'esprit de la programmation orientée objet (POO).
  • Un constructeur doit être léger et ne doit jamais échouer.
    • Par exemple, je frissonne toujours quand je vois un bloc try-catch à l'intérieur d'un constructeur. Un constructeur ne doit pas lever d'exceptions ou d'erreurs de journal.

On pourrait raisonnablement remettre en question ces directives et dire: "Mais je ne respecte pas ces règles, et mon code fonctionne bien!" À cela, je répondrais: "Eh bien, cela peut être vrai, jusqu'à ce que ce ne soit pas le cas."

  • Les exceptions et les erreurs à l'intérieur d'un constructeur sont très inattendues. À moins qu'on leur dise de le faire, les futurs programmeurs ne seront pas enclins à entourer ces appels de constructeur de code défensif.
  • En cas d'échec de la production, la trace de pile générée peut être difficile à analyser. Le haut de la trace de pile peut pointer vers l'appel du constructeur, mais beaucoup de choses se produisent dans le constructeur, et il peut ne pas pointer vers le LOC réel qui a échoué.
    • J'ai analysé de nombreuses traces de pile .NET où c'était le cas.
2
Jim G.

Ma préférence est de définir une valeur par défaut, mais je sais Java a la bibliothèque "Apache Commons" qui fait quelque chose comme ça, et je pense que c'est aussi une assez bonne idée. Je ne vois pas un problème avec le lancement d'une exception si la valeur non valide mettrait l'objet dans un état inutilisable; une chaîne n'est pas un bon exemple mais que faire si c'était pour l'ID d'un pauvre? Vous ne pouviez pas fonctionner si une valeur de null a été passé à la place, disons, d'une interface ICustomerRepository. Dans une situation comme celle-ci, lever une exception est la bonne façon de gérer les choses, je dirais.

0
Wayne Molina

Cela dépend de ce que MyClass fait. Si MyClass était en fait une classe de référentiel de données et que le texte du paramètre était une chaîne de connexion, la meilleure pratique serait de lever une ArgumentException. Cependant, si MyClass est la classe StringBuilder (par exemple), vous pouvez simplement la laisser vide.

Cela dépend donc de l'importance du texte des paramètres pour la méthode - l'objet a-t-il un sens avec une valeur nulle ou vide?

0
Steven Striga