Tôi nghĩ rằng tôi sẽ có một vết nứt khi trả lời câu hỏi của riêng tôi. Những gì tiếp theo chỉ là một cách giải quyết các vấn đề 1-3 trong câu hỏi ban đầu của tôi.
Tuyên bố miễn trừ trách nhiệm: Tôi không phải lúc nào cũng có thể sử dụng các thuật ngữ đúng khi mô tả các mẫu hoặc kỹ thuật. Xin lỗi vì điều đó.
Các mục tiêu:
- Tạo một ví dụ hoàn chỉnh về bộ điều khiển cơ bản để xem và chỉnh sửa
Users
.
- Tất cả các mã phải được kiểm tra đầy đủ và có thể nhạo báng.
- Bộ điều khiển không nên biết nơi dữ liệu được lưu trữ (có nghĩa là nó có thể được thay đổi).
- Ví dụ để hiển thị một triển khai SQL (phổ biến nhất).
- Để có hiệu suất tối đa, bộ điều khiển chỉ nên nhận dữ liệu họ cần không có trường bổ sung.
- Việc thực hiện nên tận dụng một số loại ánh xạ dữ liệu để dễ phát triển.
- Việc thực hiện phải có khả năng thực hiện tra cứu dữ liệu phức tạp.
Giải pháp
Tôi đang chia tương tác lưu trữ (cơ sở dữ liệu) liên tục của mình thành hai loại: R (Đọc) và CUD (Tạo, Cập nhật, Xóa). Kinh nghiệm của tôi là việc đọc thực sự là nguyên nhân khiến ứng dụng bị chậm lại. Và trong khi thao tác dữ liệu (CUD) thực sự chậm hơn, nó xảy ra ít thường xuyên hơn và do đó ít được quan tâm hơn.
CUD (Tạo, Cập nhật, Xóa) rất dễ dàng. Điều này sẽ liên quan đến việc làm việc với các mô hình thực tế , sau đó được truyền lại cho tôi Repositories
để kiên trì. Lưu ý, kho lưu trữ của tôi vẫn sẽ cung cấp phương thức Đọc, nhưng chỉ đơn giản là để tạo đối tượng, không hiển thị. Thêm về điều đó sau.
R (Đọc) không dễ dàng như vậy. Không có mô hình ở đây, chỉ là các đối tượng giá trị . Sử dụng mảng nếu bạn thích . Những đối tượng này có thể đại diện cho một mô hình duy nhất hoặc sự pha trộn của nhiều mô hình, bất cứ điều gì thực sự. Chúng không thú vị lắm, nhưng chúng được tạo ra như thế nào. Tôi đang sử dụng những gì tôi đang gọi Query Objects
.
Mật mã:
Mô hình người dùng
Hãy bắt đầu đơn giản với mô hình người dùng cơ bản của chúng tôi. Lưu ý rằng không có công cụ mở rộng ORM hoặc cơ sở dữ liệu nào cả. Chỉ là mô hình vinh quang thuần túy. Thêm getters, setters, xác nhận của bạn, bất cứ điều gì.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Giao diện kho lưu trữ
Trước khi tôi tạo kho lưu trữ người dùng, tôi muốn tạo giao diện kho lưu trữ của mình. Điều này sẽ xác định "hợp đồng" mà các kho lưu trữ phải tuân theo để được bộ điều khiển của tôi sử dụng. Hãy nhớ rằng, bộ điều khiển của tôi sẽ không biết dữ liệu thực sự được lưu trữ ở đâu.
Lưu ý rằng kho lưu trữ của tôi sẽ chỉ chứa ba phương thức này. Các save()
phương pháp có trách nhiệm cả việc tạo ra và cập nhật người dùng, chỉ đơn giản là tuỳ thuộc vào việc hay không đối tượng người dùng có một thiết lập id.
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
Triển khai kho lưu trữ SQL
Bây giờ để tạo ra việc thực hiện giao diện của tôi. Như đã đề cập, ví dụ của tôi sẽ là với cơ sở dữ liệu SQL. Lưu ý việc sử dụng bộ ánh xạ dữ liệu để tránh phải viết các truy vấn SQL lặp đi lặp lại.
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');
}
}
Giao diện đối tượng truy vấn
Bây giờ với CUD (Tạo, Cập nhật, Xóa) được lưu trữ bởi kho lưu trữ của chúng tôi, chúng tôi có thể tập trung vào R (Đọc). Các đối tượng truy vấn chỉ đơn giản là sự gói gọn của một số loại logic tra cứu dữ liệu. Họ không phải là người xây dựng truy vấn. Bằng cách trừu tượng hóa nó giống như kho lưu trữ của chúng tôi, chúng tôi có thể thay đổi việc triển khai và kiểm tra nó dễ dàng hơn. Một ví dụ về Đối tượng truy vấn có thể là AllUsersQuery
hoặc AllActiveUsersQuery
hoặc thậm chí MostCommonUserFirstNames
.
Bạn có thể đang nghĩ "tôi không thể tạo phương thức trong kho của mình cho những truy vấn đó sao?" Vâng, nhưng đây là lý do tại sao tôi không làm điều này:
- Kho của tôi có nghĩa là để làm việc với các đối tượng mô hình. Trong một ứng dụng thế giới thực, tại sao tôi cần phải lấy
password
trường nếu tôi muốn liệt kê tất cả người dùng của mình?
- Các kho lưu trữ thường là mô hình cụ thể, nhưng các truy vấn thường liên quan đến nhiều hơn một mô hình. Vì vậy, kho lưu trữ nào bạn đặt phương pháp của bạn vào?
- Điều này giữ cho kho lưu trữ của tôi rất đơn giản, không phải là một lớp phương thức cồng kềnh.
- Tất cả các truy vấn hiện được tổ chức thành các lớp riêng của họ.
- Thực sự, tại thời điểm này, các kho lưu trữ tồn tại đơn giản để trừu tượng lớp cơ sở dữ liệu của tôi.
Ví dụ của tôi, tôi sẽ tạo một đối tượng truy vấn để tra cứu "AllUsers". Đây là giao diện:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
Thực hiện đối tượng truy vấn
Đây là nơi chúng ta có thể sử dụng lại một trình ánh xạ dữ liệu để giúp tăng tốc độ phát triển. Lưu ý rằng tôi đang cho phép một tinh chỉnh cho bộ dữ liệu được trả về trong các trường. Đây là khoảng cách mà tôi muốn thực hiện với thao tác truy vấn được thực hiện. Hãy nhớ rằng, các đối tượng truy vấn của tôi không phải là người xây dựng truy vấn. Họ chỉ đơn giản là thực hiện một truy vấn cụ thể. Tuy nhiên, vì tôi biết rằng có lẽ tôi sẽ sử dụng cái này rất nhiều, trong một số tình huống khác nhau, tôi tự cho mình khả năng chỉ định các trường. Tôi không bao giờ muốn trả lại các lĩnh vực tôi không cần!
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();
}
}
Trước khi chuyển sang bộ điều khiển, tôi muốn đưa ra một ví dụ khác để minh họa mức độ mạnh mẽ của nó. Có lẽ tôi có một công cụ báo cáo và cần tạo một báo cáo cho AllOverdueAccounts
. Điều này có thể khó khăn với trình ánh xạ dữ liệu của tôi và tôi có thể muốn viết một số thực tế SQL
trong tình huống này. Không có vấn đề, đây là những gì đối tượng truy vấn này có thể trông như thế nào:
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...";
}
}
Điều này độc đáo giữ tất cả logic của tôi cho báo cáo này trong một lớp và thật dễ dàng để kiểm tra. Tôi có thể chế giễu nó với nội dung trái tim của tôi, hoặc thậm chí sử dụng một cách thực hiện hoàn toàn khác.
Bộ điều khiển
Bây giờ phần vui nhộn mang tất cả các mảnh lại với nhau. Lưu ý rằng tôi đang sử dụng tiêm phụ thuộc. Thông thường các phụ thuộc được đưa vào hàm tạo, nhưng tôi thực sự thích tiêm chúng ngay vào các phương thức điều khiển (tuyến) của tôi. Điều này giảm thiểu đồ thị đối tượng của bộ điều khiển và tôi thực sự thấy nó dễ đọc hơn. Lưu ý, nếu bạn không thích cách tiếp cận này, chỉ cần sử dụng phương thức xây dựng truyền thống.
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;
}
}
Suy nghĩ cuối cùng:
Điều quan trọng cần lưu ý ở đây là khi tôi sửa đổi (tạo, cập nhật hoặc xóa) các thực thể, tôi đang làm việc với các đối tượng mô hình thực và thực hiện sự kiên trì thông qua các kho lưu trữ của tôi.
Tuy nhiên, khi tôi đang hiển thị (chọn dữ liệu và gửi dữ liệu đến chế độ xem) Tôi không làm việc với các đối tượng mô hình, mà là các đối tượng giá trị cũ đơn giản. Tôi chỉ chọn các trường tôi cần và nó được thiết kế để tôi có thể tối đa hóa hiệu suất tra cứu dữ liệu của mình.
Kho lưu trữ của tôi rất sạch sẽ và thay vào đó, "mớ hỗn độn" này được sắp xếp vào các truy vấn mô hình của tôi.
Tôi sử dụng một trình ánh xạ dữ liệu để giúp phát triển, vì thật vô lý khi viết SQL lặp đi lặp lại cho các tác vụ thông thường. Tuy nhiên, bạn hoàn toàn có thể viết SQL khi cần (truy vấn phức tạp, báo cáo, v.v.). Và khi bạn làm thế, nó được xếp gọn vào một lớp được đặt tên đúng.
Tôi rất thích nghe bạn nói về cách tiếp cận của tôi!
Cập nhật tháng 7 năm 2015:
Tôi đã được hỏi trong các ý kiến nơi tôi đã kết thúc với tất cả điều này. Vâng, thực sự không xa lắm. Thật ra, tôi vẫn không thực sự thích kho. Tôi thấy chúng quá mức cần thiết cho các tra cứu cơ bản (đặc biệt nếu bạn đang sử dụng ORM) và lộn xộn khi làm việc với các truy vấn phức tạp hơn.
Tôi thường làm việc với ORM kiểu ActiveRecord, vì vậy, hầu hết tôi sẽ chỉ tham khảo các mô hình đó trực tiếp trong ứng dụng của mình. Tuy nhiên, trong các tình huống tôi có các truy vấn phức tạp hơn, tôi sẽ sử dụng các đối tượng truy vấn để làm cho các truy vấn này dễ sử dụng hơn. Tôi cũng nên lưu ý rằng tôi luôn tiêm các mô hình của mình vào các phương thức của mình, làm cho chúng dễ dàng giả hơn trong các thử nghiệm của tôi.