Les méthodes init () sont-elles une odeur de code?

20

Y a-t-il un but pour déclarer une init()méthode pour un type?

Je ne demande pas si nous devrions préférer init()un constructeur ou comment éviter de déclarerinit() .

Je demande s'il y a une raison derrière la déclaration d'une init()méthode (voir à quel point elle est courante) ou si c'est une odeur de code et devrait être évitée.


L' init()idiome est assez courant, mais je n'ai encore vu aucun avantage réel.

Je parle de types qui encouragent l'initialisation via une méthode:

class Demo {
    public void init() {
        //...
    }
}

Quand cela sera-t-il utile dans le code de production?


Je pense que cela peut être une odeur de code car cela suggère que le constructeur n'initialise pas complètement l'objet, résultant en un objet partiellement créé. L'objet ne devrait pas exister si son état n'est pas défini.

Cela me fait croire que cela peut faire partie d'une sorte de technique utilisée pour accélérer la production, au sens des applications d'entreprise. C'est la seule raison logique pour laquelle je peux penser à avoir un tel idiome, je ne suis tout simplement pas sûr de savoir comment cela serait bénéfique dans ce cas.

Vince Emigh
la source
1
"... voir à quel point c'est commun ...": est-ce courant? Pouvez-vous donner quelques exemples? Vous avez peut-être affaire à un cadre qui nécessite de séparer l'initialisation et la construction.
VENIR DU
La méthode se trouve-t-elle sur une classe de base ou une classe dérivée, ou les deux? (Ou: la méthode se trouve-t-elle sur une classe qui appartient à une hiérarchie d'héritage? La classe de base appelle-t-elle la classe init()dérivée, ou vice versa?) Si c'est le cas, c'est un exemple de laisser la classe de base exécuter un "post-constructeur "qui ne peut être exécuté qu'après la fin de la construction de la classe la plus dérivée. Il s'agit d'un exemple d'initialisation multiphase.
rwong
Si vous ne voulez pas initialiser au point d'instanciation, il serait logique de séparer les deux.
JᴀʏMᴇᴇ
cela pourrait vous intéresser. est-un-start-run-or-execute-method-a-good-practice
Laiv

Réponses:

39

Oui, c'est une odeur de code. Une odeur de code n'est pas quelque chose qui doit nécessairement toujours être supprimée. C'est quelque chose qui vous fait jeter un second coup d'œil.

Ici, vous avez un objet dans deux états fondamentalement différents: pré-init et post-init. Ces États ont des responsabilités différentes, des méthodes différentes qui peuvent être appelées et des comportements différents. Il s'agit en fait de deux classes différentes.

Si vous en faites physiquement deux classes distinctes, vous supprimerez statiquement toute une classe de bugs potentiels, au prix de rendre votre modèle non plus identique au "modèle du monde réel". Vous nommez généralement le premier Configou Setupquelque chose comme ça.

La prochaine fois, essayez de refactoriser vos idiomes construct-init en modèles à deux classes et voyez comment cela se passe pour vous.

Karl Bielefeldt
la source
6
Votre suggestion d'essayer le modèle à deux classes est bonne. Il est utile de proposer une étape concrète pour traiter une odeur de code.
Ivan
1
C'est la meilleure réponse jusqu'à présent. Une chose me dérange cependant: " Ces États ont des responsabilités différentes, des méthodes différentes qui peuvent être appelées et des comportements différents " - Ne pas séparer ces responsabilités viole la SRP. Si le but du logiciel était de reproduire un scénario du monde réel dans tous les aspects, cela aurait du sens. Mais en production, les développeurs écrivent principalement du code qui encourage une gestion facile, révisant le modèle si nécessaire pour mieux s'adapter à un environnement basé sur un logiciel (suite au commentaire suivant)
Vince Emigh
1
Le monde réel est un environnement différent. Les programmes qui tentent de le reproduire semblent spécifiques au domaine, car vous ne devriez / ne devriez pas en tenir compte dans la plupart des projets. Beaucoup de choses qui sont désapprouvées sont acceptées en ce qui concerne les projets spécifiques au domaine, donc j'essaie de garder cela aussi général que possible (comme la façon dont l'appel thisau constructeur est une odeur de code et pourrait entraîner un code sujet aux erreurs, et il est recommandé de l’éviter quel que soit le domaine auquel appartient votre projet).
Vince Emigh
14

