ChatGPT解决这个技术问题 Extra ChatGPT

Where to put model data and behaviour? [tl; dr; Use Services]

I am working with AngularJS for my latest project. In the documentation and tutorials all model data is put into the controller scope. I understand that is has to be there to be available for the controller and thus within the corresponding views.

However I dont think the model should actually be implemented there. It might be complex and have private attributes for example. Furthermore one might want to reuse it in another context/app. Putting everything into the controller totally breaks MVC pattern.

The same holds true for the behaviour of any model. If I would use DCI architecture and separate behaviour from the data model, I would have to introduce additional objects to hold the behaviour. This would be done by introducing roles and contexts.

DCI == Data Collaboration Interaction

Of course model data and behaviour could be implemented with plain javascript objects or any "class" pattern. But what would be the AngularJS way to do it? Using services?

So it comes down to this question:

How do you implement models decoupled from the controller, following AngularJS best practices?

I would up-vote this question if you could define DCI or at least provide the spelled out form. I have never seen this acronym in any software literature. Thanks.
I just added a link for DCI as a reference.
@JimRaden DCI is Dataq,Context,interaction and is a paradigm formulated firstly by the father of MVC (Trygve Reenskauge). There' quite some litterature on the subject by now. A good read is Coplien and Bjørnvig "Lean architecture"
Thanks. For better or worse, most people don't even know about the original literature by now. There are 55 million articles about MVC, according to Google, but only 250,000 that mention MCI and MVC. And on Microsoft.com? 7. AngularJS.org doesn't even mention the DCI acronym: "Your search - site:angularjs.org dci - did not match any documents".
Resource objects are basically the models in Angular.js.. am extending them.

J
Josh Darnell

You should use services if you want something usable by multiple controllers. Here's a simple contrived example:

myApp.factory('ListService', function() {
  var ListService = {};
  var list = [];
  ListService.getItem = function(index) { return list[index]; }
  ListService.addItem = function(item) { list.push(item); }
  ListService.removeItem = function(item) { list.splice(list.indexOf(item), 1) }
  ListService.size = function() { return list.length; }

  return ListService;
});

function Ctrl1($scope, ListService) {
  //Can add/remove/get items from shared list
}

function Ctrl2($scope, ListService) {
  //Can add/remove/get items from shared list
}

What would be the benefit of using a service over just creating a plain Javascript object as a model and assigning this to the controller scope?
Incase you need the same logic shared between multiple controllers. Also, this way it's easier to test things independently.
The last example kind of sucked, this one makes more sense. I edited it.
Yeah, with a plain old Javascript object you wouldn't be able to inject anything Angular into your ListService. Like in this example, if you needed to $http.get to retrieve the List data at the start, or if you needed to inject $rootScope so you could $broadcast events.
To make this example more DCI like shouldn't the data be outside of ListService?
B
Ben G

I'm currently trying this pattern, which, although not DCI, provides a classical service / model decoupling (with services for talking to web services (aka model CRUD), and model defining the object properties and methods).

Note that i only use this pattern whenever the model object needs methods working on its own properties, that i'll probably use everywhere (such as improved getter/setters). I'm not advocating doing this for every service systematically.

EDIT: I used to think this pattern would go against the "Angular model is plain old javascript object" mantra, but it seems to me now that this pattern is perfectly fine.

EDIT (2): To be even clearer, I use a Model class only to factor simple getters / setters (e.g. : to be used in view templates). For big business logic, i recommend using separate service(s) that "know" about the model, but are kept separated from them, and only include business logic. Call it a "business expert" service layer if you want

service/ElementServices.js (notice how Element is injected in the declaration)

