exception - with - try catch finally java




Quand est-il bon pour un constructeur de lancer une exception? (16)

À cause de tous les problèmes qu'une classe partiellement créée peut causer, je dirais jamais.

Si vous devez valider quelque chose pendant la construction, rendez le constructeur privé et définissez une méthode d'usine statique publique. La méthode peut lancer si quelque chose n'est pas valide. Mais si tout se vérifie, il appelle le constructeur, qui est garanti de ne pas lancer.

Quand est-il bon pour un constructeur de lancer une exception? (Ou dans le cas de l'objectif C: quand est-il bon pour un init'er de revenir à zéro?)

Il me semble qu'un constructeur doit échouer - et donc refuser de créer un objet - si l'objet n'est pas complet. Autrement dit, le constructeur devrait avoir un contrat avec son appelant pour fournir un objet fonctionnel et fonctionnel sur lequel les méthodes peuvent être appelées de manière significative? Est-ce raisonnable?


Autant que je sache, personne ne présente une solution assez évidente qui incarne le meilleur de la construction en une étape et en deux étapes.

note: Cette réponse suppose que C #, mais les principes peuvent être appliqués dans la plupart des langues.

Premièrement, les avantages des deux:

Une étape

La construction en une étape nous permet d'empêcher l'existence d'objets dans un état invalide, empêchant ainsi toute sorte de gestion d'état erronée et tous les bogues qui l'accompagnent. Cependant, certains d'entre nous se sentent bizarres parce que nous ne voulons pas que nos constructeurs lancent des exceptions, et parfois c'est ce que nous devons faire lorsque les arguments d'initialisation sont invalides.

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    public Person(string name, DateTime dateOfBirth)
    {
        if (string.IsNullOrWhitespace(name))
        {
            throw new ArgumentException(nameof(name));
        }

        if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(dateOfBirth));
        }

        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }
}

Deux étapes via la méthode de validation

La construction en deux étapes nous permet d'exécuter notre validation en dehors du constructeur, ce qui évite d'avoir à lancer des exceptions dans le constructeur. Cependant, il nous laisse avec des instances "invalides", ce qui signifie qu'il y a un état que nous devons suivre et gérer pour l'instance, ou nous le jetons immédiatement après l'allocation de tas. Cela soulève la question suivante: pourquoi effectuons-nous une allocation de tas, et donc une collecte de mémoire, sur un objet que nous n'utilisons même pas?

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    public Person(string name, DateTime dateOfBirth)
    {
        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }

    public void Validate()
    {
        if (string.IsNullOrWhitespace(Name))
        {
            throw new ArgumentException(nameof(Name));
        }

        if (DateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
        }
    }
}

Un seul étage via un constructeur privé

Alors, comment pouvons-nous garder les exceptions de nos constructeurs, et nous empêcher d'effectuer l'allocation de tas sur des objets qui seront immédiatement rejetés? C'est assez basique: nous rendons le constructeur privé et créons des instances via une méthode statique désignée pour effectuer une instanciation, et donc une allocation de tas, seulement après validation.

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    private Person(string name, DateTime dateOfBirth)
    {
        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }

    public static Person Create(
        string name,
        DateTime dateOfBirth)
    {
        if (string.IsNullOrWhitespace(Name))
        {
            throw new ArgumentException(nameof(name));
        }

        if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
        }

        return new Person(name, dateOfBirth);
    }
}

Async Single-Stage via un constructeur privé

Outre les avantages de la validation et de la prévention de l'allocation de tas susmentionnés, la méthodologie précédente nous offre un autre avantage intéressant: le soutien asynchrone. Cela s'avère pratique lorsque vous devez gérer une authentification en plusieurs étapes, par exemple lorsque vous devez récupérer un jeton de support avant d'utiliser votre API. De cette façon, vous ne vous retrouvez pas avec un client d'API "déconnecté" invalide, et vous pouvez simplement recréer le client API si vous recevez une erreur d'autorisation en essayant d'effectuer une requête.

public class RestApiClient
{
    public RestApiClient(HttpClient httpClient)
    {
        this.httpClient = new httpClient;
    }

