I read these two great articles:
我读过这两篇伟大的文章:
The state of angularjs controllers by Jonathan Creamer
乔纳森·克雷默(Jonathan Creamer)创作的《angularjs控制器的状态》(state of angularjs controller)
and
和
Rethinking AngularJS Controllers by Todd Motto
用托德·座右铭重新设计AngularJS控制器
In these articles, the authors talk about the right way to use controllers (making them anemic bridges between the view and the model) and factories/services (where the business logic should really live).
在这些文章中,作者讨论了使用控制器的正确方法(使它们在视图和模型之间形成了一种弱耦合的桥梁)和工厂/服务(业务逻辑应该真正存在的地方)。
This is great information, and I was really excited to start refactoring the controllers on one of my projects, but I quickly found that the structure shown in the articles breaks down if you have a rich object model.
这是很好的信息,我非常兴奋地开始重构我的一个项目的控制器,但是我很快发现,如果您有一个丰富的对象模型,文章中显示的结构就会崩溃。
Here's a recap of the setup from "Rethinking Angularjs Controllers":
以下是“重新考虑Angularjs控制器”的设置概述:
Here's the controller:
控制器:
app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {
var vm = this;
vm.messages = InboxFactory.messages;
vm.openMessage = function (message) {
InboxFactory.openMessage(message);
};
vm.deleteMessage = function (message) {
InboxFactory.deleteMessage(message);
};
InboxFactory
.getMessages()
.then(function () {
vm.messages = InboxFactory.messages;
});
});
and here's the factory:
这是工厂:
app.factory('InboxFactory', function InboxFactory ($location, NotificationFactory) {
factory.messages = [];
factory.openMessage = function (message) {
$location.search('id', message.id).path('/message');
};
factory.deleteMessage = function (message) {
$http.post('/message/delete', message)
.success(function (data) {
factory.messages.splice(index, 1);
NotificationFactory.showSuccess();
})
.error(function () {
NotificationFactory.showError();
});
};
factory.getMessages = function () {
return $http.get('/messages')
.success(function (data) {
factory.messages = data;
})
.error(function () {
NotificationFactory.showError();
});
};
return factory;
});
This is great and because providers
(the factory) are singletons, the data is maintained across views and can be accessed without having to reload it from the API.
这很好,因为提供者(工厂)是单例,数据可以跨视图维护,并且可以在不需要从API重新加载的情况下访问数据。
This works just fine if messages
are a top level object. But what happens if they aren't? What if this is an app for browsing the inboxes of other users? Maybe you're an administrator and you want to be able to manage and browse the inboxes of any user. Maybe you need multiple users' inboxes loaded at same time. How does this work? The problem is inbox messages are stored in the service, i.e. InboxFactory.messages
.
如果消息是*对象,那么这就很好了。但如果它们不是,会发生什么呢?如果这是一个用来浏览其他用户收件箱的应用呢?也许您是一个管理员,您希望能够管理和浏览任何用户的收件箱。也许您需要同时加载多个用户的收件箱。这是如何工作的呢?问题是收件箱消息存储在服务中,即InboxFactory.messages。
What if the hierarchy is like this:
如果等级制度是这样的:
Organization
|
__________________|____________________
| | |
Accounting Human Resources IT
| | |
________|_______ _____|______ ______|________
| | | | | | | | |
John Mike Sue Tom Joe Brad May Judy Jill
| | | | | | | | |
Inbox Inbox Inbox Inbox Inbox Inbox Inbox Inbox Inbox
Now messages
are several levels deep in the hierarchy, and have no meaning on their own. You can't store messages in the factory, InboxFactory.messages
because you have to retrieve messages for several users at a time.
现在消息在层次结构中有几个层次,它们本身没有任何意义。你不能在InboxFactory工厂中存储消息。消息,因为您必须一次检索多个用户的消息。
Now you will have an OrganizationFactory, a DepartmentFactory, a UserFactory, and an InboxFactory. Retrieving "messages" must be in the context of a user
, who is in the context of a department
, which is in the context of an organization
. How and where should the data be stored? How should it be retreived?
现在您将拥有一个组织工厂、一个部门工厂、一个用户工厂和一个InboxFactory。检索“消息”必须在用户的上下文中,在一个部门的上下文中,这是在一个组织的上下文中。如何以及在何处存储数据?如何才能得到回报?
So how should this be resolved? How should controllers, factories/services, and rich object models be structured?
那么如何解决这个问题呢?应该如何构造控制器、工厂/服务和富对象模型?
At this point in my thinking, I'm leaning towards just keeping it lean and not having a rich object model. Just store the objects on the $scope injected into the controller, and if you navigate to a new view, reload from the API. If you need some data persisted across views, you can build that bridge with a service or factory, but it shouldn't be the way you do most things.
在我的思考中,我倾向于保持它的精确性,而不是拥有一个丰富的对象模型。只需将对象存储在注入到控制器的$scope上,如果导航到新视图,则从API重新加载。如果需要跨视图持久化一些数据,可以使用服务或工厂来构建桥接,但大多数事情都不应该这样做。
How have other's solved this? Are there any patterns out there for this?
别人是怎么解决的?这有什么规律吗?
3 个解决方案
#1
3
You can use a rich object model, but for objects that are not top-level, their factories should expose an api for creating new instances rather than be used as singletons. This is is somewhat contrary to the design of many apps you see these days, which are more functional than object-oriented--I am not commenting on the pros and cons of either approach, and I don't think Angular forces you to adopt one or the other.
您可以使用富对象模型,但是对于非*对象,它们的工厂应该公开用于创建新实例的api,而不是用作单例。这与你现在看到的许多应用程序的设计有点不同,它们比面向对象的功能更实用——我不评论任何一种方法的优缺点,我也不认为你采用的是一种或另一种方式。
Your example, redesigned, in pseudocode:
你的例子,重新设计,伪代码:
app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {
var inbox = InboxFactory.createInbox();
$scope.getMessages = function(){
inbox.getMessages()
.then(...)
$scope.deleteMessages = function(){
inbox.deleteMessages()
.then(...)
});
#2
2
Your situation becomes much simpler if you adopt a route based approach (a la ngRoute
or something similar). Consider this alternative - warning untested code:
如果采用基于路由的方法(la ngRoute或类似的方法),您的情况会变得简单得多。考虑这个可选的警告未经测试的代码:
app.config(function($routeProvider) {
$routeProvider
.when('/inbox/:inboxId',
templateUrl: 'views/inbox.html',
controller: 'InboxCtrl',
controllerAs: 'inbox',
resolve: {
inboxMessages: function(InboxFactory) {
// Use use :inboxId route param if you need to work with multiple
// inboxes. Taking some libery here, we'll assuming
// `InboxFactory.getMessages()` returns a promise that will resolve to
// an array of messages;
return InboxFactory.getMessages();
}
}
// ... other routes
.otherwise: {
// ...
};
});
app.controller('InboxCtrl', function InboxCtrl (InboxFactory, inboxMessages) {
var vm = this;
vm.messages = inboxMessages;
vm.openMessage = InboxFactory.openMessage;
vm.deleteMessage = InboxFactory.deleteMessage;
});
Look how slim the controller is now! Granted I made use of some more compact syntax in a couple spots but this highlights how our controller really is just glueing things together.
看看现在的控制器有多苗条!虽然我在一些地方使用了一些更紧凑的语法,但是这突出了我们的控制器是如何把东西粘在一起的。
We can further streamline things by getting rid of InboxFactory.messages
, when would we actually use it? We're only guaranteed to have to have it be populated after InboxFactory.getMessages
resolves, so let's just have this promise resolve to the messages themselves.
我们可以通过去掉InboxFactory来进一步简化事情。消息,我们什么时候会真正使用它?我们只保证在InboxFactory之后要对它进行填充。getMessages解决了,所以让我们将这个承诺解析到消息本身。
Storing data in singletons in this way may be the easiest solution in some cases but it makes life difficult when that data must be fetched on the fly. You're going to be best off leaning on APIs and factories (as AlexMA suggests), pulling down the necessary data whenever a route changes (e.g. the user wants to look at a different inbox) and injecting that data directly into the appropriate controller.
以这种方式将数据存储在单例中可能是某些情况下最简单的解决方案,但是当数据必须被动态获取时,这将使生活变得困难。您最好依赖于api和工厂(如AlexMA所建议的),每当路由发生变化时(例如,用户希望查看不同的收件箱),都要提取必要的数据,并将数据直接注入适当的控制器。
Another benefit of this form is we get to have our data in hand at the time the controller is instantiated. We don't have to juggle asynchronous states or worry about putting lots of code in callbacks. As a corollary, we get to catch data loading errors before displaying a new inbox view and the user doesn't get stuck in a half baked state.
这种形式的另一个好处是,当控制器被实例化时,我们可以得到手头的数据。我们不需要处理异步状态或担心在回调中放入大量代码。作为一个推论,我们可以在显示新的inbox视图之前捕获数据加载错误,用户不会陷入半成熟的状态。
Further to the point of your question though notice that the burden of knowing how your rich model structure fits together is no longer the controller's problem. It just gets some data and exposes a bunch of methods to the view.
更进一步,你的问题的重点是,了解你丰富的模型结构如何组合在一起的负担不再是控制器的问题。它只获取一些数据并向视图公开一些方法。
#3
0
After MUCH tinkering and trying different approaches, my final decision is that you shouldn't persist your rich object model across views.
在进行了大量的修改和尝试不同的方法之后,我的最终决定是您不应该跨视图持久化富对象模型。
I keep the object model super lean and load just what I need for each view. There is high level data that I keep around (user information like name, id, email, etc. and organization data like which organization they are logged in with), but everything else gets loaded for the current view.
我保持对象模型超级精简,并加载每个视图所需的内容。有一些高级别的数据需要我保存(用户信息,如姓名、id、电子邮件等,以及组织数据,如他们登录的组织),但是其他所有内容都要为当前视图加载。
With this lean approach, here's what my factory would look like:
有了这种精益的方法,我的工厂将会是这样的:
app.factory('InboxFactory', function InboxFactory ($location, NotificationFactory) {
factory.messages = [];
factory.openMessage = function (message) {
$location.search('id', message.id).path('/message');
};
factory.deleteMessage = function (message) {
$http.post('/message/delete', message)
.success(function (data) {
NotificationFactory.showSuccess();
return data;
})
.error(function () {
NotificationFactory.showError();
return null;
});
};
factory.getMessages = function (userId) {
return $http.get('/messages/user/id/'+userId)
.success(function (data) {
return data;
})
.error(function () {
NotificationFactory.showError();
return null;
});
};
return factory;
});
And the controller:
控制器:
app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {
var vm = this;
vm.messages = {};
vm.openMessage = function (message) {
InboxFactory.openMessage(message);
};
vm.deleteMessage = function (message) {
InboxFactory.deleteMessage(message);
};
InboxFactory
.getMessages(userId) //you can get the userId from anywhere you want.
.then(function (data) {
vm.messages = data;
});
});
The benefits so far are:
到目前为止的好处是:
- simplified application logic
- 简化应用程序逻辑
- lean and mean, lightweight (I only load just what I need for the current state)
- 精简和平均,轻量级(我只加载当前状态所需的内容)
- less memory usage, which translates to overall better performance, especially on mobile
- 较少的内存使用,这意味着总体性能更好,特别是在移动设备上
#1
3
You can use a rich object model, but for objects that are not top-level, their factories should expose an api for creating new instances rather than be used as singletons. This is is somewhat contrary to the design of many apps you see these days, which are more functional than object-oriented--I am not commenting on the pros and cons of either approach, and I don't think Angular forces you to adopt one or the other.
您可以使用富对象模型,但是对于非*对象,它们的工厂应该公开用于创建新实例的api,而不是用作单例。这与你现在看到的许多应用程序的设计有点不同,它们比面向对象的功能更实用——我不评论任何一种方法的优缺点,我也不认为你采用的是一种或另一种方式。
Your example, redesigned, in pseudocode:
你的例子,重新设计,伪代码:
app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {
var inbox = InboxFactory.createInbox();
$scope.getMessages = function(){
inbox.getMessages()
.then(...)
$scope.deleteMessages = function(){
inbox.deleteMessages()
.then(...)
});
#2
2
Your situation becomes much simpler if you adopt a route based approach (a la ngRoute
or something similar). Consider this alternative - warning untested code:
如果采用基于路由的方法(la ngRoute或类似的方法),您的情况会变得简单得多。考虑这个可选的警告未经测试的代码:
app.config(function($routeProvider) {
$routeProvider
.when('/inbox/:inboxId',
templateUrl: 'views/inbox.html',
controller: 'InboxCtrl',
controllerAs: 'inbox',
resolve: {
inboxMessages: function(InboxFactory) {
// Use use :inboxId route param if you need to work with multiple
// inboxes. Taking some libery here, we'll assuming
// `InboxFactory.getMessages()` returns a promise that will resolve to
// an array of messages;
return InboxFactory.getMessages();
}
}
// ... other routes
.otherwise: {
// ...
};
});
app.controller('InboxCtrl', function InboxCtrl (InboxFactory, inboxMessages) {
var vm = this;
vm.messages = inboxMessages;
vm.openMessage = InboxFactory.openMessage;
vm.deleteMessage = InboxFactory.deleteMessage;
});
Look how slim the controller is now! Granted I made use of some more compact syntax in a couple spots but this highlights how our controller really is just glueing things together.
看看现在的控制器有多苗条!虽然我在一些地方使用了一些更紧凑的语法,但是这突出了我们的控制器是如何把东西粘在一起的。
We can further streamline things by getting rid of InboxFactory.messages
, when would we actually use it? We're only guaranteed to have to have it be populated after InboxFactory.getMessages
resolves, so let's just have this promise resolve to the messages themselves.
我们可以通过去掉InboxFactory来进一步简化事情。消息,我们什么时候会真正使用它?我们只保证在InboxFactory之后要对它进行填充。getMessages解决了,所以让我们将这个承诺解析到消息本身。
Storing data in singletons in this way may be the easiest solution in some cases but it makes life difficult when that data must be fetched on the fly. You're going to be best off leaning on APIs and factories (as AlexMA suggests), pulling down the necessary data whenever a route changes (e.g. the user wants to look at a different inbox) and injecting that data directly into the appropriate controller.
以这种方式将数据存储在单例中可能是某些情况下最简单的解决方案,但是当数据必须被动态获取时,这将使生活变得困难。您最好依赖于api和工厂(如AlexMA所建议的),每当路由发生变化时(例如,用户希望查看不同的收件箱),都要提取必要的数据,并将数据直接注入适当的控制器。
Another benefit of this form is we get to have our data in hand at the time the controller is instantiated. We don't have to juggle asynchronous states or worry about putting lots of code in callbacks. As a corollary, we get to catch data loading errors before displaying a new inbox view and the user doesn't get stuck in a half baked state.
这种形式的另一个好处是,当控制器被实例化时,我们可以得到手头的数据。我们不需要处理异步状态或担心在回调中放入大量代码。作为一个推论,我们可以在显示新的inbox视图之前捕获数据加载错误,用户不会陷入半成熟的状态。
Further to the point of your question though notice that the burden of knowing how your rich model structure fits together is no longer the controller's problem. It just gets some data and exposes a bunch of methods to the view.
更进一步,你的问题的重点是,了解你丰富的模型结构如何组合在一起的负担不再是控制器的问题。它只获取一些数据并向视图公开一些方法。
#3
0
After MUCH tinkering and trying different approaches, my final decision is that you shouldn't persist your rich object model across views.
在进行了大量的修改和尝试不同的方法之后,我的最终决定是您不应该跨视图持久化富对象模型。
I keep the object model super lean and load just what I need for each view. There is high level data that I keep around (user information like name, id, email, etc. and organization data like which organization they are logged in with), but everything else gets loaded for the current view.
我保持对象模型超级精简,并加载每个视图所需的内容。有一些高级别的数据需要我保存(用户信息,如姓名、id、电子邮件等,以及组织数据,如他们登录的组织),但是其他所有内容都要为当前视图加载。
With this lean approach, here's what my factory would look like:
有了这种精益的方法,我的工厂将会是这样的:
app.factory('InboxFactory', function InboxFactory ($location, NotificationFactory) {
factory.messages = [];
factory.openMessage = function (message) {
$location.search('id', message.id).path('/message');
};
factory.deleteMessage = function (message) {
$http.post('/message/delete', message)
.success(function (data) {
NotificationFactory.showSuccess();
return data;
})
.error(function () {
NotificationFactory.showError();
return null;
});
};
factory.getMessages = function (userId) {
return $http.get('/messages/user/id/'+userId)
.success(function (data) {
return data;
})
.error(function () {
NotificationFactory.showError();
return null;
});
};
return factory;
});
And the controller:
控制器:
app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {
var vm = this;
vm.messages = {};
vm.openMessage = function (message) {
InboxFactory.openMessage(message);
};
vm.deleteMessage = function (message) {
InboxFactory.deleteMessage(message);
};
InboxFactory
.getMessages(userId) //you can get the userId from anywhere you want.
.then(function (data) {
vm.messages = data;
});
});
The benefits so far are:
到目前为止的好处是:
- simplified application logic
- 简化应用程序逻辑
- lean and mean, lightweight (I only load just what I need for the current state)
- 精简和平均,轻量级(我只加载当前状态所需的内容)
- less memory usage, which translates to overall better performance, especially on mobile
- 较少的内存使用,这意味着总体性能更好,特别是在移动设备上