Kendi sorumu cevaplamak için bir boşluk bırakacağımı düşündüm. Aşağıda, orijinal sorumdaki 1-3 arasındaki sorunları çözmenin sadece bir yolu var.
Feragatname: Paternleri veya teknikleri açıklarken her zaman doğru terimleri kullanmayabilirim. Bunun için özür dilerim.
Hedefler:
- Görüntüleme ve düzenleme için temel bir denetleyicinin tam bir örneğini oluşturun
Users
.
- Tüm kodlar tamamen test edilebilir ve taklit edilebilir olmalıdır.
- Denetleyicinin verilerin nerede saklandığı hakkında hiçbir fikri olmamalıdır (yani değiştirilebilir).
- Bir SQL uygulamasını göstermek için örnek (en yaygın).
- Maksimum performans için kontrolörler yalnızca ihtiyaç duydukları verileri almalıdır; ekstra alan olmamalıdır.
- Uygulama, geliştirme kolaylığı için bir tür veri eşleştiriciden yararlanmalıdır.
- Uygulama, karmaşık veri aramaları yapabilme yeteneğine sahip olmalıdır.
Çözüm
Kalıcı depolama (veritabanı) etkileşimi iki kategoriye ayırıyorum : R (Oku) ve CUD (Oluştur, Güncelle, Sil). Deneyimlerim, bir uygulamanın yavaşlamasına neden olan okumaların gerçekten olmasıydı. Ve veri manipülasyonu (CUD) aslında daha yavaş olsa da, daha az sıklıkla gerçekleşir ve bu nedenle daha az endişe duyar.
CUD (Oluştur, Güncelle, Sil) kolaydır. Bu, daha sonra kalıcılık için bana aktarılan gerçek modellerle çalışmayı içerecektir Repositories
. Depolarım hala bir Okuma yöntemi sunacak, ancak yalnızca nesne oluşturma için görüntülenmeyecek. Daha sonra.
R (Oku) o kadar kolay değil. Burada model yok, sadece nesnelere değer verin . Diziler kullanın isterseniz . Bu nesneler tek bir modeli veya birçok modelin bir karışımını temsil edebilir, gerçekten herhangi bir şey. Bunlar kendi başlarına çok ilginç değiller, ancak nasıl üretildikleri. Dediğim şeyi kullanıyorum Query Objects
.
Kod:
Kullanıcı Modeli
Temel kullanıcı modelimizle basit başlayalım. Hiçbir ORM genişletme veya veritabanı öğesi olmadığını unutmayın. Sadece saf model görkemi. Alıcılarınızı, ayarlayıcılarınızı, doğrulamanızı, her neyse ekleyin.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Havuz Arayüzü
Kullanıcı veri havuzumu oluşturmadan önce veri havuzu arayüzümü oluşturmak istiyorum. Bu, denetleyicimin kullanabilmesi için depoların izlemesi gereken "sözleşmeyi" tanımlayacaktır. Unutmayın, denetleyicim verilerin gerçekte nerede depolandığını bilemez.
Depolarımın yalnızca bu üç yöntemi içereceğini unutmayın. save()
Yöntem hem oluşturma ve kullanıcıları güncellemeye sadece kullanıcı nesnesi bir kimlik kümesi olup olmadığını bağlı olarak sorumludur.
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
SQL Veri Havuzu Uygulaması
Şimdi benim arayüz uygulama oluşturmak için. Bahsettiğim gibi, örneğim bir SQL veritabanı ile olacaktı. Tekrarlayan SQL sorguları yazmak zorunda kalmamak için bir veri eşleyici kullanıldığına dikkat edin .
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');
}
}
Sorgu Nesnesi Arabirimi
Şimdi depomuz tarafından ilgilenilen CUD (Oluştur, Güncelle, Sil) ile R (Oku) ' ya odaklanabiliriz . Sorgu nesneleri, bir tür veri arama mantığının kapsüllenmesidir. Bunlar değil sorgu inşaatçılar. Depomuz gibi soyutlayarak uygulamayı değiştirebilir ve daha kolay test edebiliriz. Sorgu Nesnesi örneği bir AllUsersQuery
veya AllActiveUsersQuery
, hatta çift olabilir MostCommonUserFirstNames
.
"Veri havuzlarımda bu sorgular için yöntem oluşturamaz mıyım?" Diye düşünüyor olabilirsiniz. Evet, ama bunu neden yapmıyorum:
- Depolarım model nesnelerle çalışmak içindir. Gerçek bir dünya uygulamasında,
password
tüm kullanıcılarımı listelemek istiyorsam neden alanı elde etmem gerekiyor ?
- Depolar genellikle modele özgüdür, ancak sorgular genellikle birden fazla model içerir. Peki yönteminizi hangi depoya yerleştiriyorsunuz?
- Bu, depolarımı çok basit tutar; şişirilmiş bir yöntem sınıfı değil.
- Tüm sorgular artık kendi sınıflarında düzenlenmiştir.
- Gerçekten, bu noktada, veri tabanları sadece veritabanı katmanımı soyutlamak için var.
Örneğim için "AllUsers" araması için bir sorgu nesnesi oluşturacağım. İşte arayüz:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
Sorgu Nesnesi Uygulaması
Burası, veri hızlandırıcıyı geliştirmeyi hızlandırmak için tekrar kullanabiliriz. Döndürülen veri kümesine (alanlara) bir ayar yapmaya izin verdiğime dikkat edin. Bu yapılan sorgu manipüle ile gitmek istiyorum kadarıyla ilgili. Unutmayın, sorgu nesnelerim sorgu oluşturucu değildir. Sadece belirli bir sorgu gerçekleştirirler. Ancak, bunu muhtemelen çok fazla kullanacağımı bildiğim için, birkaç farklı durumda, kendime alanları belirleme yeteneği veriyorum. Asla ihtiyacım olmayan alanları iade etmek istemiyorum!
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();
}
}
Denetleyiciye geçmeden önce, bunun ne kadar güçlü olduğunu göstermek için başka bir örnek göstermek istiyorum. Belki bir raporlama motorum var ve bunun için bir rapor oluşturmam gerekiyor AllOverdueAccounts
. Bu benim veri haritacım için zor olabilir ve ben SQL
bu durumda bazı gerçek yazmak isteyebilirsiniz . Sorun değil, işte bu sorgu nesnesi neye benzeyebilir:
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...";
}
}
Bu, bu rapor için tüm mantığımı tek bir sınıfta tutar ve test edilmesi kolaydır. Bunu kalbimin içeriğiyle alay edebilir, hatta tamamen farklı bir uygulama kullanabilirim.
Kontrol eden, denetleyici
Şimdi eğlenceli kısım — tüm parçaları bir araya getiriyor. Bağımlılık enjeksiyonu kullandığımı unutmayın. Tipik olarak bağımlılıklar yapıcıya enjekte edilir, ancak aslında onları doğrudan denetleyici yöntemlerime (rotalara) enjekte etmeyi tercih ederim. Bu, denetleyicinin nesne grafiğini en aza indirir ve aslında daha okunaklı buluyorum. Bu yaklaşımı beğenmediyseniz, yalnızca geleneksel yapıcı yöntemini kullanın.
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;
}
}
Son düşünceler:
Burada dikkat edilmesi gereken önemli noktalar, varlıkları değiştirirken (oluştururken, güncellerken veya silerken), gerçek model nesnelerle çalışıyorum ve depolarım aracılığıyla kalıcılığı gerçekleştiriyorum.
Ancak, (veri seçme ve görünümlere gönderme) görüntülerken model nesneleri ile değil, düz eski değer nesneleri ile çalışıyorum. Yalnızca ihtiyacım olan alanları seçiyorum ve veri arama performansımı en üst düzeye çıkarabileceğim şekilde tasarlandı.
Depolarım çok temiz kalıyor ve bunun yerine bu "karmaşa" model sorgularımda organize ediliyor.
Genel görevler için tekrarlayan SQL yazmak sadece saçma olduğundan, geliştirmeye yardımcı olması için bir veri eşleyici kullanıyorum. Ancak, gerektiğinde SQL yazabilirsiniz (karmaşık sorgular, raporlama vb.). Ve bunu yaptığınızda, düzgün bir şekilde adlandırılmış bir sınıfa sokulur.
Yaklaşımımı benimsediğini duymak isterim!
Temmuz 2015 Güncellemesi:
Tüm bunlarla sonuçlandığım yorumlarda bana sorulmuştur. Aslında o kadar uzakta değil. Doğrusu, hala depoları gerçekten sevmiyorum. Onları temel aramalar için aşırıya kaçmış buluyorum (özellikle zaten bir ORM kullanıyorsanız) ve daha karmaşık sorgularla çalışırken dağınık buluyorum.
Genellikle ActiveRecord tarzı ORM ile çalışıyorum, bu yüzden çoğu zaman bu modelleri doğrudan uygulamam boyunca referans göstereceğim. Ancak, daha karmaşık sorgularım olduğu durumlarda, bunları daha yeniden kullanılabilir hale getirmek için sorgu nesnelerini kullanacağım. Ayrıca, modellerimi her zaman yöntemlerime enjekte ettiğimi ve testlerimde alay etmelerini kolaylaştıracağımı not etmeliyim.