MyApp.service('ElementServices', function($http, $q, Element)
{
    this.getById = function(id)
    {
        return $http.get('/element/' + id).then(
            function(response)
            {
                //this is where the Element model is used
                return new Element(response.data);
            },
            function(response)
            {
                return $q.reject(response.data.error);
            }
        );
    };
    ... other CRUD methods
}

model/Element.js (using angularjs Factory, made for object creation)

MyApp.factory('Element', function()
{
    var Element = function(data) {
        //set defaults properties and functions
        angular.extend(this, {
            id:null,
            collection1:[],
            collection2:[],
            status:'NEW',
            //... other properties

            //dummy isNew function that would work on two properties to harden code
            isNew:function(){
                return (this.status=='NEW' || this.id == null);
            }
        });
        angular.extend(this, data);
    };
    return Element;
});

I'm just getting into Angular, but I'd be curious to know if/why the veterans would think this is heresy. This is probably the way I would initially approach it as well. Could someone provide some feedback?
@Aaronius just to be clear : i have never actually read "you should never do that" on any angularjs doc or blogs, but i've always read things like "angularjs doesn't need a model, it's just using plain old javascript", and I had to discover this pattern on my own. Since this is my first real project on AngularJS, i'm putting those strong warnings, so that people don't copy/paste without thinking first.
I have settled on a roughly similar pattern. It's a shame that Angular doesn't have any real support (or seemingly desire to support) a model in the "classical" sense.
That doesn't look an heresy to me, you're using factories for what they were created for: building objects. I believe the "angularjs doesn't need a model" phrase means "you don't need to inherit from a special class, or use special methods (like ko.observable, in knockout) in order to work with models in angular, a pure js object will be enough".
Wouldn't having an appropriately named ElementService for each collection result in a bunch of nearly-identical files?
C
Community

The Angularjs documentation clearly states:

Unlike many other frameworks Angular makes no restrictions or requirements on the model. There are no classes to inherit from or special accessor methods for accessing or changing the model. The model can be primitive, object hash, or a full object Type. In short the model is a plain JavaScript object. — AngularJS Developer Guide - V1.5 Concepts - Model

So it means that's up to you how to declare a model. It's a simple Javascript object.

I personally won't use Angular Services as they were meant to behave like singleton objects you can use, for example, to keep global states across your application.


You should provide a link to where this is stated in the documentation. I did a Google search for "Angular makes no restrictions or requirements on the model", and it doesn't turn up anywhere in the official docs, as far as I can tell.
it was in the old angularjs docs (the one alive while answering): github.com/gitsome/docular/blob/master/lib/angular/ngdocs/guide/…
R
Rune FS

DCI is a paradigm and as such there's no angularJS way of doing it, either the language support DCI or it doesn't. JS support DCI rather well if you are willing to use source transformation and with some drawbacks if you are not. Again DCI has no more to do with dependency injection than say a C# class has and is definitely not a service either. So the best way to do DCI with angulusJS is to do DCI the JS way, which is pretty close to how DCI is formulated in the first place. Unless you do source transformation, you will not be able to do it fully since the role methods will be part of the object even outside the context but that's generally the problem with method injection based DCI. If you look at fullOO.info the authoritative site for DCI you could have a look at the ruby implementations they also use method injection or you could have a look at here for more information on DCI. It's mostly with RUby examples but the DCI stuff is agnostic to that. One of the keys to DCI is that what the system does is separated from what the system is. So the data object are pretty dumb but once bound to a role in a context role methods make certain behaviour available. A role is simply an identifier, nothing more, an when accessing an object through that identifier then role methods are available. There's no role object/class. With method injection the scoping of role methods is not exactly as described but close. An example of a context in JS could be

function transfer(source,destination){
   source.transfer = function(amount){
        source.withdraw(amount);
        source.log("withdrew " + amount);
        destination.receive(amount);
   };
   destination.receive = function(amount){
      destination.deposit(amount);
      destination.log("deposited " + amount);
   };
   this.transfer = function(amount){
    source.transfer(amount);
   };
}

Thanks for elaborating the DCI stuff. It is a great read. But my questions really aims at "where to put the model objects in angularjs". DCI is just in there for reference, that I might not only have a model, but split it in the DCI way. Will edit the question to make it more clear.
m
marianboda

This article about models in AngularJS might help:

http://joelhooks.com/blog/2013/04/24/modeling-data-and-state-in-your-angularjs-application/


Note that link-only answers are discouraged, SO answers should be the end-point of a search for a solution (vs. yet another stopover of references, which tend to get stale over time). Please consider adding a stand-alone synopsis here, keeping the link as a reference.
adding such a link in a comment on question would be fine though.
This link is actually a very good article, but ditto it would need to be crafted into an answer to be proper for SO
B
Brett Cassette

As stated by other posters, Angular provides no out-of-the-box base class for modeling, but one can usefully provide several functions:

Methods for interacting with a RESTful API and creating new objects Establishing relationships between models Validating data before persisting to the backend; also useful for displaying real-time errors Caching and lazy-loading to keep from making wasteful HTTP requests State machine hooks (before/after save, update, create, new, etc)

One library that does all of these things well is ngActiveResource (https://github.com/FacultyCreative/ngActiveResource). Full disclosure--I wrote this library--and I have used it successfully in building several enterprise-scale applications. It's well tested, and provides an API that should be familiar to Rails developers.

My team and I continue to actively develop this library, and I'd love to see more Angular developers contribute to it and battle test it.


Hey! This is really great! I will plug it into my app right now. Battle testing just started.
I as just looking at your post and was wondering what the differences between your ngActiveResource and Angular's $resource service. I'm a little new to Angular, and quickly browsed both sets of docs, but they seem to offer a lot of overlap. Was ngActiveResource developed prior to the $resource service being available?
T
TGH

An older question, but I think the topic is more relevant than ever given the new direction of Angular 2.0. I would say a best practice is to write code with as few dependencies on a particular framework as possible. Only use the framework specific parts where it adds direct value.

Currently it seems like the Angular service is one of the few concepts that will make it to the next generation of Angular, so it's probably smart to follow the general guideline of moving all logic to services. However, I would argue that you can make decoupled models even without a direct dependency on Angular services. Creating self contained objects with only necessary dependencies and responsibilities is probably the way to go. It also makes life a lot easier when doing automated testing. Single responsibility is a buzz work these days, but it does make a lot of sense!

Here is an example of a patter I consider good for decoupling the object model from the dom.

http://www.syntaxsuccess.com/viewarticle/548ebac8ecdac75c8a09d58e

A key goal is to structure your code in a way that makes it just as easy to use from a unit tests as from a view. If you achieve that you are well positioned to write realistic and useful tests.


g
georgeawg

I've tried to tackle that exact issue in this blog post.

Basically, the best home for data modeling is in services and factories. However, depending on how you retrieve your data and the complexity of the behaviors you need, there are lots of different ways to go about the implementation. Angular currently has no standard way or best practice.

The post covers three approaches, using $http, $resource, and Restangular.

Here's some example code for each, with a custom getResult() method on the Job model:

Restangular (easy peasy):

angular.module('job.models', [])
  .service('Job', ['Restangular', function(Restangular) {
    var Job = Restangular.service('jobs');

    Restangular.extendModel('jobs', function(model) {
      model.getResult = function() {
        if (this.status == 'complete') {
          if (this.passed === null) return "Finished";
          else if (this.passed === true) return "Pass";
          else if (this.passed === false) return "Fail";
        }
        else return "Running";
      };

      return model;
    });

    return Job;
  }]);

$resource (slightly more convoluted):

angular.module('job.models', [])
    .factory('Job', ['$resource', function($resource) {
        var Job = $resource('/api/jobs/:jobId', { full: 'true', jobId: '@id' }, {
            query: {
                method: 'GET',
                isArray: false,
                transformResponse: function(data, header) {
                    var wrapped = angular.fromJson(data);
                    angular.forEach(wrapped.items, function(item, idx) {
                        wrapped.items[idx] = new Job(item);
                    });
                    return wrapped;
                }
            }
        });

        Job.prototype.getResult = function() {
            if (this.status == 'complete') {
                if (this.passed === null) return "Finished";
                else if (this.passed === true) return "Pass";
                else if (this.passed === false) return "Fail";
            }
            else return "Running";
        };

        return Job;
    }]);

$http (hardcore):

angular.module('job.models', [])
    .service('JobManager', ['$http', 'Job', function($http, Job) {
        return {
            getAll: function(limit) {
                var params = {"limit": limit, "full": 'true'};
                return $http.get('/api/jobs', {params: params})
                  .then(function(response) {
                    var data = response.data;
                    var jobs = [];
                    for (var i = 0; i < data.objects.length; i ++) {
                        jobs.push(new Job(data.objects[i]));
                    }
                    return jobs;
                });
            }
        };
    }])
    .factory('Job', function() {
        function Job(data) {
            for (attr in data) {
                if (data.hasOwnProperty(attr))
                    this[attr] = data[attr];
            }
        }

        Job.prototype.getResult = function() {
            if (this.status == 'complete') {
                if (this.passed === null) return "Finished";
                else if (this.passed === true) return "Pass";
                else if (this.passed === false) return "Fail";
            }
            else return "Running";
        };

        return Job;
    });

The blog post itself goes into more detail on the reasoning behind why you might use each approach, as well as code examples of how to use the models in your controllers:

AngularJS Data Models: $http VS $resource VS Restangular

There's a possibility Angular 2.0 will offer a more robust solution to data modeling that gets everyone on the same page.