angularjs z dziedziczeniem oop w działaniu

Abstrakcyjny

Pracuję nad aplikacją, która używa struktury kątowej jako ramy po stronie klienta, obecnie jest kanciasta i naprawdę się z niej cieszę, choć teraz uważam, że używam do kopiowania i wklejania kodu, który chciałbym zorganizować w hierarchię klas . Na przykład okna dialogowe mają wspólny zestaw funkcjonalności, muszą być otwierane, zamykane, kod, który zapewniatypeahead Funkcjonalność jest również pierwszym kandydatem do dziedziczenia po rodzicielskiej klasie BaseTypeaheadClass, choć jedną rzeczą, której nie znalazłem w kanciastym, jest standardowy sposób organizacji tych hierarchii. Oba kontrolery, usługi, dostawcy korzystają ze zwykłych funkcji javascript, które można rozszerzyć za pomocąprototype, więc moje pytanie brzmi:

Pytanie

Jaki jest kątowy sposób organizacji moich funkcji klasowych, czy istnieją jakieś standardowe mechanizmy, które pozwolą wyprowadzić jedną klasę z innej

P.S.

Moje przypuszczenia dotyczące problemu:

Zdefiniuj implementację klas bazowych jako usług, dzięki czemu będą one łatwo wstrzykiwane do dowolnego kontrolera lub innych usług, w których ta konkretna klasa będzie potrzebnaDefiniowaćOOP usługi i dostarczać metody takie jakdefine, derive, itp., które będą używane do tworzenia klas bazowych / pochodnychEdytować

Od czasu, kiedy początkowo zadawałem pytanie, minęło trochę czasu. Od tego czasu wyszedłem z podejściem, które z powodzeniem wykorzystuję w kilku projektach, które bardzo lubię i chcę się nimi podzielić z każdym.

Obecnie angular nie dostarcza żadnych konstrukcji do porządkowania hierarchii klas i szkoda, ponieważ mniej lub bardziej duża aplikacja nie może wystarczyć tylko dla modeli Model / View / Controller / ..., musi zorganizować swój kod w obiektach OOP.

Już od dłuższego czasu pracuję w dziedzinie tworzenia stron internetowych i nie widziałem nawet jednego projektu korporacyjnego, który masowo korzystałby z OOP z JavaScript. Widziałem ogromną i ładnie zorganizowaną logikę po stronie serwera / bazy danych + blisko nieskończonego spaghetti ze skryptami javascript z dodatkiem zoo frameworków i bibliotek po stronie klienta.

Żadne struktury MVVM, MVP, takie jak knockout.js, szkielet, inne ... nie są w stanie zastąpić OOP jako takiego. Jeśli nie używasz podstawowych zasad programowania zorientowanego, takich jak Klasy, Obiekty, Dziedziczenie, Abstrakcja, Polimorfizm, masz poważne kłopoty, to, co skończysz, to mega długie spaghetti javascript.

Jeśli chodzi o Angular, myślę, że jest to framework bardzo różniący się od frameworków knockout.js / backbone.js / jakichkolwiek innych struktur MVV-cokolwiek, ale zgodnie z moją praktyką nie jest to srebrna kula zdolna zastąpić OOP. Kiedy próbuję nie używać OOP z Angularem, kończę na zduplikowanej logice zlokalizowanej głównie w kontrolerach. I niestety nie ma (nie znalazłem) czystego i kanciastego sposobu na pokonanie tego problemu.

Ale udało mi się (myślę) rozwiązać ten problem.

Użyłem kompaktowej, zerowej zależności, która po prostu implementujeJohn Resig's Simple JavaScript Inheritance (https://github.com/tracker1/core-js/blob/master/js-extensions/040-Class.js). Z pomocą tej biblioteki udało mi się utworzyć / odziedziczyć / utworzyć abstrakcyjne metody / zastąpić je, innymi słowy, zrób wszystko, do czego przyzwyczaiłem się po stronie serwera.

Oto przykład użycia:

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();
}]);

OOP + Angular bardzo ładnie grają razem, obiekty tworzone w kontekście kątowym mogą automatycznie korzystać z wtrysku zależności za pośrednictwem usług, więc nie musisz wprowadzać instancji do konstruktorów OOP, a to sprawia, że ​​hierarchia OOP jest bardzo wąska i wolna od nieistotnych rzeczy to musi być (i jest) obsługiwane przez angular.js

Tak więc baw się tym podejściem i prześlij tutaj opinię na temat uzyskanych wyników lub napotkanych problemów,

Kolejna edycja

Ostatnio miałem do czynienia z kilkoma problemami z oryginalną implementacją Class.js, jak następuje:

1) Jeśli będziesz przekazywał referencje do swoich metod instancji jako wywołania zwrotne do innych metod, metody te mogą nie działać tak, jak oczekujesz ich działania. Będą tracić odniesienie dothis. W takim przypadku spodziewasz się zobaczyć swój obecny obiekt w środkuthis ale będzie to albo najwyższy poziomWindow lub inny obiekt kontekstowy w zależności od tego, jak wywołanie zwrotne wywołuje Twoją metodę. Dzieje się tak dzięki architekturze JavaScript. Aby walczyć z tym problemem, jest wyjątkowyClassMember funkcja, która instruujeClass aby powiązać metodę z kontekstem obiektu podczas jej tworzenia (sprawdźUsage poniżej, aby uzyskać dalsze wskazówki).

2) Oczywiście oryginałClass.js implementacja nie wie nic o typie kątowym deklaracji metody kontrolera, tj.

Class.extend('YourClassDisplayName', {
    ctor: function () {
        // Some useful constructor logic
    },
    controller: ['$scope', '$attrs', function ($scope, $attrs) {
        // Do something with $scope and $attrs
    }]
});

Bieżąca implementacja rozumie powyższą składnię

3) Przy korzystaniu z powyższego podejścia bez odpowiedniej obsługi złamałby kąt$$annotate„w procesie, więc odwołując się do powyższego przykładu uniemożliwiłoby to wstrzyknięcie$scope i$attrs wClassMember metoda lub metoda zastępowana, która używathis.base(...) połączenia. Tak też jest naprawione.

Gotchas:

1) Podczas korzystaniathis.base(...) w asynchronicznej procedurze obsługi (coś takiego$http.get(..., function() { self.base(...); })) proszę to zanotowaćthis.base(...) połączenie ma ograniczony czas życia i zaraz po powrocie metodythis.base(...) przestaje istnieć. Powinieneś więc zapisać odwołanie do metody podstawowej jawnie, jeśli planujesz wywoływać metody bazowe w sposób asynchroniczny. to znaczy:

...
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`
})

Rozwiązałem wszystkie powyższe problemy (z wyjątkiem jednego problemu, którego nie można rozwiązać z powodu architektury JavaScript) i chciałbym się nim podzielić z innymi, mam nadzieję, że na tym skorzystasz:

/* 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;
});
Kolejna edycja

Ostatecznie natknąłem się na inny problem związany z oryginalną implementacją Johna Resiga w odniesieniu do kątowego, a problem jest związany z procesem adnotacji kątowej (używanym do wstrzykiwania zależności), który używa Function.prototype.toString () i niektórych Regex'es dla cel wyodrębnienia nazw zależności. Problem z oryginalną implementacją polega na tym, że nie spodziewa się tego i dlatego nie można zadeklarować metod, które akceptują zależności, więc trochę zmodyfikowałem implementację, aby poradzić sobie z wcześniej opisanym problemem i tutaj:

/* 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;
});

questionAnswers(3)

yourAnswerToTheQuestion