Résumé
Je travaille sur une application qui utilise angular comme framework côté client, angular est actuellement génial et je suis vraiment heureux de l'utiliser, mais maintenant je trouve que j'utilise trop de code copié-collé que je voudrais organiser en hiérarchie de classe. Par exemple, les boîtes de dialogue partagent un ensemble commun de fonctionnalités, elles doivent être ouvertes, fermées, le code qui fournit les fonctions d'ouverture, de fermeture, de fermeture et d'ouverture des boîtes de dialogue est très important. typeahead
est également un premier candidat pour hériter d'un parent BaseTypeaheadClass, bien qu'une chose que je n'ai pas trouvé dans angular est une manière standard d'organiser ces hiérarchies. Les contrôleurs, les services et les fournisseurs utilisent tous les deux des fonctions javascript ordinaires, qui peuvent être étendues au moyen de prototype
donc ma question est :
Question
Quelle est la manière angulaire d'organiser les fonctions de mes classes ? Existe-t-il des mécanismes standard permettant de dériver une classe d'une autre ?
P.S.
Mes suppositions sur le problème :
- Définir l'implémentation des classes de base en tant que services, ce qui permet de les injecter facilement dans n'importe quel contrôleur ou autre service où cette classe spécifique est nécessaire.
- Définir
OOP
et fournir des méthodes telles quedefine
,derive
etc. qui seront utilisées pour créer des classes de base/dérivées.
Editar
Un certain temps s'est écoulé depuis le moment où j'ai posé ma question initiale. Depuis lors, j'ai mis au point une approche que j'utilise avec succès dans plusieurs projets, que j'aime beaucoup et que je souhaite partager avec tout le monde.
Actuellement, angular ne fournit pas de constructions pour organiser les hiérarchies de classes et c'est dommage car une application plus ou moins grande ne peut se contenter des constructions Modèle/Vue/Contrôleur/..., elle doit organiser son code en objets OOP.
Je travaille dans le domaine du développement web depuis assez longtemps déjà et je n'ai pas vu un seul projet d'entreprise qui tirait massivement parti de la POO avec JavaScript. Ce que j'ai vu, c'est une logique énorme et bien organisée côté serveur / côté base de données + des spaghettis javascript presque infinis, graissés avec des tas de frameworks et de bibliothèques côté client.
Aucun MVVM, les frameworks MVP tels que knockout.js, backbone, autres... ne sont capables de remplacer la POO en tant que telle. Si vous n'utilisez pas les principes de base de la programmation orientée tels que les classes, les objets, l'héritage, l'abstraction, le polymorphisme, vous avez de gros problèmes, et ce que vous obtiendrez sera un méga long spaghetti javascript.
En ce qui concerne Angular, je pense qu'il s'agit d'un framework très différent de knockout.js / backbone.js / tout autre framework MVV-quelque chose, mais selon ma pratique, ce n'est pas non plus une solution miracle capable de remplacer la POO. Lorsque j'essaie de ne pas utiliser la POO avec Angular, je me retrouve avec une logique dupliquée située principalement dans les contrôleurs. Et malheureusement, il n'y a pas (je n'ai pas trouvé) de moyen propre et angulaire de résoudre ce problème.
Mais j'ai réussi (je pense) à résoudre ce problème.
J'ai utilisé une librairie compacte, sans dépendance, qui implémente seulement John Resig's Simple JavaScript Inheritance
( https://github.com/tracker1/core-js/blob/master/js-extensions/040-Class.js ). Avec l'aide de cette bibliothèque, j'ai pu créer / hériter / créer des méthodes abstraites / les surcharger, en d'autres termes faire tout ce que j'ai l'habitude de faire du côté serveur.
Voici un exemple d'utilisation :
Application.factory('SomeChildObject', ['$http', 'SomeParentClass', function ($http, SomeParentClass) {
var SomeChildClass = SomeParentClass.extend({
init: function() { // Constructor
this._super.init(123, 231); // call base constructor
},
someFunction: function() {
// Notice that your OOP now knows everything that can be injected into angular service, which is pretty cool :)
$http({method: 'GET', url: '/someUrl'}).then(function(){
this._super.someFunction(); // call base function implementation
});
}
});
// return new SomeChildClass(); // We are not returning instance here!
return SomeChildClass; // Service is a function definition not an instance of an object
}]);
// So now we can both use this service in angular and have the ability to extend it using the `extend` method call, like so:
Application.controller('MegaController', ['$scope', 'SomeChildClass', function ($scope, SomeChildClass) {
$scope.someObject = new SomeChildClass();
}]);
La POO et Angular vont très bien ensemble, les objets créés dans le contexte d'Angular peuvent profiter de l'injection de dépendance via les services automatiquement, donc vous n'avez pas besoin d'injecter des instances dans vos constructeurs POO et ce fait rend votre hiérarchie POO très mince et libre de choses non pertinentes qui doivent être (et sont) gérées par Angular.js.
Jouez donc avec cette approche et donnez votre avis ici sur les résultats que vous avez obtenus ou les problèmes que vous avez rencontrés,
Un autre montage
Récemment, j'ai rencontré quelques problèmes avec l'implémentation originale de Class.js, comme suit :
1) Si vous passez une référence à vos méthodes d'instance en tant que callbacks à d'autres méthodes, ces méthodes peuvent ne pas fonctionner comme vous le souhaitez. Elles perdront la référence à this
. Dans ce cas, vous vous attendez à voir votre objet actuel à l'intérieur de l'objet. this
mais ce sera soit le niveau supérieur Window
ou un autre objet de contexte selon la façon dont le callback appelle votre méthode. Cela se produit en raison de l'architecture JavaScript. Afin de combattre ce problème, une méthode spéciale ClassMember
est fournie, qui demande à Class
pour lier votre méthode au contexte de l'objet lors de sa création (vérifier Usage
ci-dessous pour plus d'informations).
2) Évidemment original Class.js
ne sait rien du type angulaire de déclarations de méthodes de contrôleur, c'est-à-dire
Class.extend('YourClassDisplayName', {
ctor: function () {
// Some useful constructor logic
},
controller: ['$scope', '$attrs', function ($scope, $attrs) {
// Do something with $scope and $attrs
}]
});
L'implémentation actuelle comprend la syntaxe ci-dessus
3) Si l'on utilise l'approche ci-dessus sans la manipulation appropriée, il y aura une rupture de l'angulaire. $$annotate
sur le processus, donc en se référant à l'exemple ci-dessus il serait impossible d'injecter $scope
y $attrs
en en ClassMember
ou une méthode surchargée qui utilise la méthode this.base(...)
appels. Donc, ceci est également corrigé.
Des problèmes :
1) Lorsque vous utilisez this.base(...)
dans un gestionnaire d'opérations asynchrones (quelque chose comme $http.get(..., function() { self.base(...); })
) veuillez noter que this.base(...)
a une durée de vie limitée et dès que la méthode retourne this.base(...)
cesse d'exister. Vous devez donc sauvegarder explicitement la référence à la méthode de base si vous prévoyez d'appeler les méthodes de base de manière asynchrone :
...
var self = this;
var base = this.base;
...
$http.get(..., function () {
base.call(self, ...); // or base.apply(self, ...), or base() if you don't care about `this`
})
J'ai résolu tous les problèmes ci-dessus (à l'exception d'un seul qui ne peut être résolu en raison de l'architecture JavaScript) et je voudrais le partager avec tout le monde, en espérant que vous en tirerez profit :
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*
* Inspired by base2 and Prototype
* Angular adaptations by Denis Yaremov http://github.com/lu4
* Usage:
---------------------------------
var X = Class.extend('X', {
ctor: function () {
this.name = "I'm X";
},
myOrdinaryMethod: function (x, y, z) {
console.log([this.name, x, y, z]);
},
myClassMemberMethod: ClassMember(function (x, y, z) {
console.log([this.name, x, y, z]);
})
});
var Y = Class.extend('Y', {
ctor: function () {
this.name = "I'm Y";
},
myOrdinaryMethod: function (x, y, z) {
console.log([this.name, x, y, z]);
},
myClassMemberMethod: ClassMember(function (x, y, z) {
console.log([this.name, x, y, z]);
})
});
var x = new X();
var y = new Y();
x.myClassMemberMethod('a', 'b', 'c'); // ["I'm X", "a", "b", "c"]
y.myClassMemberMethod('u', 'v', 'm'); // ["I'm Y", "u", "v", "m"]
x.myOrdinaryMethod('a', 'b', 'c'); // ["I'm X", "a", "b", "c"]
y.myOrdinaryMethod('u', 'v', 'm'); // ["I'm Y", "u", "v", "m"]
y.theirOrdinaryMethod = x.myOrdinaryMethod;
y.theirClassMemberMethod = x.myClassMemberMethod;
y.theirOrdinaryMethod('a', 'b', 'c'); // ["I'm Y", "a", "b", "c"]
y.theirClassMemberMethod('u', 'v', 'm'); // ["I'm X", "u", "v", "m"]
*/
angular.module('app').factory('ClassMember', function () {
return function ClassMember(fn) {
if (this instanceof ClassMember) {
this.fn = fn;
} else {
return new ClassMember(fn);
}
};
});
angular.module('app').factory('Class', function (ClassMember) {
var runtime = { initializing: false },
fnTest = /xyz/.test(function() { xyz; }) ? /\bbase\b/ : /.*/,
FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m,
STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var toString = Object.prototype.toString;
// The base Class implementation (does nothing)
function Class() { };
Class.members = { };
// Create a new Class that inherits from this class
Class.extend = function extend(displayName, properties) {
var array;
var targetMembers = {};
var sourceMembers = this.members;
for (var memberName in sourceMembers) {
if (sourceMembers.hasOwnProperty(memberName)) {
targetMembers[memberName] = sourceMembers[memberName];
}
}
var base = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the ctor constructor)
runtime.initializing = true;
var prototype = new this();
runtime.initializing = false;
// Copy the properties over onto the new prototype
for (var name in properties) {
if (properties.hasOwnProperty(name)) {
// Check if we're overwriting an existing function
var property = properties[name];
// Support angular's controller/service/factory declaration notation
if (toString.call(property) === '[object Array]') {
array = property;
var item = array[array.length - 1];
if (toString.call(item) === '[object Function]' || item instanceof ClassMember) {
property = array[array.length - 1];
} else {
array = null;
}
} else {
array = null;
}
var isClassMember = property instanceof ClassMember;
if (isClassMember) {
property = property.fn;
}
if (typeof property === "function") {
if (typeof base[name] === "function" && fnTest.test(property)) {
property = (function (propertyName, fn) {
var args = fn.toString().replace(STRIP_COMMENTS, '').match(FN_ARGS)[1];
return (new Function('propertyName', 'fn', 'base', 'return function (' + args + ') {\n\
var prevBase = this.base;\n\
var hasBase = "base" in this;\n\
\n\
// Add a new .base() method that is the same method\n\
// but on the super-class\n\
\n\
this.base = base[propertyName];\n\
\n\
// The method only need to be bound temporarily, so we\n\
// remove it when we\'re done executing\n\
var ret = fn.call(this' + (!!args ? (', ' + args) : args) + ');\n\
\n\
if (hasBase) {\n\
this.base = prevBase;\n\
} else {\n\
delete this["base"];\n\
}\n\
return ret;\n\
}'))(propertyName, fn, base);
})(name, property);
}
if (isClassMember) {
targetMembers[name] = property;
} else if (name in targetMembers) {
delete targetMembers[name];
}
if (array) {
array[array.length - 1] = property;
property = array;
}
prototype[name] = property;
} else {
prototype[name] = property;
}
}
}
var membersArray = [];
for (var i in targetMembers) {
if (targetMembers.hasOwnProperty(i)) {
membersArray.push({ name: i, fn: targetMembers[i] });
}
}
// All construction is actually done in the ctor method
var ChildClass = (new Function("runtime", "members", "FN_ARGS", "STRIP_COMMENTS", "return function " + (displayName || "Class") + "() {\n\
if (!runtime.initializing && this.ctor)\n\
{\n\
var length = members.length;\n\
for (var i = 0; i < length; i++)\n\
{\n\
var item = members[i];\n\
this[item.name] = (function (me, fn) {\n\
var args = fn.toString().replace(STRIP_COMMENTS, '').match(FN_ARGS)[1];\n\
return args ? (new Function('me', 'fn', 'return function (' + args + ') { return fn.call(me, ' + args + '); }'))(me, fn) : function () { return fn.call(me); };\n\
})(this, item.fn);\n\
\n\
}\n\
this.ctor.apply(this, arguments);\n\
}\n\
}"))(runtime, membersArray, FN_ARGS, STRIP_COMMENTS);
ChildClass.members = targetMembers;
// Populate our constructed prototype object
ChildClass.prototype = prototype;
// Enforce the constructor to be what we expect
ChildClass.prototype.constructor = ChildClass;
// And make this class extendable
ChildClass.extend = extend;
return ChildClass;
};
return Class;
});
Un autre montage
Finalement, je suis tombé sur un autre problème lié à l'implémentation originale de John Resig par rapport à angular, et le problème est lié au processus d'annotation d'angular (utilisé pour l'injection de dépendances) qui utilise Function.prototype.toString() et quelques Regex dans le but d'extraire les noms des dépendances. Et le problème avec l'implémentation originale est qu'elle ne s'attend pas à cela et donc vous n'êtes pas en mesure de déclarer des méthodes qui acceptent les dépendances, donc j'ai modifié l'implémentation un peu pour traiter le problème décrit précédemment et le voici :
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*
* Inspired by base2 and Prototype
* Angular adaptations by Denis Yaremov http://github.com/lu4
* Usage:
---------------------------------
var X = Class.extend('X', {
ctor: function () {
this.name = "I'm X";
},
myOrdinaryMethod: function (x, y, z) {
console.log([this.name, x, y, z]);
},
myClassMemberMethod: ClassMember(function (x, y, z) {
console.log([this.name, x, y, z]);
})
});
var Y = Class.extend('Y', {
ctor: function () {
this.name = "I'm Y";
},
myOrdinaryMethod: function (x, y, z) {
console.log([this.name, x, y, z]);
},
myClassMemberMethod: ClassMember(function (x, y, z) {
console.log([this.name, x, y, z]);
})
});
var x = new X();
var y = new Y();
x.myClassMemberMethod('a', 'b', 'c'); // ["I'm X", "a", "b", "c"]
y.myClassMemberMethod('u', 'v', 'm'); // ["I'm Y", "u", "v", "m"]
x.myOrdinaryMethod('a', 'b', 'c'); // ["I'm X", "a", "b", "c"]
y.myOrdinaryMethod('u', 'v', 'm'); // ["I'm Y", "u", "v", "m"]
y.theirOrdinaryMethod = x.myOrdinaryMethod;
y.theirClassMemberMethod = x.myClassMemberMethod;
y.theirOrdinaryMethod('a', 'b', 'c'); // ["I'm Y", "a", "b", "c"]
y.theirClassMemberMethod('u', 'v', 'm'); // ["I'm X", "u", "v", "m"]
*/
angular.module('homer').factory('Class', function () {
function ClassMember(fn) {
if (this instanceof ClassMember) {
this.fn = fn;
return this;
} else {
return new ClassMember(fn);
}
}
function ClassEvent() {
if (this instanceof ClassEvent) {
return this;
} else {
return new ClassEvent();
}
}
var runtime = { initializing: false },
fnTest = /xyz/.test(function () { xyz; }) ? /\bbase\b/ : /.*/,
fnArgs = /^function\s*[^\(]*\(\s*([^\)]*)\)/m,
stripComments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var toString = Object.prototype.toString;
// The base Class implementation (does nothing)
function Class() { };
Class.events = {};
Class.members = {};
// Create a new Class that inherits from this class
Class.extend = function Extend(displayName, properties) {
var array;
var targetEvents = {};
var sourceEvents = this.events;
var targetMembers = {};
var sourceMembers = this.members;
for (var eventName in sourceEvents) {
if (sourceEvents.hasOwnProperty(eventName)) {
targetEvents[eventName] = sourceEvents[eventName];
}
}
for (var memberName in sourceMembers) {
if (sourceMembers.hasOwnProperty(memberName)) {
targetMembers[memberName] = sourceMembers[memberName];
}
}
var base = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the ctor constructor)
runtime.initializing = true;
var prototype = new this();
runtime.initializing = false;
// Copy the properties over onto the new prototype
for (var name in properties) {
if (properties.hasOwnProperty(name)) {
// Check if we're overwriting an existing function
var property = properties[name];
// Support angular's controller/service/factory declaration notation
if (toString.call(property) === '[object Array]') {
array = property;
var item = array[array.length - 1];
if (toString.call(item) === '[object Function]' || item instanceof ClassMember) {
property = array[array.length - 1];
} else {
array = null;
}
} else {
array = null;
}
var isClassMember = property instanceof ClassMember;
if (isClassMember) {
property = property.fn;
}
var isClassEvent = property instanceof ClassEvent;
if (isClassEvent) {
property = (function() {
function Subscriber(fn) {
Subscriber.listeners.push(fn.bind(this));
};
Subscriber.listeners = [];
Subscriber.fire = function() {
var listeners = Subscriber.listeners;
for (var i = 0; i < listeners.length; i++) {
var result = listeners[i].apply(this, arguments);
if (result !== undefined) return result;
}
return void 0;
}
return Subscriber;
})();
}
if (typeof property === "function") {
if (typeof base[name] === "function" && fnTest.test(property)) {
property = (function (propertyName, fn) {
var args = fn.toString().replace(stripComments, '').match(fnArgs)[1];
return (new Function('propertyName', 'fn', 'base', 'return function (' + args + ') {\n\
var prevBase = this.base;\n\
var hasBase = "base" in this;\n\
\n\
// Add a new .base() method that is the same method\n\
// but on the super-class\n\
\n\
this.base = base[propertyName];\n\
\n\
// The method only need to be bound temporarily, so we\n\
// remove it when we\'re done executing\n\
var ret = fn.call(this' + (!!args ? (', ' + args) : args) + ');\n\
\n\
if (hasBase) {\n\
this.base = prevBase;\n\
} else {\n\
delete this["base"];\n\
}\n\
return ret;\n\
}'))(propertyName, fn, base);
})(name, property);
}
if (isClassEvent) {
targetEvents[name] = property;
} else {
delete targetEvents[name];
}
if (isClassMember) {
targetMembers[name] = property;
} else if (name in targetMembers) {
delete targetMembers[name];
}
if (array) {
array[array.length - 1] = property;
property = array;
}
prototype[name] = property;
} else {
prototype[name] = property;
}
}
}
var eventsArray = [];
for (var targetEventName in targetEvents) {
if (targetEvents.hasOwnProperty(targetEventName)) {
eventsArray.push({ name: targetEventName, fn: targetEvents[targetEventName] });
}
}
var membersArray = [];
for (var targetMemberName in targetMembers) {
if (targetMembers.hasOwnProperty(targetMemberName)) {
membersArray.push({ name: targetMemberName, fn: targetMembers[targetMemberName] });
}
}
// All construction is actually done in the ctor method
var ChildClass = (new Function("runtime", "events", "members", "FN_ARGS", "STRIP_COMMENTS", "return function " + (displayName || "Class") + "() {\n\
if (!runtime.initializing && this.ctor)\n\
{\n\
var length = members.length;\n\
var bind = function (me, $$fn$$) {\n\
var args = $$fn$$.toString().replace(STRIP_COMMENTS, '').match(FN_ARGS)[1];\n\
var result = args ? (new Function('me', '$$fn$$', 'return function (' + args + ') { return $$fn$$.apply(me, arguments); }'))(me, $$fn$$) : function () { return $$fn$$.apply(me, arguments); };\n\
return result;\n\
};\n\
for (var i = 0; i < length; i++)\n\
{\n\
var item = members[i];\n\
var fn = item.fn;\n\
var name = item.name;\n\
var property = this[name] = bind(this, fn);\n\
if (fn.fire) {\n\
property.fire = bind(this, fn.fire);\n\
}\n\
if (fn.listeners) {\n\
property.listeners = fn.listeners;\n\
}\n\
}\n\
\n\
var length = events.length;\n\
for (var i = 0; i < length; i++)\n\
{\n\
var item = events[i];\n\
var fn = item.fn;\n\
var name = item.name;\n\
var property = this[name] = bind(this, fn);\n\
if (fn.fire) {\n\
property.fire = bind(this, fn.fire);\n\
}\n\
if (fn.listeners) {\n\
property.listeners = fn.listeners;\n\
}\n\
}\n\
this.ctor.apply(this, arguments);\n\
}\n\
}"))(runtime, eventsArray, membersArray, fnArgs, stripComments);
ChildClass.members = targetMembers;
// Populate our constructed prototype object
ChildClass.prototype = prototype;
// Enforce the constructor to be what we expect
ChildClass.prototype.constructor = ChildClass;
// And make this class extendable
ChildClass.extend = Extend;
ChildClass.event = ClassEvent;
ChildClass.member = ClassMember;
return ChildClass;
};
Class.member = ClassMember;
Class.event = ClassEvent;
return Class;
});
0 votes
Bonjour, utilisez-vous toujours l'héritage de John Resig de la manière décrite dans votre exemple ? Rencontrez-vous le problème de "l'héritage profond" ? J'ai essayé de faire la même chose, mais si mes
BaseClass
a un objet à l'intérieur, alors cette approche ne fonctionne pas, parce que toutes les instances de Child Classes ont une référence à la même instance de "object property". Je ne sais donc pas si c'est un problème conceptuel ou si j'utilise simplement la mauvaise version de l'implémentation de la classe de John Resig. Merci !0 votes
Bonjour Alex, non, je n'ai pas utilisé l'approche de l'héritage profond, mais j'ai été confronté à un autre problème lié à la dépendance circulaire (voir la lecture suivante ->). goo.gl/g5OBNC + goo.gl/FUBFwF ). En conséquence, j'en suis venu à penser que le problème de dépendance circulaire est le résultat de mauvaises habitudes de codage qui peuvent me frapper lorsque je n'attends pas cela. Donc, en ce qui concerne votre problème, je pense que l'imbrication des classes est une mauvaise chose à faire puisque vous ne serez pas en mesure de les tester de manière isolée en raison de la nature prototypique de javascript. Avez-vous essayé de reconsidérer votre architecture ?
0 votes
Il y a des tonnes de cadres d'héritage plus sophistiqués sur Internet, donc vous pouvez aussi essayer de substituer l'héritage exact de John Resig par l'héritage de quelqu'un d'autre, mais encore une fois, je ne recommanderais pas d'imbriquer les classes en elles-mêmes en général, de cette façon vous violez les limites de dépendance, en faisant qu'une dépendance contienne deux classes (c'est la solution à laquelle je suis arrivé, avant de lire sur les dépendances circulaires)...
0 votes
Vous devriez ajouter un plunker avec une démo - cela semble fascinant.
0 votes
Ticket Stackoverflow sur le même sujet : stackoverflow.com/questions/21483555/