ChatGPT解决这个技术问题 Extra ChatGPT

AngularJS: Understanding design pattern

In the context of this post by Igor Minar, lead of AngularJS:

MVC vs MVVM vs MVP. What a controversial topic that many developers can spend hours and hours debating and arguing about. For several years AngularJS was closer to MVC (or rather one of its client-side variants), but over time and thanks to many refactorings and api improvements, it's now closer to MVVM – the $scope object could be considered the ViewModel that is being decorated by a function that we call a Controller. Being able to categorize a framework and put it into one of the MV* buckets has some advantages. It can help developers get more comfortable with its apis by making it easier to create a mental model that represents the application that is being built with the framework. It can also help to establish terminology that is used by developers. Having said, I'd rather see developers build kick-ass apps that are well-designed and follow separation of concerns, than see them waste time arguing about MV* nonsense. And for this reason, I hereby declare AngularJS to be MVW framework - Model-View-Whatever. Where Whatever stands for "whatever works for you". Angular gives you a lot of flexibility to nicely separate presentation logic from business logic and presentation state. Please use it fuel your productivity and application maintainability rather than heated discussions about things that at the end of the day don't matter that much.

Are there any recommendations or guidelines for implementing AngularJS MVW (Model-View-Whatever) design pattern in client-side applications?

upvoted for ... than see them waste time arguing about MV* nonsense.
You don't need Angular to follow a word class design pattern.

C
Community

Thanks to a huge amount of valuable sources I've got some general recommendations for implementing components in AngularJS apps:

Controller

Controller should be just an interlayer between model and view. Try to make it as thin as possible.

It is highly recommended to avoid business logic in controller. It should be moved to model.

Controller may communicate with other controllers using method invocation (possible when children wants to communicate with parent) or $emit, $broadcast and $on methods. The emitted and broadcasted messages should be kept to a minimum.

Controller should not care about presentation or DOM manipulation.

Try to avoid nested controllers. In this case parent controller is interpreted as model. Inject models as shared services instead.

Scope in controller should be used for binding model with view and encapsulating View Model as for Presentation Model design pattern.

Scope

Treat scope as read-only in templates and write-only in controllers. The purpose of the scope is to refer to model, not to be the model.

When doing bidirectional binding (ng-model) make sure you don't bind directly to the scope properties.

Model

Model in AngularJS is a singleton defined by service.

Model provides an excellent way to separate data and display.

Models are prime candidates for unit testing, as they typically have exactly one dependency (some form of event emitter, in common case the $rootScope) and contain highly testable domain logic.

Model should be considered as an implementation of particular unit. It is based on single-responsibility-principle. Unit is an instance that is responsible for its own scope of related logic that may represent single entity in real world and describe it in programming world in terms of data and state.

Model should encapsulate your application’s data and provide an API to access and manipulate that data.

Model should be portable so it can be easily transported to similar application.

By isolating unit logic in your model you have made it easier to locate, update, and maintain.

Model can use methods of more general global models that are common for the whole application.

Try to avoid composition of other models into your model using dependency injection if it is not really dependent to decrease components coupling and increase unit testability and usability.

Try to avoid using event listeners in models. It makes them harder to test and generally kills models in terms of single-responsibility-principle.

Model Implementation

As model should encapsulate some logic in terms of data and state, it should architecturally restrict access to its members thus we can guarantee loose coupling.

The way to do it in AngularJS application is to define it using factory service type. This will allow us to define private properties and methods very easy and also return publically accessible ones in single place that will make it really readable for developer.

An example:

