我想我应该尝试回答我自己的问题。以下只是解决我最初问题中的问题1-3的一种方法。
免责声明:在描述模式或技术时,我可能并不总是使用正确的术语。很抱歉。
目标:
Create a complete example of a basic controller for viewing and editing Users.
All code must be fully testable and mockable.
The controller should have no idea where the data is stored (meaning it can be changed).
Example to show a SQL implementation (most common).
For maximum performance, controllers should only receive the data they need—no extra fields.
Implementation should leverage some type of data mapper for ease of development.
Implementation should have the ability to perform complex data lookups.
解决方案
我将持久存储(数据库)交互分为两类:R(读取)和CUD(创建、更新、删除)。我的经验是,读取是真正导致应用程序变慢的原因。虽然数据操作(CUD)实际上更慢,但它发生的频率要低得多,因此不太值得关注。
CUD(创建,更新,删除)很容易。这将涉及使用实际模型,然后传递给我的存储库进行持久化。注意,我的存储库仍将提供Read方法,但只是用于对象创建,而不是显示。稍后再详细介绍。
R(读)不那么容易。这里没有模型,只有值对象。如果您愿意,可以使用数组。这些对象可以表示单个模型,也可以表示多个模型的混合,实际上什么都可以。它们本身并不是很有趣,但它们是如何产生的却很有趣。我使用的是我所谓的查询对象。
代码:
用户模型
让我们从基本用户模型开始。注意,这里根本没有ORM扩展或数据库之类的东西。纯粹是模特的荣耀。添加getter, setter,验证等等。
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
库接口
在创建用户存储库之前,我想创建存储库接口。这将定义存储库必须遵循的“契约”,以便由我的控制器使用。记住,我的控制器不知道数据实际存储在哪里。
注意,我的存储库将只包含这三个方法。save()方法负责创建和更新用户,这仅仅取决于用户对象是否具有id集。
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
SQL存储库实现
现在要创建接口的实现。如前所述,我的示例将使用SQL数据库。注意,使用数据映射器可以避免编写重复的SQL查询。
class SQLUserRepository implements UserRepositoryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function find($id)
{
// Find a record with the id = $id
// from the 'users' table
// and return it as a User object
return $this->db->find($id, 'users', 'User');
}
public function save(User $user)
{
// Insert or update the $user
// in the 'users' table
$this->db->save($user, 'users');
}
public function remove(User $user)
{
// Remove the $user
// from the 'users' table
$this->db->remove($user, 'users');
}
}
查询对象接口
现在我们的存储库处理了CUD(创建、更新、删除),我们可以专注于R(读取)。查询对象只是某种类型的数据查找逻辑的封装。它们不是查询构建器。通过像我们的存储库一样抽象它,我们可以更容易地更改它的实现并测试它。查询对象的一个例子可能是AllUsersQuery或AllActiveUsersQuery,甚至是MostCommonUserFirstNames。
您可能会想“难道我不能在存储库中为这些查询创建方法吗?”是的,但我不这么做的原因是:
My repositories are meant for working with model objects. In a real world app, why would I ever need to get the password field if I'm looking to list all my users?
Repositories are often model specific, yet queries often involve more than one model. So what repository do you put your method in?
This keeps my repositories very simple—not an bloated class of methods.
All queries are now organized into their own classes.
Really, at this point, repositories exist simply to abstract my database layer.
对于我的例子,我将创建一个查询对象来查找“AllUsers”。界面如下:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
查询对象实现
This is where we can use a data mapper again to help speed up development. Notice that I am allowing one tweak to the returned dataset—the fields. This is about as far as I want to go with manipulating the performed query. Remember, my query objects are not query builders. They simply perform a specific query. However, since I know that I'll probably be using this one a lot, in a number of different situations, I'm giving myself the ability to specify the fields. I never want to return fields I don't need!
class AllUsersQuery implements AllUsersQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch($fields)
{
return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
}
}
在继续讨论控制器之前,我想展示另一个示例,以说明这是多么强大。也许我有一个报告引擎,需要为allverdueaccounts创建一个报告。对于我的数据映射器,这可能很棘手,在这种情况下,我可能需要编写一些实际的SQL。没问题,下面是这个查询对象的样子:
class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch()
{
return $this->db->query($this->sql())->rows();
}
public function sql()
{
return "SELECT...";
}
}
这很好地将该报告的所有逻辑保存在一个类中,并且易于测试。我可以随心所欲地模拟它,甚至完全使用不同的实现。
控制器
现在是有趣的部分——把所有的部分组合在一起。注意,我使用的是依赖注入。通常依赖关系被注入到构造函数中,但实际上我更喜欢将它们直接注入到我的控制器方法(路由)中。这最小化了控制器的对象图,而且我发现它更容易读懂。注意,如果您不喜欢这种方法,可以使用传统的构造函数方法。
class UsersController
{
public function index(AllUsersQueryInterface $query)
{
// Fetch user data
$users = $query->fetch(['first_name', 'last_name', 'email']);
// Return view
return Response::view('all_users.php', ['users' => $users]);
}
public function add()
{
return Response::view('add_user.php');
}
public function insert(UserRepositoryInterface $repository)
{
// Create new user model
$user = new User;
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the new user
$repository->save($user);
// Return the id
return Response::json(['id' => $user->id]);
}
public function view(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('view_user.php', ['user' => $user]);
}
public function edit(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('edit_user.php', ['user' => $user]);
}
public function update(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Update the user
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the user
$repository->save($user);
// Return success
return true;
}
public function delete(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Delete the user
$repository->delete($user);
// Return success
return true;
}
}
最后的想法:
这里需要注意的重要事项是,当我修改(创建、更新或删除)实体时,我使用的是真实的模型对象,并通过存储库执行持久化。
然而,当我显示(选择数据并将其发送到视图)时,我并没有使用模型对象,而是使用普通的旧值对象。我只选择我需要的字段,它的设计使我可以最大限度地提高数据查找性能。
我的存储库非常干净,相反,这些“混乱”被组织到我的模型查询中。
我使用数据映射器来帮助开发,因为为常见任务编写重复的SQL实在是太荒谬了。但是,您绝对可以在需要的地方编写SQL(复杂的查询、报告等)。当你这样做的时候,它会很好地隐藏在一个正确命名的类中。
我很想听听你对我的方法的看法!
2015年7月更新:
有人在评论中问我,我是怎么得出这些结论的。其实也没差那么远。说实话,我仍然不太喜欢存储库。我发现它们对于基本的查找(特别是如果您已经在使用ORM)来说是多余的,并且在处理更复杂的查询时是混乱的。
我通常使用ActiveRecord风格的ORM,所以大多数情况下,我将在整个应用程序中直接引用这些模型。但是,在有更复杂查询的情况下,我将使用查询对象来提高这些查询的可重用性。我还应该指出,我总是将我的模型注入到我的方法中,使它们更容易在测试中模拟。