Fonction constructeur vs fonctions d'usine

150

Quelqu'un peut-il clarifier la différence entre une fonction de constructeur et une fonction d'usine en Javascript.

Quand utiliser l'un au lieu de l'autre?

Sinan
la source

Réponses:

149

La différence fondamentale est qu'une fonction constructeur est utilisée avec le newmot - clé (ce qui oblige JavaScript à créer automatiquement un nouvel objet, à définir thisdans la fonction cet objet et à renvoyer l'objet):

var objFromConstructor = new ConstructorFunction();

Une fonction d'usine est appelée comme une fonction "régulière":

var objFromFactory = factoryFunction();

Mais pour qu'elle soit considérée comme une "fabrique", elle aurait besoin de retourner une nouvelle instance d'un objet: vous ne l'appelleriez pas une fonction "fabrique" si elle retournait simplement un booléen ou quelque chose. Cela ne se produit pas automatiquement comme avec new, mais cela permet une plus grande flexibilité dans certains cas.

Dans un exemple très simple, les fonctions référencées ci-dessus pourraient ressembler à ceci:

function ConstructorFunction() {
   this.someProp1 = "1";
   this.someProp2 = "2";
}
ConstructorFunction.prototype.someMethod = function() { /* whatever */ };

function factoryFunction() {
   var obj = {
      someProp1 : "1",
      someProp2 : "2",
      someMethod: function() { /* whatever */ }
   };
   // other code to manipulate obj in some way here
   return obj;
}

Bien sûr, vous pouvez rendre les fonctions d'usine beaucoup plus compliquées que ce simple exemple.

L'un des avantages des fonctions d'usine est que l'objet à renvoyer peut être de plusieurs types différents en fonction de certains paramètres.

nnnnnn
la source
14
"(EDIT: et cela peut être un problème car sans nouveau, la fonction fonctionnera toujours mais pas comme prévu)." Ce n'est un problème que si vous essayez d'appeler une fonction d'usine avec "new" ou si vous essayez d'utiliser le mot-clé "this" pour l'attribuer à l'instance. Sinon, vous créez simplement un nouvel objet arbitraire et vous le renvoyez. Pas de problème, juste une façon différente et plus flexible de faire les choses, avec moins de passe-partout et sans divulguer les détails d'instanciation dans l'API.
Eric Elliott
6
Je voulais souligner que les exemples pour les deux cas (fonction constructeur vs fonction usine) devraient être cohérents. L'exemple de la fonction d'usine n'inclut pas someMethodles objets retournés par l'usine, et c'est là que cela devient un peu brumeux. À l'intérieur de la fonction d'usine, si on le fait simplement var obj = { ... , someMethod: function() {}, ... }, cela conduirait à ce que chaque objet retourné contienne une copie différente someMethoddont nous pourrions ne pas vouloir. C'est là que l'utilisation newet prototypeà l'intérieur de la fonction d'usine aideraient.
Bharat Khatri
3
Comme vous l'avez déjà mentionné, certaines personnes essaient d'utiliser des fonctions d'usine simplement parce qu'elles n'ont pas l'intention de laisser des bogues là où les gens oublient d'utiliser newavec la fonction constructeur; Je pensais que c'était là où il faudrait peut-être voir un exemple de remplacement des constructeurs par des fonctions d'usine et c'est là que je pensais que la cohérence des exemples était requise. Quoi qu'il en soit, la réponse est suffisamment informative. C'était juste un point que je voulais soulever, non pas que je mette en cause la qualité de la réponse de quelque manière que ce soit.
Bharat Khatri
4
Pour moi, le plus grand avantage des fonctions Factory est que vous obtenez une meilleure encapsulation et un meilleur masquage des données, ce qui peut être utile dans certaines applications. S'il n'y a aucun problème à rendre publiques et facilement modifiables toutes les propriétés et méthodes d'instance par les utilisateurs, alors je suppose que la fonction Constructor est plus appropriée à moins que vous n'aimiez pas le mot clé "new" comme certaines personnes le font.
devius
1
@Federico - les méthodes de fabrique ne doivent pas renvoyer simplement un objet simple. Ils peuvent utiliser en newinterne, ou utiliser Object.create()pour créer un objet avec un prototype spécifique.
nnnnnn
110