Ça dépend.

Une initméthode est une odeur de code lorsqu'il n'est pas nécessaire de séparer l'initialisation de l'objet du constructeur. Il y a parfois des cas où il est logique de séparer ces étapes.

Une recherche rapide sur Google m'a donné cet exemple. Je peux facilement imaginer plus de cas où le code exécuté pendant l'allocation d'objet (le constructeur) pourrait mieux être séparé de l'initialisation elle-même. Peut-être que vous avez un système nivelé, et l'allocation / construction a lieu au niveau X, mais l'initialisation uniquement au niveau Y, car seul Y peut fournir les paramètres nécessaires. Peut-être que le "init" est coûteux et doit être exécuté uniquement pour un sous-ensemble des objets alloués, et la détermination de ce sous-ensemble ne peut être effectuée qu'au niveau Y. Ou vous souhaitez remplacer la méthode (virtuelle) "init" dans un dérivé classe qui ne peut pas être faite avec un constructeur. Peut-être que le niveau X vous fournit des objets alloués à partir d'un arbre d'héritage, mais le niveau Y n'est pas au courant de la dérivation concrète, seulement de l'interface commune (oùinit peut-être défini).

Bien sûr, d'après mon expérience, ces cas ne sont qu'un petit pourcentage du cas standard où toute l'initialisation peut être effectuée directement dans le constructeur, et chaque fois que vous voyez une initméthode distincte , ce pourrait être une bonne idée de remettre en question sa nécessité.

Doc Brown
la source
2
La réponse dans ce lien indique qu'il est utile pour réduire la quantité de travail dans le constructeur, mais il encourage les objets partiellement créés. Les petits constructeurs peuvent être obtenus par décomposition, donc il me semble que les réponses créent un nouveau problème (le potentiel d'oublier d'appeler toutes les méthodes d'initialisation requises, laissant l'objet sujet aux erreurs) et tombe dans la catégorie d'odeur de code
Vince Emigh
@VinceEmigh: ok, c'était le premier exemple que je pouvais trouver ici sur la plate-forme SE, peut-être pas le meilleur, mais il existe des cas d'utilisation légitimes pour une initméthode distincte . Cependant, chaque fois que vous voyez une telle méthode, n'hésitez pas à remettre en question sa nécessité.
Doc Brown
Je remets en question / conteste chaque cas d'utilisation, car je pense qu'il n'y a aucune situation où ce serait une nécessité. Pour moi, c'est un mauvais timing de création d'objet, et cela devrait être évité car c'est un candidat pour des erreurs qui peuvent être évitées grâce à une conception appropriée. S'il y avait une bonne utilisation d'une init()méthode, je suis sûr que je gagnerais à en apprendre davantage sur son objectif. Excusez mon ignorance, je suis juste étonné de voir à quel point j'ai du mal à trouver une utilisation, m'empêchant de considérer cela comme quelque chose à éviter
Vince Emigh
1
@VinceEmigh: quand vous ne pouvez pas penser à une telle situation, vous devez travailler votre imagination ;-). Ou relisez ma réponse, ne la réduisez pas seulement à "allocation". Ou travaillez avec plus de frameworks de différents fournisseurs.
Doc Brown
1
@DocBrown Utiliser l'imagination plutôt que des pratiques éprouvées conduit à un code génial, comme l'initialisation à double accolade: intelligent, mais inefficace et doit être évité. Je sais que les fournisseurs l'utilisent, mais cela ne signifie pas que l'utilisation est justifiée. Si vous le sentez, faites-le-moi savoir pourquoi, car c'est ce que j'ai essayé de comprendre. Vous sembliez déterminé à avoir un objectif bénéfique, mais il semble que vous ayez du mal à donner un exemple qui encourage une bonne conception. Je sais dans quelles circonstances il pourrait être utilisé, mais devrait- il être utilisé?
Vince Emigh
5

