Saya tahu Anda berpikir (atau mungkin berteriak), "bukan pertanyaan lain yang menanyakan di mana validasi berada dalam arsitektur berlapis?!?" Ya, tapi mudah-mudahan ini akan menjadi sedikit berbeda dalam hal ini.
Saya sangat percaya bahwa validasi mengambil banyak bentuk, berbasis konteks dan bervariasi di setiap tingkat arsitektur. Itu adalah dasar untuk pos - membantu mengidentifikasi jenis validasi apa yang harus dilakukan di setiap lapisan. Selain itu, pertanyaan yang sering muncul adalah di mana cek otorisasi berada.
Skenario contoh berasal dari aplikasi untuk bisnis katering. Secara berkala di siang hari, pengemudi dapat menyerahkan ke kantor kelebihan uang tunai yang telah mereka kumpulkan saat membawa truk dari lokasi ke lokasi. Aplikasi ini memungkinkan pengguna untuk merekam 'setetes uang tunai' dengan mengumpulkan ID pengemudi, dan jumlahnya. Berikut beberapa kode kerangka untuk menggambarkan lapisan yang terlibat:
public class CashDropApi // This is in the Service Facade Layer
{
[WebInvoke(Method = "POST")]
public void AddCashDrop(NewCashDropContract contract)
{
// 1
Service.AddCashDrop(contract.Amount, contract.DriverId);
}
}
public class CashDropService // This is the Application Service in the Domain Layer
{
public void AddCashDrop(Decimal amount, Int32 driverId)
{
// 2
CommandBus.Send(new AddCashDropCommand(amount, driverId));
}
}
internal class AddCashDropCommand // This is a command object in Domain Layer
{
public AddCashDropCommand(Decimal amount, Int32 driverId)
{
// 3
Amount = amount;
DriverId = driverId;
}
public Decimal Amount { get; private set; }
public Int32 DriverId { get; private set; }
}
internal class AddCashDropCommandHandler : IHandle<AddCashDropCommand>
{
internal ICashDropFactory Factory { get; set; } // Set by IoC container
internal ICashDropRepository CashDrops { get; set; } // Set by IoC container
internal IEmployeeRepository Employees { get; set; } // Set by IoC container
public void Handle(AddCashDropCommand command)
{
// 4
var driver = Employees.GetById(command.DriverId);
// 5
var authorizedBy = CurrentUser as Employee;
// 6
var cashDrop = Factory.CreateCashDrop(command.Amount, driver, authorizedBy);
// 7
CashDrops.Add(cashDrop);
}
}
public class CashDropFactory
{
public CashDrop CreateCashDrop(Decimal amount, Employee driver, Employee authorizedBy)
{
// 8
return new CashDrop(amount, driver, authorizedBy, DateTime.Now);
}
}
public class CashDrop // The domain object (entity)
{
public CashDrop(Decimal amount, Employee driver, Employee authorizedBy, DateTime at)
{
// 9
...
}
}
public class CashDropRepository // The implementation is in the Data Access Layer
{
public void Add(CashDrop item)
{
// 10
...
}
}
Saya telah menunjukkan 10 lokasi tempat saya melihat pemeriksaan validasi ditempatkan dalam kode. Pertanyaan saya adalah pemeriksaan apa yang akan Anda lakukan, jika ada, berkinerja di masing-masing diberi aturan bisnis berikut (bersama dengan pemeriksaan standar untuk panjang, rentang, format, jenis, dll):
- Jumlah penurunan uang tunai harus lebih besar dari nol.
- Setetes tunai harus memiliki Driver yang valid.
- Pengguna saat ini harus diberi wewenang untuk menambahkan tetes uang tunai (pengguna saat ini bukan pengemudi).
Silakan bagikan pemikiran Anda, bagaimana Anda memiliki atau akan mendekati skenario ini dan alasan untuk pilihan Anda.
sumber
CashDropAmount
objek nilai daripada menggunakan aDecimal
. Memeriksa apakah driver ada atau tidak akan dilakukan dalam penangan perintah dan hal yang sama berlaku untuk aturan otorisasi. Anda bisa mendapatkan otorisasi secara gratis dengan melakukan sesuatu seperti diApprover approver = approverService.findById(employeeId)
mana ia dilemparkan jika karyawan tidak dalam peran pemberi persetujuan.Approver
hanya akan menjadi objek nilai, bukan entitas. Anda juga bisa menyingkirkan pabrik atau penggunaan metode pabrik Anda pada AR sebagai gantinya:cashDrop = driver.dropCash(...)
.Jawaban:
Saya setuju bahwa apa yang Anda validasi akan berbeda di setiap lapisan aplikasi. Saya biasanya hanya memvalidasi apa yang diperlukan untuk mengeksekusi kode dalam metode saat ini. Saya mencoba memperlakukan komponen yang mendasarinya sebagai kotak hitam dan tidak memvalidasi berdasarkan bagaimana komponen-komponen tersebut diimplementasikan.
Jadi, sebagai contoh, di kelas CashDropApi Anda, saya hanya akan memverifikasi bahwa 'kontrak' bukan nol. Ini mencegah NullReferenceExceptions dan semua yang diperlukan untuk memastikan metode ini dijalankan dengan benar.
Saya tidak tahu bahwa saya akan memvalidasi apa pun di kelas layanan atau perintah dan pawang hanya akan memverifikasi bahwa 'perintah' tidak nol karena alasan yang sama seperti di kelas CashDropApi. Saya telah melihat (dan melakukan) validasi kedua cara wrt ke kelas pabrik dan entitas. Satu atau yang lain adalah tempat Anda ingin memvalidasi nilai 'jumlah' dan bahwa parameter lainnya tidak nol (aturan bisnis Anda).
Repositori seharusnya hanya memvalidasi bahwa data yang terkandung dalam objek konsisten dengan skema yang ditentukan dalam database Anda dan operasi daa akan berhasil. Misalnya, jika Anda memiliki kolom yang tidak boleh nol atau memiliki panjang maks, dll.
Sedangkan untuk pemeriksaan keamanan, saya pikir itu benar-benar masalah niat. Karena aturan ini dimaksudkan untuk mencegah akses yang tidak sah, saya ingin melakukan pemeriksaan ini sedini mungkin untuk mengurangi jumlah langkah yang tidak perlu yang saya ambil jika pengguna tidak diotorisasi. Saya mungkin akan memasukkannya ke dalam CashDropApi.
sumber
Aturan bisnis pertama Anda
Sepertinya invarian
CashDrop
entitas Anda danAddCashDropCommand
kelas Anda . Ada beberapa cara yang saya gunakan untuk memberlakukan invarian seperti ini:Aturan kedua Anda pada dasarnya lebih luas (mengingat rincian dalam pertanyaan): apakah valid berarti entitas Pengemudi memiliki bendera yang menunjukkan bahwa mereka dapat mengemudi (yaitu tidak memiliki SIM mereka ditangguhkan), apakah itu berarti bahwa pengemudi itu sebenarnya bekerja hari itu atau apakah itu hanya berarti bahwa driverId, yang diteruskan ke CashDropApi, valid di toko persistensi.
Dalam setiap kasus ini, Anda harus menavigasi model domain Anda dan mendapatkan
Driver
contoh dari AndaIEmployeeRepository
, seperti yang Anda lakukan dalamlocation 4
contoh kode Anda. Jadi, di sini Anda perlu memastikan bahwa panggilan ke repositori tidak mengembalikan nol, dalam hal ini driverId Anda tidak valid dan Anda tidak dapat melanjutkan proses lebih lanjut.Untuk 2 lainnya (hipotesis saya) cek (apakah pengemudi memiliki SIM yang berlaku, adalah pengemudi yang bekerja hari ini) Anda menjalankan aturan bisnis.
Apa yang saya cenderung lakukan di sini adalah menggunakan koleksi kelas validator yang beroperasi pada entitas (seperti pola spesifikasi dari buku Eric Evans - Domain Driven Design). Saya telah menggunakan FluentValidation untuk membuat aturan dan validator ini. Saya kemudian dapat menyusun (dan karenanya menggunakan kembali) aturan yang lebih kompleks / lebih lengkap dari aturan yang lebih sederhana. Dan saya dapat memutuskan lapisan mana dalam arsitektur saya untuk menjalankannya. Tetapi saya memiliki mereka semua disandikan di satu tempat, tidak tersebar di seluruh sistem.
Aturan ketiga Anda terkait dengan masalah lintas sektoral: otorisasi. Karena Anda sudah menggunakan wadah IoC (dengan asumsi bahwa wadah IoC Anda mendukung intersepsi metode), Anda dapat melakukan beberapa AOP . Tulis apsect yang melakukan otorisasi dan Anda dapat menggunakan wadah IoC Anda untuk menyuntikkan perilaku otorisasi ini di tempat yang seharusnya. Kemenangan besar di sini adalah Anda telah menulis logika sekali, tetapi Anda dapat menggunakannya kembali di seluruh sistem Anda.
Untuk menggunakan intersepsi melalui proxy dinamis (Castle Windsor, Spring.NET, Ninject 3.0, dll) kelas target Anda perlu mengimplementasikan antarmuka atau mewarisi dari kelas dasar. Anda akan mencegat sebelum panggilan ke metode target, memeriksa otorisasi pengguna dan mencegah panggilan dari melanjutkan ke metode yang sebenarnya (membuang pengecualian, mencatat, mengembalikan nilai yang menunjukkan kegagalan, atau sesuatu yang lain) jika pengguna tidak memiliki peran yang tepat untuk melakukan operasi.
Dalam kasus Anda, Anda bisa mencegat panggilan ke salah satu
Masalah di sini mungkin yang
CashDropService
tidak bisa dicegat karena tidak ada antarmuka / kelas dasar. AtauAddCashDropCommandHandler
tidak sedang dibuat oleh IoC Anda, oleh karena itu IoC Anda tidak dapat membuat proxy dinamis untuk mencegat panggilan. Spring.NET memiliki fitur yang berguna di mana Anda dapat menargetkan metode pada kelas dalam suatu perakitan melalui regex, jadi ini mungkin berhasil.Semoga ini memberi Anda beberapa ide.
sumber
Untuk aturan:
Saya akan melakukan validasi di lokasi (1) untuk aturan bisnis (1) dan memastikan Id tidak nol atau negatif (dengan asumsi nol valid) sebagai pra-periksa aturan (2). Alasannya adalah aturan saya "Jangan melintasi batas layer dengan data yang salah yang dapat Anda periksa dengan informasi yang tersedia". Pengecualian untuk hal ini adalah jika layanan melakukan validasi sebagai bagian dari tugasnya kepada penelepon lain. Dalam hal ini, itu akan cukup untuk memiliki validasi hanya di sana.
Untuk aturan (2) dan (3), ini harus dilakukan pada lapisan akses basis data (atau lapisan db itu sendiri) hanya karena melibatkan akses db. Tidak perlu melakukan perjalanan antar lapisan dengan sengaja.
Dalam aturan tertentu (3) dapat dihindari jika kita membiarkan GUI mencegah pengguna yang tidak sah menekan tombol yang mengaktifkan skenario ini. Meskipun ini lebih sulit untuk dikodekan, lebih baik.
Pertanyaan bagus!
sumber