    public async Task<RestApiClient> Create(string username, string password)
    {
        if (username == null)
        {
            throw new ArgumentNullException(nameof(username));
        }

        if (password == null)
        {
            throw new ArgumentNullException(nameof(password));
        }

        var basicAuthBytes = Encoding.ASCII.GetBytes($"{username}:{password}");
        var basicAuthValue = Convert.ToBase64String(basicAuthBytes);

        var authenticationHttpClient = new HttpClient
        {
            BaseUri = new Uri("https://auth.example.io"),
            DefaultRequestHeaders = {
                Authentication = new AuthenticationHeaderValue("Basic", basicAuthValue)
            }
        };

        using (authenticationHttpClient)
        {
            var response = await httpClient.GetAsync("login");
            var content = response.Content.ReadAsStringAsync();
            var authToken = content;
            var restApiHttpClient = new HttpClient
            {
                BaseUri = new Uri("https://api.example.io"), // notice this differs from the auth uri
                DefaultRequestHeaders = {
                    Authentication = new AuthenticationHeaderValue("Bearer", authToken)
                }
            };

            return new RestApiClient(restApiHttpClient);
        }
    }
}

Les inconvénients de cette méthode sont peu nombreux, selon mon expérience.

En général, l'utilisation de cette méthodologie signifie que vous ne pouvez plus utiliser la classe en tant que DTO car la désérialisation d'un objet sans constructeur public par défaut est difficile, au mieux. Cependant, si vous utilisiez l'objet en tant que DTO, vous ne devriez pas vraiment valider l'objet lui-même, mais plutôt invalider les valeurs sur l'objet lorsque vous essayez de les utiliser, car techniquement, les valeurs ne sont pas "invalides" en ce qui concerne à la DTO.

Cela signifie également que vous finirez par créer des méthodes ou des classes d'usine lorsque vous devez autoriser un conteneur IOC à créer l'objet, car sinon le conteneur ne saura pas comment instancier l'objet. Cependant, dans de nombreux cas, les méthodes d'usine finissent par être l'une des méthodes Create .


Il est raisonnable pour un constructeur de lancer une exception tant qu'il se nettoie correctement. Si vous suivez le paradigme RAII (Resource Acquisition Is Initialization), il est assez courant qu'un constructeur fasse un travail significatif; un constructeur bien écrit nettoiera à son tour s'il ne peut pas être complètement initialisé.


Il n'y a généralement rien à gagner en divorçant l'initialisation de l'objet de la construction. RAII est correct, un appel réussi au constructeur devrait soit aboutir à un objet live entièrement initialisé, soit échouer, et TOUS les échecs à n'importe quel point dans n'importe quel chemin de code devraient toujours lever une exception. Vous ne gagnez rien en utilisant une méthode init () séparée, sauf une complexité supplémentaire à un certain niveau. Le contrat de ctor devrait être soit qu'il retourne un objet valide fonctionnel, soit qu'il le nettoie après lui-même et le lance.

Considérons, si vous implémentez une méthode init séparée, vous devez toujours l' appeler. Il aura toujours le potentiel de lancer des exceptions, elles doivent toujours être manipulées et elles doivent pratiquement toujours être appelées immédiatement après le constructeur, sauf que maintenant vous avez 4 états d'objets possibles au lieu de 2 (IE, construit, initialisé, non initialisé, et échoué vs juste valide et inexistant).

En tout cas, j'ai couru dans 25 ans de cas de développement OO où il semble qu'une méthode d'init séparée permettrait de «résoudre certains problèmes» sont des défauts de conception. Si vous n'avez pas besoin d'un objet MAINTENANT alors vous ne devriez pas le construire maintenant, et si vous en avez besoin maintenant, alors vous en avez besoin initialisé. KISS devrait toujours être le principe suivi, avec le concept simple que le comportement, l'état, et l'API de n'importe quelle interface devraient refléter ce que l'objet fait, pas comment il le fait, le code client ne devrait même pas être conscient que l'objet de l'état interne qui nécessite une initialisation, ainsi le modèle init après viole ce principe.