Mon expérience se décompose en deux groupes:

  1. Code où init () est réellement requis. Cela peut se produire lorsqu'une superclasse ou un framework empêche le constructeur de votre classe d'obtenir toutes ses dépendances pendant la construction.
  2. Code où init () est utilisé mais aurait pu être évité.

Dans mon expérience personnelle, je n'ai vu que quelques exemples de (1) mais de nombreux autres exemples de (2). Par conséquent, je suppose généralement qu'un init () est une odeur de code, mais ce n'est pas toujours le cas. Parfois, vous ne pouvez tout simplement pas vous en sortir.

J'ai trouvé que l'utilisation du modèle de générateur peut souvent aider à supprimer le besoin / désir d'avoir un init ().

Ivan
la source
1
Si une superclasse ou un framework ne permet pas à un type d'obtenir les dépendances dont il a besoin via le constructeur, comment l'ajout d'une init()méthode le résoudrait-il? La init()méthode aurait besoin de paramètres pour accepter les dépendances, ou vous devriez instancier les dépendances dans la init()méthode, ce que vous pourriez également faire avec un constructeur. Pouvez-vous donner un exemple?
Vince Emigh
1
@VinceEmigh: init () peut parfois être utilisé pour charger un fichier de configuration à partir d'une source externe, ouvrir une connexion à une base de données ou quelque chose de ce genre. La méthode DoFn.initialize () (du framework Apache Crunch) est utilisée de cette manière. Il peut également être utilisé pour charger des champs internes non sérialisables (les DoFns doivent être sérialisables). Les deux problèmes ici sont que (1) quelque chose doit garantir que la méthode d'initialisation est appelée et (2) l'objet doit savoir où il va obtenir (ou comment il va construire) ces ressources.
Ivan
1

Un scénario typique lorsqu'une méthode Init est utile est lorsque vous avez un fichier de configuration que vous souhaitez modifier et que la modification est prise en compte sans redémarrer l'application. Bien entendu, cela ne signifie pas qu'une méthode Init doit être appelée séparément d'un constructeur. Vous pouvez appeler une méthode Init à partir d'un constructeur, puis l'appeler plus tard lorsque / si les paramètres de configuration changent.

Pour résumer: comme pour la plupart des dilemmes, que ce soit une odeur de code ou non, cela dépend de la situation et des circonstances.

Vladimir Stokic
la source
Si les mises à jour de configuration, et cela nécessite l'objet pour réinitialiser / changer d' état de ce en fonction de la configuration, vous ne pensez pas qu'il serait préférable d'avoir l'acte d'objet comme un observateur vers Config?
Vince Emigh
@Vince Emigh Pas forcément. Observer fonctionnerait si je connais le moment exact où la configuration change. Cependant, si les données de configuration sont conservées dans un fichier qui peut être modifié en dehors d'une application, alors il n'y a pas d'approche vraiment ellegant. Par exemple, si j'ai un programme qui analyse certains fichiers et transforme les données en un modèle interne, et qu'un fichier de configuration séparé contient des valeurs par défaut pour les données manquantes, si je modifie les valeurs par défaut, je les relirai lors de la prochaine exécution l'analyse. Avoir une méthode Init dans mon application serait très pratique dans ce cas.
Vladimir Stokic
Si le fichier de configuration est modifié en externe lors de l'exécution, il n'y a aucun moyen de recharger ces variables sans une sorte de notification informant votre application qu'elle doit appeler init ( update/ reloadserait probablement plus descriptif pour ce type de comportement) pour réellement enregistrer ces changements . Dans ce cas, cette notification entraînerait un changement en interne des valeurs de la configuration dans votre application, ce qui, je crois, pourrait être observé en faisant en sorte que votre configuration soit observable, informant les observateurs lorsque la configuration est invitée à modifier l'une de ses valeurs. Ou est-ce que je comprends mal votre exemple?
Vince Emigh
Une application prend des fichiers externes (exportés à partir d'un SIG ou d'un autre système), analyse ces fichiers et les transforme en un modèle interne que les systèmes dont l'application fait partie utilisent. Au cours de ce processus, certaines lacunes de données peuvent être trouvées et ces lacunes de données peuvent être comblées en définissant les valeurs par défaut. Ces valeurs par défaut peuvent être stockées dans un fichier de configuration qui peut être lu au début du processus de transformation, qui est appelé chaque fois qu'il y a de nouveaux fichiers à transformer. C'est là qu'une méthode Init peut être utile.
Vladimir Stokic
1

