Search code examples
javascriptangularjsoopangularjs-service

Prototypal inheritance using Angularjs module.service


I´ve been working with Angularjs for a couple of months now, and I was wondering how to implement efficient OOP using this framework.

I am working in a project where I need to instantiate a "terminal" class, which has constructor properties and a series of methods (as a usual class). In normal JS I would use the pseudoclassical pattern (separating the constructor properties from methods in the prototype chain).

From I know about Angularjs, model.service() would be the best choice, as I create a new instance of the service each time I call it. But I've usually seen the next implementation when it comes to define the methods:

myApp.service('helloWorldFromService', function() {
    this.sayHello = function() {
        return "Hello, World!"
    };
});

But, wouldn't it create the function sayHello() everytime I call the class? I was wondering if it would be better to separate the functions, something like:

myApp.service('helloWorldFromService', function() {
    this.msg= "Hello, World!";
});

helloWorldFromService.prototype.showMessage = function() {
    return this.msg;
};

In that way, the function showMessage() will be created just once in memory, and shared across all instances of the services created.

Also, if this is possible (and if it really makes the code more efficient), what would be the way to implement it? (the code above is just a wild guess)

Thank you


Solution

  • Edit in response to comment: Looks like if you simply return the constructor function for your object you can do this, then access the prototype through the call to the service.

    Quick explanation, in the example, clicking "Get Again" will call the prototype function changeHelloWorldString and update the string with the name of the control. Once you click "Change Prototype", that changeHelloWorldString function is changed to append " [PROTOTYPE CHANGE]" to the string. Clicking either "Get Again" button will demonstrate that the prototype change made in the second controller affected the prototype chain for the object in both controllers.

    See the example below:

    angular.module('myModule2', [])
      .factory('myService', function() {
        function FactoryConstructor(thirdFunction) {
          this.helloWorldFunction = function() {
            return this.helloWorldString;
          }
          this.thirdFunction = thirdFunction;
        }
        FactoryConstructor.prototype.helloWorldString = 'Hello World';
        FactoryConstructor.prototype.changeHelloWorldString = function(newString) {
          this.helloWorldString = newString;
        };
        FactoryConstructor.prototype.changeThirdFunction = function(newFunction) {
          this.thirdFunction = newFunction;
        }
        return FactoryConstructor;
      })
      .controller('ctrl1', function($scope, myService) {
        var factoryResult = new myService(function() {
          this.helloWorldString += ' first';
        });
        $scope.hwString = factoryResult.helloWorldString;
        $scope.hwString2 = factoryResult.helloWorldFunction();
        // console.log(factoryResult instanceof myService) //tested true
        $scope.getAgain = function() {
          factoryResult.changeHelloWorldString('ctrl1 String');
          factoryResult.thirdFunction();
          $scope.hwString = factoryResult.helloWorldString;
          $scope.hwString2 = factoryResult.helloWorldFunction();
        }
      })
      .controller('ctrl2', function($scope, myService) {
        var factoryResult = new myService(function() {
          this.helloWorldString += ' second';
        });
        $scope.hwString = factoryResult.helloWorldString;
        $scope.hwString2 = factoryResult.helloWorldFunction();
        // console.log(factoryResult instanceof myService) //tested true
        $scope.getAgain = function() {
          factoryResult.changeHelloWorldString('ctrl2 String');
          factoryResult.thirdFunction();
          factoryResult.changeThirdFunction(function() {
            this.helloWorldString += ' third';
          });
          $scope.hwString = factoryResult.helloWorldString;
          $scope.hwString2 = factoryResult.helloWorldFunction();
        }
        
        $scope.changePrototype = function() {
          myService.prototype.changeHelloWorldString = function(newString) {
            this.helloWorldString = newString + " [PROTOTYPE CHANGE]";
          }
        }
      });
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <div ng-app='myModule2'>
      <div ng-controller='ctrl1'>
        <div>{{hwString}}</div>
        <div>{{hwString2}}</div>
        <button ng-click='getAgain()'>Get again</button>
      </div>
      <div ng-controller='ctrl2'>
        <div>{{hwString}}</div>
        <div>{{hwString2}}</div>
        <button ng-click='getAgain()'>Get again</button>
        <button ng-click='changePrototype()'>Change Prototype</button>
      </div>
    </div>

    Another excellent explanation of this is found in this answer. This may show a better/cleaner way to do what this example shows by using a provider (which service and factory are both derived from, see the side note from the author).

    Rest of original post below for background

    Angular services are singletons that can be injected into many places. So if you do this:

    angular.module('myModule', [])
    .service('myService', function() {
      var myService = this;
      this.helloWorldString = 'Hello World String';
      this.helloWorldFunction = function() {
        return myService.helloWorldString;
      }
    })
    .controller('main', function($scope, myService) {
      $scope.getAgain = function() {
        $scope.hwString = myService.helloWorldString;
        $scope.hwString2 = myService.helloWorldFunction();
      }
      $scope.getAgain();
    })
    .controller('notMain', function($scope, myService) {
      myService.helloWorldString = 'edited Hello World String';
      $scope.hwString = myService.helloWorldString;
      $scope.hwString2 = myService.helloWorldFunction();
    });
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <div ng-app='myModule'>
      <div ng-controller='main'>
        <div>{{hwString}}</div>
        <div>{{hwString2}}</div>
        <button ng-click='getAgain()'>Get again</button>
      </div>
      <div ng-controller='notMain'>
        <div>{{hwString}}</div>
        <div>{{hwString2}}</div>
      </div>
    </div>

    You'll notice that initially the two are different, because the first pair was obtained before the change, but the change made in the second controller DOES, in fact, affect the first. Just click the Get Again button and it will re-pull the info from the service and it now matches, proving they're the same object despite being injected into two different controllers.

    Looks like what you really want is a factory (although this is mostly semantics, you can change out "factory" for "service" in this next example and it will produce the same result. This can also be seen in the Angular docs themselves. The documentation for a service never actually uses .service, it uses .factory throughout). That way you can, in essence, construct a new instance of your factory object when you call, in this example, 'myService(...)'. Using those function parameters, you can customize properties, including functions, of the object you return, as you can see in the example.

    angular.module('myModule2', [])
      .factory('myService', function() {
        return function(stringInput, thirdFunction) {
          return {
            helloWorldString: stringInput,
            helloWorldFunction: function() {
              return this.helloWorldString;
            },
            thirdFunction: thirdFunction
          }
        }
      })
      .controller('ctrl1', function($scope, myService) {
        var factoryResult = myService('Hello World String', function () {
          this.helloWorldString += ' first';
        });
        $scope.hwString = factoryResult.helloWorldString;
        $scope.hwString2 = factoryResult.helloWorldFunction();
      
        $scope.getAgain = function() {
          factoryResult.thirdFunction();
          $scope.hwString = factoryResult.helloWorldString;
          $scope.hwString2 = factoryResult.helloWorldFunction();
        }
      })
      .controller('ctrl2', function($scope, myService) {
        var factoryResult = myService('new Hello World String', function () {
          this.helloWorldString += ' second';
        });
        $scope.hwString = factoryResult.helloWorldString;
        $scope.hwString2 = factoryResult.helloWorldFunction();
      
        $scope.getAgain = function() {
          factoryResult.thirdFunction();
          $scope.hwString = factoryResult.helloWorldString;
          $scope.hwString2 = factoryResult.helloWorldFunction();
        }
      });
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <div ng-app='myModule2'>
      <div ng-controller='ctrl1'>
        <div>{{hwString}}</div>
        <div>{{hwString2}}</div>
        <button ng-click='getAgain()'>Get again</button>
      </div>
      <div ng-controller='ctrl2'>
        <div>{{hwString}}</div>
        <div>{{hwString2}}</div>
        <button ng-click='getAgain()'>Get again</button>
      </div>
    </div>