Saya pikir saya akan menjawab pertanyaan saya sendiri. Berikut ini hanyalah satu cara untuk menyelesaikan masalah 1-3 dalam pertanyaan awal saya.
Penafian: Saya mungkin tidak selalu menggunakan istilah yang tepat saat menjelaskan pola atau teknik. Maaf untuk itu.
Tujuan:
- Buat contoh lengkap dari pengontrol dasar untuk melihat dan mengedit
Users
.
- Semua kode harus sepenuhnya dapat diuji dan dapat dipermainkan.
- Pengontrol seharusnya tidak tahu di mana data disimpan (artinya dapat diubah).
- Contoh untuk menunjukkan implementasi SQL (paling umum).
- Untuk kinerja maksimum, pengontrol hanya akan menerima data yang mereka butuhkan — tidak ada bidang tambahan.
- Implementasi harus memanfaatkan beberapa jenis data mapper untuk kemudahan pengembangan.
- Implementasi harus memiliki kemampuan untuk melakukan pencarian data yang kompleks.
Solusinya
Saya membagi interaksi penyimpanan (basis data) saya menjadi dua kategori: R (Baca) dan CUD (Buat, Perbarui, Hapus). Pengalaman saya adalah bahwa membaca benar-benar menyebabkan aplikasi melambat. Dan sementara manipulasi data (CUD) sebenarnya lebih lambat, itu terjadi jauh lebih jarang, dan karena itu jauh lebih sedikit dari masalah.
CUD (Buat, Perbarui, Hapus) mudah. Ini akan melibatkan bekerja dengan model yang sebenarnya , yang kemudian diteruskan ke saya Repositories
untuk kegigihan. Catatan, repositori saya masih akan menyediakan metode Baca, tetapi hanya untuk pembuatan objek, bukan tampilan. Lebih lanjut tentang itu nanti.
R (Baca) tidak mudah. Tidak ada model di sini, hanya nilai objek . Gunakan array jika Anda mau . Objek-objek ini dapat mewakili model tunggal atau campuran dari banyak model, apa pun sebenarnya. Ini tidak terlalu menarik pada mereka sendiri, tetapi bagaimana mereka dihasilkan. Saya menggunakan apa yang saya panggil Query Objects
.
Kode:
Model Pengguna
Mari kita mulai dengan model pengguna dasar kami. Perhatikan bahwa tidak ada perluasan ORM atau basis data sama sekali. Hanya kemuliaan model murni. Tambahkan getter, setter, validasi, apa pun.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Antarmuka Repositori
Sebelum saya membuat repositori pengguna saya, saya ingin membuat antarmuka repositori saya. Ini akan menentukan "kontrak" yang harus diikuti oleh repositori agar dapat digunakan oleh pengontrol saya. Ingat, pengontrol saya tidak akan tahu di mana data sebenarnya disimpan.
Perhatikan bahwa repositori saya hanya akan berisi setiap tiga metode ini. The save()
Metode bertanggung jawab untuk kedua menciptakan dan memperbarui pengguna, hanya tergantung pada apakah atau tidak objek pengguna memiliki set id.
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
Implementasi Repositori SQL
Sekarang untuk membuat implementasi antarmuka saya. Seperti yang disebutkan, contoh saya adalah dengan database SQL. Catat penggunaan data mapper untuk mencegah keharusan menulis query SQL berulang.
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');
}
}
Antarmuka Objek Kueri
Sekarang dengan CUD (Buat, Perbarui, Hapus) diurus oleh repositori kami, kami dapat fokus pada R (Baca). Objek kueri hanyalah enkapsulasi dari beberapa jenis logika pencarian data. Mereka bukan pembangun permintaan. Dengan mengabstraksikannya seperti repositori kami, kami dapat mengubah implementasinya dan mengujinya lebih mudah. Contoh Obyek Kueri mungkin berupa AllUsersQuery
atau AllActiveUsersQuery
, atau bahkan MostCommonUserFirstNames
.
Anda mungkin berpikir "tidak bisakah saya membuat metode di repositori saya untuk pertanyaan itu?" Ya, tetapi inilah mengapa saya tidak melakukan ini:
- Repositori saya dimaksudkan untuk bekerja dengan objek model. Dalam aplikasi dunia nyata, mengapa saya harus mendapatkan
password
bidang jika saya ingin mendaftar semua pengguna saya?
- Repositori sering model spesifik, namun permintaan sering melibatkan lebih dari satu model. Jadi repositori apa yang Anda masukkan dalam metode Anda?
- Ini membuat repositori saya sangat sederhana — bukan kelas metode yang membengkak.
- Semua pertanyaan sekarang diatur ke dalam kelas mereka sendiri.
- Sungguh, pada titik ini, repositori ada hanya untuk abstrak lapisan database saya.
Sebagai contoh saya, saya akan membuat objek permintaan untuk mencari "AllUsers". Inilah antarmuka:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
Implementasi Objek Kueri
Di sinilah kita bisa menggunakan data mapper lagi untuk membantu mempercepat pengembangan. Perhatikan bahwa saya mengizinkan satu tweak untuk dataset yang dikembalikan - bidang. Sejauh ini saya ingin memanipulasi kueri yang dilakukan. Ingat, objek kueri saya bukan pembuat kueri. Mereka hanya melakukan kueri tertentu. Namun, karena saya tahu bahwa saya mungkin akan sering menggunakan ini, dalam sejumlah situasi yang berbeda, saya memberikan diri saya kemampuan untuk menentukan bidang. Saya tidak pernah ingin mengembalikan bidang yang tidak saya butuhkan!
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();
}
}
Sebelum beralih ke controller, saya ingin menunjukkan contoh lain untuk menggambarkan betapa kuatnya ini. Mungkin saya memiliki mesin pelaporan dan perlu membuat laporan untuk AllOverdueAccounts
. Ini bisa rumit dengan mapper data saya, dan saya mungkin ingin menulis beberapa aktual SQL
dalam situasi ini. Tidak masalah, di sini terlihat seperti apa objek query ini:
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...";
}
}
Ini dengan baik menyimpan semua logika saya untuk laporan ini dalam satu kelas, dan mudah untuk diuji. Saya dapat mengejeknya dengan sepenuh hati, atau bahkan menggunakan implementasi yang berbeda sama sekali.
Pengendali
Sekarang bagian yang menyenangkan — kumpulkan semuanya. Perhatikan bahwa saya menggunakan injeksi ketergantungan. Biasanya dependensi disuntikkan ke konstruktor, tetapi saya sebenarnya lebih suka menyuntikkannya langsung ke metode controller saya (rute). Ini meminimalkan grafik objek pengontrol, dan saya benar-benar merasa lebih terbaca. Catatan, jika Anda tidak menyukai pendekatan ini, cukup gunakan metode konstruktor tradisional.
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;
}
}
Pikiran terakhir:
Hal penting yang perlu diperhatikan di sini adalah ketika saya memodifikasi (membuat, memperbarui atau menghapus) entitas, saya bekerja dengan objek model nyata, dan melakukan persistensi melalui repositori saya.
Namun, ketika saya menampilkan (memilih data dan mengirimkannya ke tampilan), saya tidak bekerja dengan objek model, melainkan objek nilai lama yang polos. Saya hanya memilih bidang yang saya butuhkan, dan itu dirancang agar saya dapat memaksimalkan kinerja pencarian data saya.
Repositori saya tetap sangat bersih, dan sebagai gantinya "kekacauan" ini diatur dalam pertanyaan model saya.
Saya menggunakan data mapper untuk membantu pengembangan, karena itu konyol untuk menulis SQL berulang untuk tugas-tugas umum. Namun, Anda benar-benar dapat menulis SQL di mana diperlukan (pertanyaan rumit, pelaporan, dll.). Dan ketika Anda melakukannya, itu terselip dengan baik ke dalam kelas yang diberi nama dengan benar.
Saya ingin mendengar pendapat Anda tentang pendekatan saya!
Pembaruan Juli 2015:
Saya telah ditanya dalam komentar di mana saya berakhir dengan semua ini. Sebenarnya tidak terlalu jauh. Sejujurnya, saya masih tidak terlalu menyukai repositori. Saya menemukan mereka berlebihan untuk pencarian dasar (terutama jika Anda sudah menggunakan ORM), dan berantakan ketika bekerja dengan pertanyaan yang lebih rumit.
Saya biasanya bekerja dengan ORM gaya ActiveRecord, jadi paling sering saya hanya akan merujuk model-model itu langsung di seluruh aplikasi saya. Namun, dalam situasi di mana saya memiliki kueri yang lebih kompleks, saya akan menggunakan objek kueri untuk membuatnya lebih dapat digunakan kembali. Saya juga harus mencatat bahwa saya selalu menyuntikkan model saya ke dalam metode saya, membuatnya lebih mudah untuk mengejek dalam tes saya.