Cela dépend de la façon dont vous les utilisez.

J'utilise ce modèle dans des langages récupérés comme Java / C # lorsque je ne veux pas continuer à réallouer un objet sur le tas (comme lorsque je crée un jeu vidéo et que je dois maintenir des performances élevées, les récupérateurs tuent les performances). J'utilise le constructeur pour faire d'autres allocations de tas dont il a besoin et initpour créer l'état utile de base juste avant chaque fois que je veux le réutiliser. Ceci est lié au concept de pools d'objets.

Il est également utile si vous avez plusieurs constructeurs qui partagent un sous-ensemble commun d'instructions d'initialisation, mais dans ce cas, initils seront privés. De cette façon, je peux minimiser chaque constructeur autant que possible, donc chacun ne contient que ses instructions uniques et un seul appel initpour faire le reste.

En général cependant, c'est une odeur de code.

Cody
la source
Une reset()méthode ne serait-elle pas plus descriptive pour votre première déclaration? Quant au second (plusieurs constructeurs), avoir plusieurs constructeurs est une odeur de code. Il suppose que l'objet a plusieurs objectifs / responsabilités, suggérant une violation de SRP. Un objet doit avoir une seule responsabilité et le constructeur doit définir les dépendances requises pour cette seule responsabilité. Si vous avez plusieurs constructeurs en raison de valeurs facultatives, ils devraient se télescoper (ce qui est également une odeur de code, devrait plutôt utiliser un générateur).
Vince Emigh
@VinceEmigh vous pouvez utiliser init ou reset, ce n'est qu'un nom après tout. Init a plus de sens dans le contexte, j'ai tendance à l'utiliser car cela n'a pas de sens de réinitialiser un objet qui n'a jamais été défini pour la première fois que je l'utilise. Quant au problème des constructeurs, j'essaye d'éviter d'avoir beaucoup de constructeurs, mais parfois c'est utile. Regardez la stringliste des constructeurs de n'importe quelle langue , des tonnes d'options. Pour moi, il s'agit généralement de 3 constructeurs maximum, mais le sous-ensemble commun d'instructions en tant qu'initialisation a du sens quand elles partagent un code mais diffèrent de quelque manière que ce soit.
Cody
Les noms sont extrêmement importants. Ils décrivent le comportement en cours sans obliger le client à lire le contrat. Un mauvais nom peut conduire à de fausses hypothèses. Quant aux constructeurs multiples dans String, cela pourrait être résolu en découplant la création de chaînes. En fin de compte, a Stringest un String, et son constructeur ne doit accepter que ce qui est nécessaire pour qu'il fonctionne selon les besoins. La plupart de ces constructeurs sont exposés à des fins de conversion, ce qui constitue une mauvaise utilisation des constructeurs. Les constructeurs ne doivent pas exécuter la logique, sinon ils risquent d'échouer l'initialisation, vous laissant avec un objet inutile.
Vince Emigh
Le JDK est rempli de designs horribles, je pourrais en énumérer environ 10 du haut de ma tête. La conception de logiciels a évolué depuis que les principaux aspects de nombreux langages ont été exposés publiquement, et ils persistent en raison de la possibilité de casser le code s'il devait être repensé pour les temps modernes.
Vince Emigh
1

init()Les méthodes peuvent avoir un certain sens lorsque vous avez des objets qui nécessitent des ressources externes (comme, par exemple, une connexion réseau) qui sont utilisées simultanément par d'autres objets. Vous pourriez ne pas vouloir / avoir besoin de monopoliser la ressource pendant la durée de vie de l'objet. Dans de telles situations, vous pouvez ne pas vouloir allouer la ressource dans le constructeur lorsque l'allocation de ressource est susceptible d'échouer.

En particulier dans la programmation intégrée, vous voulez avoir une empreinte mémoire déterministe, il est donc courant (bon?) D'appeler vos constructeurs tôt, peut-être même statiques, et de ne s'initialiser que plus tard lorsque certaines conditions sont remplies.

