Style de codage OOP: tout initialiser sur le constructeur?


14

Je me considère toujours comme un apprenti programmeur, donc je cherche toujours à apprendre une "meilleure" façon de programmer en général. Aujourd'hui, mon collègue a soutenu que mon style de codage fait un travail inutile et je veux entendre les opinions des autres. En règle générale, lorsque je conçois une classe en langage OOP (généralement C ++ ou Python), je sépare l'initialisation en deux parties différentes:

class MyClass1 {
public:
    Myclass1(type1 arg1, type2 arg2, type3 arg3);
    initMyClass1();
private:
    type1 param1;
    type2 param2;
    type3 param3;
    type4 anotherParam1;
};

// Only the direct assignments from the input arguments are done in the constructor
MyClass1::myClass1(type1 arg1, type2 arg2, type3 arg3)
    : param1(arg1)
    , param2(arg2)
    , param3(arg3)
    {}

// Any other procedure is done in a separate initialization function 
MyClass1::initMyClass1() {
    // Validate input arguments before calculations
    if (checkInputs()) {
    // Do some calculations here to figure out the value of anotherParam1
        anotherParam1 = someCalculation();
    } else {
        printf("Something went wrong!\n");
        ASSERT(FALSE)
    }
}

(ou équivalent python)

class MyClass1:

    def __init__(self, arg1, arg2, arg3):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3
        #optional
        self.anotherParam1 = None

    def initMyClass1():
        if checkInputs():
            anotherParam1 = someCalculation()
        else:
            raise "Something went wrong!"

Quelle est votre opinion sur cette approche? Dois-je m'abstenir de fractionner le processus d'initialisation? La question n'est pas seulement limitée à C ++ et Python, et les réponses pour d'autres langages sont également appréciées.





Pourquoi le faites-vous généralement? Habitude? Vous a-t-on déjà donné une raison de le faire?
JeffO

@JeffO J'ai pris cette habitude lorsque je travaillais pour faire une interface graphique avec la bibliothèque MFC. La plupart des classes liées à l'interface utilisateur, telles que CApp, CWindow, CDlg, etc., ont des fonctions OnInit () avec lesquelles vous pouvez remplacer, qui répondent à leurs messages de réponse.
Caladbolgll

Réponses:


28

Bien que cela soit parfois problématique, l'initialisation de tout dans le constructeur présente de nombreux avantages:

  1. S'il y a une erreur, elle se produit le plus rapidement possible et est la plus facile à diagnostiquer. Par exemple, si null est une valeur d'argument non valide, testez et échouez dans le constructeur.
  2. L'objet est toujours dans un état valide. Un collègue ne peut pas se tromper et oublier d'appeler initMyClass1()car il n'est pas là . "Les composants les moins chers, les plus rapides et les plus fiables sont ceux qui ne sont pas là."
  3. Si cela a du sens, l'objet peut être rendu immuable, ce qui présente de nombreux avantages.

2

Pensez à l'abstraction que vous fournissez à vos utilisateurs.

Pourquoi scinder quelque chose qui pourrait être fait en un seul coup en deux?

L'initialisation supplémentaire est juste quelque chose de supplémentaire pour les programmeurs qui utilisent votre API pour se souvenir, et fournit plus pour se tromper s'ils ne le font pas correctement, mais pour quelle valeur pour eux pour ce fardeau supplémentaire?

Vous voulez fournir des abstractions simples, faciles à utiliser et difficiles à utiliser. La programmation est assez difficile sans choses gratuites à retenir / cerceaux à franchir. Vous voulez que vos utilisateurs d'API (même si ce n'est que vous qui utilisez votre propre API) tombent dans le gouffre du succès .


1

Initialisez tout sauf la zone Big Data. Les outils d'analyse statique signaleront les champs non initialisés dans le constructeur. Cependant, le moyen le plus productif / sûr est d'avoir toutes les variables membres avec des constructeurs par défaut et d'initialiser explicitement uniquement celles qui nécessitent une initialisation non par défaut.


0

Il y a des cas où l'objet a beaucoup d'initialisation qui peut être divisé en deux catégories:

  1. Les attributs qui sont immuables ou n'ont pas besoin d'être réinitialisés.

  2. Attributs qui pourraient avoir besoin de revenir aux valeurs d'origine (ou valeurs modélisées) en fonction d'une condition après avoir accompli leur travail, une sorte de réinitialisation logicielle. par exemple des connexions dans un pool de connexions.

Ici, la deuxième partie de l'initialisation étant conservée dans une fonction séparée, par exemple InitialiseObject (), peut être appelée dans le ctor.

La même fonction peut être appelée ultérieurement si une réinitialisation logicielle est requise, sans avoir à supprimer et recréer l'objet.


0

Comme d'autres l'ont dit, c'est généralement une bonne idée d'initialiser dans le constructeur.

Il existe cependant des raisons pour lesquelles cela peut ne pas s'appliquer dans certains cas.

La gestion des erreurs

Dans de nombreux langages, la seule façon de signaler une erreur dans un constructeur est de lever une exception.

Si votre initialisation a une chance raisonnable de déclencher une erreur, par exemple si elle implique des entrées-sorties ou si ses paramètres peuvent être saisis par l'utilisateur, alors le seul mécanisme qui vous est ouvert est de lever une exception. Dans certains cas, cela peut ne pas être ce que vous voulez et il peut être plus judicieux de séparer le code sujet aux erreurs en une fonction d'initialisation distincte.

L'exemple le plus courant est probablement en C ++ si la norme de projet / organisation est de désactiver les exceptions.

Machine d'état

C'est le cas lorsque vous modélisez un objet qui a des transitions d'état explicites. Par exemple, un fichier ou un socket qui peut être ouvert et fermé.

Dans ce cas, il est courant que la construction (et la suppression) d'objet ne traite que des attributs orientés mémoire (nom de fichier, port, etc.). Il y aura ensuite des fonctions pour gérer spécifiquement les transitions d'état, par exemple ouvrir, fermer, qui sont effectivement des fonctions d'initialisation et de suppression.

Les avantages résident dans la gestion des erreurs, comme ci-dessus, mais il peut également être judicieux de séparer la construction de l'initialisation (par exemple, vous créez un vecteur de fichiers et vous les ouvrez de manière asynchrone).

L'inconvénient, comme d'autres l'ont dit, est que vous imposez désormais la charge de la gestion de l'état à l'utilisateur de vos classes. Si vous pouviez gérer avec juste la construction, vous pourriez, par exemple, utiliser RAII pour le faire automatiquement.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.