Validasi dan otorisasi dalam arsitektur berlapis

13

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):

  1. Jumlah penurunan uang tunai harus lebih besar dari nol.
  2. Setetes tunai harus memiliki Driver yang valid.
  3. 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.

SonOfPirate
sumber
SE bukan platform yang tepat untuk "mendorong diskusi teoretis dan subyektif". Voting untuk ditutup.
tammers
Pernyataan dengan kata-kata yang buruk. Saya benar-benar mencari praktik terbaik.
SonOfPirate
2
@tdammers - Ya itu adalah tempat yang tepat. Setidaknya itu yang diinginkan. Dari FAQ: 'Pertanyaan subyektif diizinkan.' Itu sebabnya mereka membuat situs ini bukannya Stack Overflow. Jangan menjadi Nazi Dekat. Jika pertanyaannya menyebalkan, itu akan memudar menjadi ketidakjelasan.
FastAl
@FastAI: Ini bukan bagian 'subyektif', tapi 'diskusi' yang menggangguku.
tammers
Saya pikir Anda dapat memanfaatkan objek nilai di sini dengan memiliki CashDropAmountobjek nilai daripada menggunakan a Decimal. 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 di Approver approver = approverService.findById(employeeId)mana ia dilemparkan jika karyawan tidak dalam peran pemberi persetujuan. Approverhanya akan menjadi objek nilai, bukan entitas. Anda juga bisa menyingkirkan pabrik atau penggunaan metode pabrik Anda pada AR sebagai gantinya: cashDrop = driver.dropCash(...).
plalx

Jawaban:

2

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.

jpm70
sumber
1

Aturan bisnis pertama Anda

Jumlah penurunan uang tunai harus lebih besar dari nol.

Sepertinya invarian CashDropentitas Anda dan AddCashDropCommandkelas Anda . Ada beberapa cara yang saya gunakan untuk memberlakukan invarian seperti ini:

  1. Ambil rute Design By Contract dan gunakan Kontrak Code dengan kombinasi Prasyarat, Postkondisi dan [ContractInvariantMethod] tergantung pada kasus Anda.
  2. Tulis kode eksplisit di konstruktor / setter yang melempar ArgumentException jika Anda memasukkan jumlah yang kurang dari 0.

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 Drivercontoh dari Anda IEmployeeRepository, seperti yang Anda lakukan dalam location 4contoh 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

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

Masalah di sini mungkin yang CashDropServicetidak bisa dicegat karena tidak ada antarmuka / kelas dasar. Atau AddCashDropCommandHandlertidak 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.

RobertMS
sumber
Bisakah Anda menjelaskan bagaimana saya akan "menggunakan wadah IoC Anda untuk menyuntikkan perilaku otorisasi ini di tempat yang seharusnya"? Ini kedengarannya menarik tetapi membuat AOP dan IOC bekerja bersama jauh dari saya sejauh ini.
SonOfPirate
Sedangkan sisanya, saya setuju dengan menempatkan validasi di konstruktor dan / atau setter untuk mencegah objek memasuki keadaan yang tidak valid (menangani invarian). Tetapi di luar itu dan referensi ke cek nol setelah pergi ke IEmployeeRepository untuk mencari driver, Anda tidak memberikan rincian di mana Anda akan melakukan sisa validasi. Mengingat penggunaan FluentValidation dan penggunaan kembali, dll yang disediakannya, di mana Anda akan menerapkan aturan dalam model yang diberikan?
SonOfPirate
Saya telah mengedit jawaban saya - lihat apakah ini membantu. Adapun "di mana Anda akan menerapkan aturan dalam model yang diberikan?"; mungkin sekitar 4, 5, 6, 7 di penangan perintah Anda. Anda memiliki akses ke repositori yang dapat menghasilkan informasi yang Anda butuhkan untuk melakukan validasi tingkat bisnis. Tapi saya pikir ada orang lain yang tidak setuju dengan saya di sini.
RobertMS
Untuk memperjelas, semua dependensi sedang disuntikkan. Saya tinggalkan itu untuk menjaga kode referensi singkat. Pertanyaan saya lebih berkaitan dengan memiliki ketergantungan dalam aspek karena aspek tidak disuntikkan melalui wadah. Jadi, bagaimana AuthorizationAspect mendapatkan referensi ke Layanan Authorization, misalnya?
SonOfPirate
1

Untuk aturan:

1- Jumlah penurunan uang tunai harus lebih besar dari nol.

2- Setoran tunai harus memiliki Driver yang valid.

3 - Pengguna saat ini harus berwenang untuk menambahkan tetes uang tunai (pengguna saat ini bukan pengemudi).

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!

Tidak mungkin
sumber
+1 untuk otorisasi - menaruhnya di UI adalah alternatif yang tidak saya sebutkan dalam jawaban saya.
RobertMS
Meskipun memiliki pemeriksaan otorisasi di UI memang memberikan pengalaman yang lebih interaktif bagi pengguna, saya mengembangkan API berbasis layanan dan tidak dapat membuat asumsi tentang aturan apa yang telah atau belum diterapkan oleh penelepon. Itu karena begitu banyak pemeriksaan ini dapat dengan mudah didelegasikan ke UI sehingga saya memilih untuk menggunakan proyek API sebagai dasar untuk posting. Saya mencari praktik terbaik daripada buku teks cepat dan mudah.
SonOfPirate
@SonOfPirate, INMO, UI perlu melakukan validasi karena lebih cepat dan memiliki lebih banyak data daripada layanan (dalam beberapa kasus). Sekarang layanan tidak boleh mengirim data di luar batas tanpa melakukan validasinya sendiri karena ini adalah bagian dari tanggung jawabnya selama Anda ingin layanan tidak mempercayai klien. Oleh karena itu, saya menyarankan agar pemeriksaan non-db dilakukan dalam layanan (lagi) sebelum mengirim data ke database untuk diproses lebih lanjut.
NoChance