À part de tels cas, je pense que tout devrait aller dans un constructeur.

tofro
la source
Si le type nécessite une connexion, vous devez utiliser DI. S'il y a un problème lors de la création de la connexion, vous ne devez pas créer l'objet qui l'exige. Si vous bousculez la création d'une connexion à l'intérieur de la classe, vous créerez un objet, cet objet instanciera ses dépendances (la connexion). Si l'instanciation des dépendances échoue, vous vous retrouvez avec un objet que vous ne pouvez pas utiliser, gaspillant ainsi des ressources.
Vince Emigh
Pas nécessairement. Vous vous retrouvez avec un objet que vous ne pouvez temporairement pas utiliser à toutes fins. Dans de tels cas, l'objet pourrait simplement agir comme une file d'attente ou un proxy jusqu'à ce que la ressource devienne disponible. Condamner entièrement les initméthodes est trop restrictif, à mon humble avis.
1er
0

En général, je préfère un constructeur qui reçoit tous les arguments requis pour une instance fonctionnelle. Cela met en évidence toutes les dépendances de cet objet.

D'autre part, j'utilise un cadre de configuration simple qui nécessite un constructeur public sans paramètre et des interfaces pour injecter des dépendances et des valeurs de configuration. Après cela, le cadre de configuration appelle la initméthode de l'objet: maintenant vous avez reçu tout ce que j'ai pour vous, faites les dernières étapes pour vous préparer au travail. Mais attention: c'est le framework de configuration qui appelle automatiquement la méthode init, vous n'oublierez donc pas de l'appeler.

Bernhard Hiller
la source
0

Il n'y a pas d'odeur de code si la méthode init () - est sémantiquement intégrée dans le cycle de vie d'état de l'objet.

Si vous devez appeler init () pour mettre l'objet dans un état cohérent, c'est une odeur de code.

Il existe plusieurs raisons techniques pour lesquelles une telle structure existe:

  1. crochet de cadre
  2. réinitialiser l'objet à son état initial (éviter la redondance)
  3. possibilité de passer outre lors des tests
oopexpert
la source
1
Mais pourquoi un ingénieur logiciel l'intégrerait-il dans le cycle de vie? Y a-t-il un but justifiable (étant donné qu'il encourage les objets partiellement construits) et ne pourrait pas être abattu par une alternative plus efficace? Je pense que l'incorporer dans le cycle de vie serait une odeur de code, et qu'il devrait être remplacé par un meilleur timing de création d'objet (Pourquoi allouer de la mémoire à un objet si vous ne prévoyez pas de l'utiliser à ce moment-là? Pourquoi créer un objet qui est partiellement construit lorsque vous pouvez simplement attendre jusqu'à ce que vous ayez réellement besoin de l'objet avant de le créer?)
Vince Emigh
Le fait est que l'objet doit être utilisable AVANT d'appeler la méthode init. Peut-être d'une autre manière qu'après son appel. Voir modèle d'état. Dans le sens de construction partielle d'objet, c'est une odeur de code.
oopexpert
-4

Le nom init peut parfois être opaque. Prenons une voiture et un moteur. Pour démarrer la voiture (uniquement sous tension, pour écouter la radio), vous voulez vérifier que tous les systèmes sont prêts à fonctionner.

Vous construisez donc un moteur, une porte, une roue, etc. votre écran affiche le moteur = éteint.

Pas besoin de commencer à surveiller le moteur, etc. car ceux-ci sont tous chers. Ensuite, lorsque vous tournez la clé pour allumer, vous appelez moteur-> démarrer. Il commence à exécuter tous les processus coûteux.

Vous voyez maintenant engine = on. Et le processus d'allumage démarre.

La voiture ne démarre pas sans que le moteur soit disponible.

Vous pouvez remplacer le moteur par votre calcul complexe. Comme une cellule Excel. Toutes les cellules n'ont pas besoin d'être actives avec tous les gestionnaires d'événements tout le temps. Lorsque vous vous concentrez sur une cellule, vous pouvez la démarrer. Augmenter ainsi les performances.

Luc Franken
la source