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);
}
}
}
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");
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.
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.
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.
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."
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.
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?