Avantages de l'utilisation de constructeurs

  • La plupart des livres vous apprennent à utiliser les constructeurs et new

  • this fait référence au nouvel objet

  • Certaines personnes aiment la façon dont var myFoo = new Foo();lit.

Désavantages

  • Les détails de l'instanciation sont divulgués dans l'API appelante (via l' newexigence), de sorte que tous les appelants sont étroitement couplés à l'implémentation du constructeur. Si jamais vous avez besoin de la flexibilité supplémentaire de l'usine, vous devrez refactoriser tous les appelants (certes le cas exceptionnel, plutôt que la règle).

  • L'oubli newest un bogue si courant, vous devriez fortement envisager d'ajouter une vérification standard pour vous assurer que le constructeur est appelé correctement ( if (!(this instanceof Foo)) { return new Foo() }). EDIT: Depuis ES6 (ES2015), vous ne pouvez pas oublier newavec un classconstructeur, sinon le constructeur lancera une erreur.

  • Si vous effectuez la instanceofvérification, cela laisse une ambiguïté quant à savoir si newc'est nécessaire ou non . À mon avis, cela ne devrait pas être le cas. Vous avez effectivement court-circuité l' newexigence, ce qui signifie que vous pouvez supprimer l'inconvénient n ° 1. Mais alors, vous avez juste une fonction d'usine dans tout sauf le nom , avec un passe-partout supplémentaire, une lettre majuscule et un thiscontexte moins flexible .

Les constructeurs enfreignent le principe d'ouverture / fermeture

Mais ma principale préoccupation est qu'elle viole le principe ouvert / fermé. Vous commencez par exporter un constructeur, les utilisateurs commencent à utiliser le constructeur, puis vous réalisez que vous avez besoin de la flexibilité d'une fabrique, à la place (par exemple, pour basculer l'implémentation pour utiliser des pools d'objets, ou pour instancier entre les contextes d'exécution, ou pour ont plus de flexibilité d'héritage en utilisant le prototypage OO).

Vous êtes coincé, cependant. Vous ne pouvez pas effectuer la modification sans casser tout le code qui appelle votre constructeur avec new. Vous ne pouvez pas passer à l'utilisation de pools d'objets pour des gains de performances, par exemple.

De plus, l'utilisation de constructeurs vous donne un caractère trompeur instanceofqui ne fonctionne pas dans les contextes d'exécution et ne fonctionne pas si votre prototype de constructeur est remplacé. Cela échouera également si vous commencez à revenir thisde votre constructeur, puis que vous passez à l'exportation d'un objet arbitraire, ce que vous devrez faire pour activer le comportement de type usine dans votre constructeur.

Avantages de l'utilisation des usines

  • Moins de code - aucun passe-partout requis.

  • Vous pouvez renvoyer n'importe quel objet arbitraire et utiliser n'importe quel prototype arbitraire, ce qui vous donne plus de flexibilité pour créer différents types d'objets qui implémentent la même API. Par exemple, un lecteur multimédia qui peut créer des instances de lecteurs HTML5 et Flash, ou une bibliothèque d'événements qui peut émettre des événements DOM ou des événements de socket Web. Les usines peuvent également instancier des objets dans des contextes d'exécution, tirer parti des pools d'objets et permettre des modèles d'héritage prototypique plus flexibles.

  • Vous n'auriez jamais besoin de passer d'une usine à un constructeur, donc la refactorisation ne sera jamais un problème.

  • Aucune ambiguïté sur l'utilisation new. Ne fais pas ça. (Cela va faire thismal se comporter, voir point suivant).

  • thisse comporte normalement - vous pouvez donc l'utiliser pour accéder à l'objet parent (par exemple, à l'intérieur player.create(), thisfait référence à player, comme n'importe quel autre appel de méthode le ferait. callet applyégalement réassigner this, comme prévu. Si vous stockez des prototypes sur l'objet parent, cela peut être un excellent moyen de permuter dynamiquement les fonctionnalités et d'activer un polymorphisme très flexible pour votre instanciation d'objet.

  • Aucune ambiguïté sur l'opportunité de capitaliser ou non. Ne fais pas ça. Les outils Lint se plaindront, puis vous serez tenté d'essayer de les utiliser new, puis vous annulerez l'avantage décrit ci-dessus.

  • Certaines personnes aiment la manière var myFoo = foo();ou var myFoo = foo.create();lit.

Désavantages

  • newne se comporte pas comme prévu (voir ci-dessus). Solution: ne l'utilisez pas.

  • thisne fait pas référence au nouvel objet (à la place, si le constructeur est invoqué avec une notation par points ou une notation entre crochets, par exemple foo.bar () - thisfait référence à foo- comme toute autre méthode JavaScript - voir les avantages).

Eric Elliott
la source
2
Dans quel sens voulez-vous dire que les constructeurs rendent les appelants étroitement liés à leur implémentation? En ce qui concerne les arguments du constructeur, ceux-ci devront être passés même à la fonction de fabrique pour qu'elle puisse les utiliser et appeler le constructeur approprié à l'intérieur.
Bharat Khatri
4
Concernant la violation d'Open / Closed: n'est-ce pas uniquement une question d'injection de dépendances? Si A a besoin de B, que A appelle new B () ou A appelle BFactory.create (), les deux introduisent le couplage. Si par contre vous donnez à A une instance de B dans la racine de la composition, A n'a pas besoin de savoir du tout comment B est instancié. Je pense que les constructeurs et les usines ont leur utilité; les constructeurs sont pour une instanciation simple, les usines pour une instanciation plus complexe. Mais dans les deux cas, injecter vos dépendances est judicieux.
Stefan Billiet
1
DI est bon pour l'injection d'état: configuration, objets de domaine, etc. C'est exagéré pour tout le reste.
Eric Elliott
1
Le problème est que le fait d'exiger newviole le principe ouvert / fermé. Voir medium.com/javascript-scene/ ... pour une discussion beaucoup plus large que ne le permettent ces commentaires.
Eric Elliott
3
Étant donné que n'importe quelle fonction peut renvoyer un nouvel objet en JavaScript, et que bon nombre d'entre elles le font sans le newmot - clé, je ne pense pas que le newmot - clé offre réellement une lisibilité supplémentaire. OMI, il semble idiot de sauter à travers des cerceaux afin de permettre aux appelants de taper plus.
Eric Elliott
39

Un constructeur renvoie une instance de la classe sur laquelle vous l'appelez. Une fonction d'usine peut tout renvoyer. Vous utiliseriez une fonction d'usine lorsque vous avez besoin de renvoyer des valeurs arbitraires ou lorsqu'une classe a un processus de configuration important.

Ignacio Vazquez-Abrams
la source
6

Un exemple de fonction constructeur

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");
  • newcrée un objet prototypé sur User.prototypeet appelle Useravec l'objet créé comme thisvaleur.

  • new traite une expression d'argument pour son opérande comme facultative:

         let user = new User;

    provoquerait un newappel Usersans arguments.

  • newrenvoie l'objet qu'il a créé, sauf si le constructeur renvoie une valeur d'objet , qui est renvoyée à la place. Il s'agit d'un cas de bord qui, pour la plupart, peut être ignoré.

Avantages et inconvénients

Les objets créés par des fonctions de constructeur héritent des propriétés de la propriété du constructeur prototypeet retournent true à l'aide de l' instanceOfopérateur de la fonction de constructeur.

Les comportements ci-dessus peuvent échouer si vous modifiez dynamiquement la valeur de la prototypepropriété du constructeur après avoir déjà utilisé le constructeur. Cela est rare et ne peut pas être modifié si le constructeur a été créé à l'aide du classmot - clé.

Les fonctions du constructeur peuvent être étendues à l'aide du extendsmot - clé.

Les fonctions constructeur ne peuvent pas être renvoyées en nulltant que valeur d'erreur. Puisqu'il ne s'agit pas d'un type de données objet, il est ignoré par new.

Un exemple de fonction d'usine

function User(name, age) {
  return {
    name,
    age,
  }
};

let user = User("Tom", 23);

Ici, la fonction d'usine est appelée sans new. La fonction est entièrement responsable de l'utilisation directe ou indirecte de ses arguments et du type d'objet qu'elle renvoie. Dans cet exemple, il renvoie un simple [Object object] avec certaines propriétés définies à partir des arguments.

Avantages et inconvénients

Cache facilement les complexités d'implémentation de la création d'objet à l'appelant. Ceci est particulièrement utile pour les fonctions de code natif dans un navigateur.

La fonction d'usine n'a pas toujours besoin de renvoyer des objets du même type, et pourrait même revenir en nulltant qu'indicateur d'erreur.

Dans des cas simples, les fonctions d'usine peuvent être simples dans leur structure et leur signification.

Les objets retournés n'héritent généralement pas de la prototypepropriété de la fonction de fabrique et retournent à falsepartir de instanceOf factoryFunction.

La fonction de fabrique ne peut pas être étendue en toute sécurité à l'aide du extendsmot - clé car les objets étendus hériteraient de la prototypepropriété des fonctions de fabrique au lieu de la prototypepropriété du constructeur utilisée par la fonction de fabrique.

traktor53
la source
1
C'est une réponse tardive postée en réponse à cette question sur le même sujet,
traktor53
non seulement "null", mais le "nouveau" ignorera également tout type de données prémitif renvoyé par la fonction constructeur.
Vishal le
2

Les usines sont «toujours» meilleures. Lorsque vous utilisez des langages orientés objet,

  1. décider du contrat (les méthodes et ce qu'ils feront)
  2. Créez des interfaces qui exposent ces méthodes (en javascript, vous n'avez pas d'interfaces, vous devez donc trouver un moyen de vérifier l'implémentation)
  3. Créez une fabrique qui renvoie une implémentation de chaque interface requise.

Les implémentations (les objets réels créés avec new) ne sont pas exposées à l'utilisateur / consommateur d'usine. Cela signifie que le développeur d'usine peut s'étendre et créer de nouvelles implémentations tant qu'il / elle ne rompt pas le contrat ... et cela permet au consommateur d'usine de simplement bénéficier de la nouvelle API sans avoir à changer son code ... s'ils ont utilisé une nouvelle implémentation et qu'une "nouvelle" implémentation arrive, alors ils doivent changer chaque ligne qui utilise "new" pour utiliser la "nouvelle" implémentation ... avec l'usine leur code ne change pas ...

Usines - mieux que tout autre chose - le cadre de printemps est entièrement construit autour de cette idée.

Colin Saxton
la source
Comment l'usine résout-elle ce problème de devoir changer chaque ligne?
Nom de code Jack
0

Les usines sont une couche d'abstraction et, comme toutes les abstractions, elles ont un coût en complexité. Lorsque vous rencontrez une API basée en usine, déterminer quelle est l'usine pour une API donnée peut être difficile pour le consommateur d'API. Avec les constructeurs, la découvrabilité est triviale.

Lorsque vous décidez entre les cteurs et les usines, vous devez décider si la complexité est justifiée par l'avantage.

Il convient de noter que les constructeurs Javascript peuvent être des usines arbitraires en renvoyant autre chose que ceci ou indéfini. Ainsi, dans js, vous pouvez obtenir le meilleur des deux mondes: API détectable et mise en pool / mise en cache d'objets.

PeterH
la source
5
En JavaScript, le coût d'utilisation des constructeurs est plus élevé que le coût d'utilisation des usines car toute fonction de JS peut renvoyer un nouvel objet. Les constructeurs ajoutent de la complexité en: exigeant new, en modifiant le comportement de this, en modifiant la valeur de retour, en connectant une référence de prototype, en activant instanceof(qui ment et ne doit pas être utilisé à cette fin). Apparemment, tous ces éléments sont des "caractéristiques". En pratique, ils nuisent à la qualité de votre code.
Eric Elliott
0

Pour les différences, Eric Elliott a très bien précisé,

Mais pour la deuxième question:

Quand utiliser l'un au lieu de l'autre?

Si vous venez de l'arrière-plan orienté objet, la fonction Constructor vous semble plus naturelle. de cette façon, vous ne devez pas oublier d'utiliser le newmot-clé.

Mostafa
la source