Je ne peux pas parler de la meilleure pratique en Objective-C, mais en C ++ c'est bien pour un constructeur de lancer une exception. Surtout qu'il n'y a pas d'autre moyen de s'assurer qu'une condition exceptionnelle rencontrée lors de la construction est signalée sans avoir recours à l'invocation d'une méthode isOK ().

La fonctionnalité fonction try block a été conçue spécifiquement pour prendre en charge les échecs dans l'initialisation des membres du constructeur (bien qu'elle puisse également être utilisée pour les fonctions régulières). C'est le seul moyen de modifier ou d'enrichir l'information d'exception qui sera lancée. Mais en raison de son objectif de conception d'origine (utilisation dans les constructeurs), il ne permet pas que l'exception soit avalée par une clause catch () vide.


Je ne suis pas sûr que n'importe quelle réponse puisse être entièrement agnostique. Certaines langues gèrent différemment les exceptions et la gestion de la mémoire.

J'ai déjà travaillé dans le cadre de normes de codage exigeant que des exceptions ne soient jamais utilisées et seulement des codes d'erreur sur les initialiseurs, parce que les développeurs avaient été brûlés par le langage en manipulant mal les exceptions. Les langages sans récupération de place géreront le tas et la pile de manière très différente, ce qui peut être important pour les objets non RAII. Il est important cependant qu'une équipe décide d'être cohérente afin qu'ils sachent par défaut s'ils doivent appeler des initialiseurs après les constructeurs. Toutes les méthodes (y compris les constructeurs) devraient également être bien documentées quant aux exceptions qu'ils peuvent lancer, afin que les appelants sachent comment les gérer.

Je suis généralement en faveur d'une construction en une seule étape, car il est facile d'oublier d'initialiser un objet, mais il y a beaucoup d'exceptions à cela.

  • Le support de votre langue pour les exceptions n'est pas très bon.
  • Vous avez une raison de conception pressante pour toujours utiliser new et delete
  • Votre initialisation nécessite beaucoup de temps processeur et doit être exécutée asynchrone avec le thread qui a créé l'objet.
  • Vous créez une DLL qui peut lancer des exceptions en dehors de son interface vers une application utilisant une langue différente. Dans ce cas, il ne s'agit pas forcément de lancer des exceptions, mais de s'assurer qu'elles sont interceptées avant l'interface publique. (Vous pouvez attraper des exceptions C ++ en C #, mais il y a des cerceaux à passer.)
  • Constructeurs statiques (C #)

Lancez une exception si vous ne parvenez pas à initialiser l'objet dans le constructeur, par exemple des arguments illégaux.

En règle générale, une exception doit toujours être levée dès que possible, car elle facilite le débogage lorsque la source du problème est plus proche de la méthode, signalant que quelque chose ne va pas.


Le contrat habituel dans OO est que les méthodes objet fonctionnent réellement.

Donc, en tant que corrolaire, ne jamais retourner un objet zombie forme un constructeur / init.

Un zombie n'est pas fonctionnel et il peut manquer des composants internes. Juste une exception null-pointeur qui attend de se produire.

J'ai d'abord fait des zombies en Objective C, il y a plusieurs années.

Comme toutes les règles générales, il y a une "exception".

Il est tout à fait possible qu'une interface spécifique puisse avoir un contrat qui dit qu'il existe une méthode "initialize" qui est autorisée à déclencher une exception. Il se peut qu'un objet implémentant cette interface ne réponde pas correctement à tous les appels sauf les paramètres de propriété jusqu'à ce que initialize ait été appelée. J'ai utilisé cela pour les pilotes de périphériques dans un système d'exploitation OO au cours du processus de démarrage, et c'était faisable.

En général, vous ne voulez pas d'objets zombies. Dans des langages comme Smalltalk, les choses deviennent un peu pétillantes, mais l'abus de devenir est aussi un mauvais style. Devenir permet à un objet de se transformer en un autre objet in-situ, il n'y a donc pas besoin de wrapper d'enveloppe (Advanced C ++) ou de pattern de stratégie (GOF).


Le travail du constructeur consiste à amener l'objet dans un état utilisable. Il y a essentiellement deux écoles de pensée à ce sujet.

Un groupe favorise la construction en deux étapes. Le constructeur ne fait que mettre l'objet dans un état dormant dans lequel il refuse de travailler. Il y a une fonction supplémentaire qui fait l'initialisation réelle.

Je n'ai jamais compris le raisonnement derrière cette approche. Je suis fermement dans le groupe qui supporte la construction en une étape, où l'objet est entièrement initialisé et utilisable après la construction.

Les constructeurs en une étape doivent lancer s'ils ne parviennent pas à initialiser complètement l'objet. Si l'objet ne peut pas être initialisé, il ne doit pas être autorisé à exister, donc le constructeur doit lancer.


Les agents ne sont pas censés faire des choses «intelligentes», de sorte qu'il n'est pas nécessaire de lancer une exception de toute façon. Utilisez une méthode Init () ou Setup () si vous souhaitez effectuer une configuration d'objet plus complexe.


Oui, si le constructeur ne parvient pas à construire une de ses parties internes, il peut être - par choix - de sa responsabilité de lancer (et dans certaines langues de déclarer) une exception explicite , dûment notée dans la documentation du constructeur.

Ce n'est pas la seule option: Il pourrait finir le constructeur et construire un objet, mais avec une méthode 'isCoherent ()' retournant false, afin de pouvoir signaler un état incohérent (qui peut être préférable dans certains cas, pour pour éviter une interruption brutale du workflow d'exécution due à une exception)
Attention: comme le dit EricSchaefer dans son commentaire, cela peut apporter une certaine complexité au test unitaire (un lancer peut augmenter la complexité cyclomatique de la fonction en raison de la condition qui la déclenche)

Si cela échoue à cause de l'appelant (comme un argument nul fourni par l'appelant, où le constructeur appelé attend un argument non nul), le constructeur lancera quand même une exception d'exécution non vérifiée.