angular.module('search')
.factory( 'searchModel', ['searchResource', function (searchResource) {

  var itemsPerPage = 10,
  currentPage = 1,
  totalPages = 0,
  allLoaded = false,
  searchQuery;

  function init(params) {
    itemsPerPage = params.itemsPerPage || itemsPerPage;
    searchQuery = params.substring || searchQuery;
  }

  function findItems(page, queryParams) {
    searchQuery = queryParams.substring || searchQuery;

    return searchResource.fetch(searchQuery, page, itemsPerPage).then( function (results) {
      totalPages = results.totalPages;
      currentPage = results.currentPage;
      allLoaded = totalPages <= currentPage;

      return results.list
    });
  }

  function findNext() {
    return findItems(currentPage + 1);
  }

  function isAllLoaded() {
    return allLoaded;
  }

  // return public model API  
  return {
    /**
     * @param {Object} params
     */
    init: init,

    /**
     * @param {Number} page
     * @param {Object} queryParams
     * @return {Object} promise
     */
    find: findItems,

    /**
     * @return {Boolean}
     */
    allLoaded: isAllLoaded,

    /**
     * @return {Object} promise
     */
    findNext: findNext
  };
});

Creating new instances

Try to avoid having a factory that returns a new able function as this begins to break down dependency injection and the library will behave awkwardly, especially for third parties.

A better way to accomplish the same thing is to use the factory as an API to return a collection of objects with getter and setter methods attached to them.

angular.module('car')
 .factory( 'carModel', ['carResource', function (carResource) {

  function Car(data) {
    angular.extend(this, data);
  }

  Car.prototype = {
    save: function () {
      // TODO: strip irrelevant fields
      var carData = //...
      return carResource.save(carData);
    }
  };

  function getCarById ( id ) {
    return carResource.getById(id).then(function (data) {
      return new Car(data);
    });
  }

  // the public API
  return {
    // ...
    findById: getCarById
    // ...
  };
});

Global Model

In general try to avoid such situations and design your models properly thus it can be injected into controller and used in your view.

In particular case some methods require global accessibility within application. To make it possible you can define ‘common’ property in $rootScope and bind it to commonModel during application bootstrap:

angular.module('app', ['app.common'])
.config(...)
.run(['$rootScope', 'commonModel', function ($rootScope, commonModel) {
  $rootScope.common = 'commonModel';
}]);

All your global methods will live within ‘common’ property. This is some kind of namespace.

But do not define any methods directly in your $rootScope. This can lead to unexpected behavior when used with ngModel directive within your view scope, generally littering your scope and leads to scope methods overriding issues.

Resource

Resource lets you interact with different data sources.

Should be implemented using single-responsibility-principle.

In particular case it is a reusable proxy to HTTP/JSON endpoints.

Resources are injected in models and provide possibility to send/retrieve data.

Resource implementation

A factory which creates a resource object that lets you interact with RESTful server-side data sources.

The returned resource object has action methods which provide high-level behaviors without the need to interact with the low level $http service.

Services

Both model and resource are services.

Services are unassociated, loosely coupled units of functionality that are self-contained.

Services are a feature that Angular brings to client-side web apps from the server side, where services have been commonly used for a long time.

Services in Angular apps are substitutable objects that are wired together using dependency injection.

Angular comes with different types of services. Each one with its own use cases. Please read Understanding Service Types for details.

Try to consider main principles of service architecture in your application.

In general according to Web Services Glossary:

A service is an abstract resource that represents a capability of performing tasks that form a coherent functionality from the point of view of providers entities and requesters entities. To be used, a service must be realized by a concrete provider agent.

Client-side structure

In general client side of the application is splitted into modules. Each module should be testable as a unit.

Try to define modules depending on feature/functionality or view, not by type. See Misko’s presentation for details.

Module components may be conventionally grouped by types such as controllers, models, views, filters, directives etc.

But module itself remains reusable, transferable and testable.

It is also much easier for developers to find some parts of code and all its dependencies.

Please refer to Code Organization in Large AngularJS and JavaScript Applications for details.

An example of folders structuring:

|-- src/
|   |-- app/
|   |   |-- app.js
|   |   |-- home/
|   |   |   |-- home.js
|   |   |   |-- homeCtrl.js
|   |   |   |-- home.spec.js
|   |   |   |-- home.tpl.html
|   |   |   |-- home.less
|   |   |-- user/
|   |   |   |-- user.js
|   |   |   |-- userCtrl.js
|   |   |   |-- userModel.js
|   |   |   |-- userResource.js
|   |   |   |-- user.spec.js
|   |   |   |-- user.tpl.html
|   |   |   |-- user.less
|   |   |   |-- create/
|   |   |   |   |-- create.js
|   |   |   |   |-- createCtrl.js
|   |   |   |   |-- create.tpl.html
|   |-- common/
|   |   |-- authentication/
|   |   |   |-- authentication.js
|   |   |   |-- authenticationModel.js
|   |   |   |-- authenticationService.js
|   |-- assets/
|   |   |-- images/
|   |   |   |-- logo.png
|   |   |   |-- user/
|   |   |   |   |-- user-icon.png
|   |   |   |   |-- user-default-avatar.png
|   |-- index.html

Good example of angular application structuring is implemented by angular-app - https://github.com/angular-app/angular-app/tree/master/client/src

This is also considered by modern application generators - https://github.com/yeoman/generator-angular/issues/109


I have a one concern about: "It is highly recommended to avoid business logic in controller. It should be moved to model." However from official documentation you can read: "In general, a Controller shouldn't try to do too much. It should contain only the business logic needed for a single view.". Are we talking about the same thing?
I would say - treat Controller as View Model.
+1. Some great advices here! 2. Unfortunately the example of searchModel doesn't follow the re-usability advice. It would be better the import the constants via the constant service. 3. Any explanation what is meant here?: Try to avoid having a factory that returns a new able function
Also overwriting object's prototype property breaks the inheritance, instead one can use Car.prototype.save = ...
@ChristianAichinger, this is about the nature of JavaScript prototype chain that forces you either to use an object in your two-way binding expression to make sure you write to the exact property or setter function. In case of using the direct property of your scope (without a dot) you have a risk to hide the desired target property with the newly created one in the nearest upper scope in the prototype chain when writing to it. This is better explained in Misko's presentation
C
Community

I believe Igor's take on this, as seen in the quote you have provided, is just the iceberg tip of a far greater problem.

MVC and its derivatives (MVP, PM, MVVM) are all good and dandy within a single agent, but a server-client architecture is for all purposes a two-agent system, and people are often so obsessed with these patterns that they forget that the problem at hand is far more complex. By trying to adhere to these principles they actually end up with a flawed architecture.

Let's do this bit by bit.

The guidelines

Views

Within Angular context, the view is the DOM. The guidelines are:

Do:

Present scope variable (read only).

Call the controller for actions.

Don't:

Put any logic.

As tempting, short, and harmless this looks:

ng-click="collapsed = !collapsed"

It pretty much signify any developer that now to understand how the system work they need to inspect both the Javascript files, and the HTML ones.

Controllers

Do:

Bind the view to the 'model' by placing data on the scope.

Respond to user actions.

Deal with presentation logic.

Don't:

Deal with any business logic.

The reason for the last guideline is that controllers are sisters to views, not entities; nor they are reusable.

You could argue that directives are reusable, but directives too are sisters to views (DOM) - they were never intended to correspond to entities.

Sure, sometimes views represent entities, but that's a rather specific case.

In other words, controllers shall focus on presentation - if you throw business logic in, not only you are likely to end up with an inflated, little-manageable controller, but you also violate the separation of concern principle.

As such, controllers in Angular are really more of Presentation Model or MVVM.

And so, if controllers shouldn't deal with business logic, who should?

What is a model?

Your client model is often partial and stale

Unless you are writing an offline web application, or an application that is terribly simple (few entities), you client model is highly likely to be:

Partial Either it doesn't have all entities (like in the case of pagination) Or it doesn't have all the data (like in the case of pagination)

Either it doesn't have all entities (like in the case of pagination)

Or it doesn't have all the data (like in the case of pagination)

Stale - If the system has more than one user, at any point you can't be sure that the model the client holds is the same as the one the server hold.

The real model must persist

In traditional MCV, the model is the only thing being persisted. Whenever we talk about models, these must be persisted at some point. Your client may manipulate models at will, but until the roundtrip to the server was completed successfully, the job ain't done.

Consequences

The two points above should serve as a caution - the model your client holds can only involve a partial, mostly simple business logic.

As such, it is perhaps wise, within client context, to use lowercase M - so it's really mVC, mVP, and mVVm. The big M is for the server.

Business logic

Perhaps one of the most important concepts about business models is that you can subdivide them to 2 types (I omit the third view-business one as that's a story for another day):

Domain logic - aka Enterprise business rules, the logic that is application-independent. For example, give a model with firstName and sirName properties, a getter like getFullName() can be considered application-independent.

Application logic - aka Application business rules, which is application specific. For instance, error checks and handling.

It is important to stress that both of these within a client context are not 'real' business logic - they only deal with the portion of it that is important for the client. Application logic (not domain logic) should have the responsibility of facilitating communication with the server and most user interaction; while the domain logic is largely small-scale, entity-specific, and presentation-driven.

The question still remains - where do you throw them within an angular application?

3 vs 4 layer architecture

All these MVW frameworks use 3 layers:

https://i.stack.imgur.com/fsGn4.png

But there are two fundamental issues with this when it comes to clients:

The model is partial, stale and doesn't persist.

No place to put application logic.

An alternative to this strategy is the 4 layer strategy:

https://i.stack.imgur.com/6iZFu.jpg

The real deal here is the application business rules layer (Use cases), which often goes amiss on clients.

This layer is realised by interactors (Uncle Bob), which is pretty much what Martin Fowler calls an operation script service layer.

Concrete example

Consider the following web application:

The application shows a paginated list of users.

The user clicks 'Add user'.

A model opens with a form to fill user details.

The user fills the form and hit submit.

A few things should happen now:

The form should be client-validated.

A request shall be sent to the server.

An error shall be handled, if there is one.

The user list may or may not (due to pagination) needs updating.

Where do we throw all of this?

If your architecture involves a controller that calls $resource, all of this will happen within the controller. But there is a better strategy.

A proposed solution

The following diagram shows how the problem above can be solve by adding another application logic layer in Angular clients:

https://i.stack.imgur.com/prEEe.png

So we add a layer between controller to $resource, this layer (lets call it interactor):

Is a service. In the case of users, it may be called UserInteractor.

It provide methods corresponding to use cases, encapsulating application logic.

It controls the requests made to the server. Instead of a controller calling $resource with free-form parameters, this layer ensure that requests made to the server return data on which domain logic can act.

It decorates the returned data structure with domain logic prototype.

And so, with the requirements of the concrete example above:

The user clicks 'Add user'.

The controller asks the interactor for a blank user model, the is decorated with business logic method, like validate()

Upon submission, the controller calls the model validate() method.

If failed, the controller handles the error.

If successful, the controller calls the interactor with createUser()

The interactor calls $resource

Upon response, the interactor delegates any errors to the controller, which handles them.

Upon successful response, the interactor ensures that if needed, the user list updates.


So AngularJS is defined MVW (where W is for whatever) since I can choose to have a Controller (with all business logic in it) or a View Model/Presenter (without business logic but just some code to fill the view) with BL in a separate service? Am I right?
Best answer. Do you have a real example on GitHub of a 4-layered angular app?
@RPallas, No I don't (wish I had time for this). We are currently trying out an architecture where the 'application logic' is just a boundary interactor; a resolver between it and the controller and a view model that has some view logic. We're still experimenting, so not 100% of the pros or cons. But Once done I hope to write a blog somewhere.
@heringer Basically, we introduced models - OOP constructs that represent domain entities. It is these models that communicate with resources, not controllers. They encapsulate domain logic. Controllers call models, which in turn call resources.
@alex440 No. Although it's been two months now that a serious blog post about this topic is at the tip of my fingers. Xmas is coming - possibly then.
P
Pankaj Parkar

A minor issue comparing to the great advices in Artem's answer, but in terms of code readability, I found best to define the API completely inside the return object, to minimize going back and forth in code to look wheverer variables are defined:

angular.module('myModule', [])
// or .constant instead of .value
.value('myConfig', {
  var1: value1,
  var2: value2
  ...
})
.factory('myFactory', function(myConfig) {
  ...preliminary work with myConfig...
  return {
    // comments
    myAPIproperty1: ...,
    ...
    myAPImethod1: function(arg1, ...) {
    ...
    }
  }
});

If the return object becomes looking "too crowded", that is a sign that the Service is doing too much.


A
Alexander Farber

AngularJS doest not implement MVC in traditional way, rather it implements something closer to MVVM(Model-View-ViewModel), ViewModel can also be referred as binder(in angular case it can be $scope). The Model--> As we know model in angular can be just plain old JS objects or the data in our application

The View--> the view in angularJS is the HTML which has been parsed and compiled by angularJS by applying the directives or instructions or bindings, Main point here is in angular the input is not just the plain HTML string(innerHTML), rather it is DOM created by browser.

The ViewModel--> ViewModel is actually the binder/bridge between your view and model in angularJS case it is $scope, to initialize and augment the $scope we use Controller.

If i wanna summarize the answer: In angularJS application $scope has reference to the data, Controller controls the behaviour, and View handles the layout by interacting with controller to behave accordingly.


N
Naveen Reddy

To be crisp about the question, Angular uses different design patterns which we already encountered in our regular programming. 1) When we registers our controllers or directives, factory, services etc with respect to our module. Here it is hiding the data from the global space. Which is Module pattern. 2) When angular uses its dirty checking for comparing the scope variables, here it uses Observer Pattern. 3) All the parent child scopes in our controllers uses Prototypal pattern. 4) In case of injecting the services it uses Factory Pattern.

Overall it uses different known design patterns to solve the problems.