Parlant strictement d'un point de vue Java, à chaque fois que vous initialisez un constructeur avec des valeurs illégales, il devrait lancer une exception. De cette façon, il n'est pas construit dans un mauvais état.


Si vous écrivez Contrôles UI (ASPX, WinForms, WPF, ...) vous devriez éviter de jeter des exceptions dans le constructeur car le concepteur (Visual Studio) ne peut pas les gérer lorsqu'il crée vos contrôles. Connaissez votre cycle de vie de contrôle (événements de contrôle) et utilisez l'initialisation paresseuse autant que possible.


Si vous utilisez des fabriques ou des méthodes d'usine pour la création de tous les objets, vous pouvez éviter les objets non valides sans lancer d'exceptions aux constructeurs. La méthode de création devrait retourner l'objet demandé s'il est capable de créer un, ou null si ce n'est pas le cas. Vous perdez un peu de flexibilité dans la gestion des erreurs de construction dans l'utilisateur d'une classe, car le fait de retourner null ne vous dit pas ce qui s'est mal passé dans la création de l'objet. Mais cela évite également d'ajouter la complexité de plusieurs gestionnaires d'exceptions chaque fois que vous demandez un objet, et le risque d'attraper des exceptions que vous ne devriez pas gérer.


Voir les sections 17.2 et 17.4 FAQ C ++.

En général, j'ai trouvé que le code est plus facile à porter et maintenir les résultats si les constructeurs sont écrits pour qu'ils n'échouent pas, et le code qui échoue est placé dans une méthode séparée qui retourne un code d'erreur et laisse l'objet dans un état inerte .


Vous devriez absolument jeter une exception d'un constructeur si vous êtes incapable de créer un objet valide. Cela vous permet de fournir des invariants appropriés dans votre classe.

En pratique, vous devrez peut-être faire très attention. Rappelez-vous qu'en C ++, le destructeur ne sera pas appelé, donc si vous lancez après avoir alloué vos ressources, vous devez prendre soin de bien gérer cela!

Cette page a une discussion approfondie de la situation en C ++.







constructor