Jangan khawatir tentang prinsip tanggung jawab tunggal. Ini tidak akan membantu Anda membuat keputusan yang baik di sini karena Anda dapat secara subyektif memilih konsep tertentu sebagai "tanggung jawab." Anda bisa mengatakan tanggung jawab kelas adalah mengelola kegigihan data ke database, atau Anda bisa mengatakan tanggung jawabnya adalah untuk melakukan semua pekerjaan yang terkait dengan menciptakan pengguna. Ini hanya level yang berbeda dari perilaku aplikasi, dan keduanya merupakan ekspresi konseptual yang valid dari "tanggung jawab tunggal." Jadi prinsip ini tidak membantu untuk menyelesaikan masalah Anda.
Prinsip yang paling berguna untuk diterapkan dalam kasus ini adalah prinsip yang paling tidak mengejutkan . Jadi mari kita ajukan pertanyaan: apakah mengherankan bahwa repositori dengan peran utama mempertahankan data ke database juga mengirim email?
Ya, itu sangat mengejutkan. Ini adalah dua sistem eksternal yang sepenuhnya terpisah, dan namanya SaveChanges
tidak menyiratkan juga mengirim pemberitahuan. Fakta bahwa Anda mendelegasikan hal ini ke suatu acara membuat perilaku tersebut bahkan lebih mengejutkan, karena seseorang yang membaca kode tidak lagi dapat dengan mudah melihat perilaku tambahan apa yang muncul. Ketidaksadaran merusak keterbacaan. Terkadang, manfaatnya sebanding dengan biaya keterbacaan, tetapi tidak ketika Anda secara otomatis menggunakan sistem eksternal tambahan yang memiliki efek yang dapat diamati oleh pengguna akhir. (Logging dapat dikecualikan di sini karena efeknya pada dasarnya adalah pencatatan untuk tujuan debugging. Pengguna akhir tidak mengkonsumsi log, sehingga tidak ada salahnya selalu login.) Lebih buruk lagi, ini mengurangi fleksibilitas dalam waktu mengirim email, sehingga tidak memungkinkan untuk melakukan interleave operasi lain antara save dan notifikasi.
Jika kode Anda biasanya perlu mengirim pemberitahuan ketika pengguna berhasil dibuat, Anda dapat membuat metode yang melakukannya:
public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
repo.Add(user);
repo.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
Tetapi apakah ini menambah nilai tergantung pada spesifikasi aplikasi Anda.
Saya sebenarnya tidak menyarankan keberadaan SaveChanges
metode ini sama sekali. Metode ini mungkin akan melakukan transaksi database, tetapi repositori lain mungkin telah memodifikasi database dalam transaksi yang sama . Fakta bahwa semua itu dilakukan, sekali lagi mengejutkan, karena SaveChanges
secara spesifik terkait dengan repositori pengguna ini.
Pola paling mudah untuk mengelola transaksi basis data adalah using
blok luar :
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
context.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
Ini memberi programmer kontrol eksplisit atas ketika perubahan untuk semua repositori disimpan, memaksa kode untuk secara eksplisit mendokumentasikan urutan peristiwa yang harus terjadi sebelum komit, memastikan rollback dikeluarkan karena kesalahan (dengan asumsi bahwa DataContext.Dispose
mengeluarkan rollback), dan menghindari disembunyikan koneksi antara kelas stateful.
Saya juga lebih suka tidak mengirim email langsung dalam permintaan. Akan lebih kuat untuk mencatat perlunya pemberitahuan dalam antrian. Ini akan memungkinkan penanganan kegagalan yang lebih baik. Secara khusus, jika terjadi kesalahan dalam mengirim email, itu dapat dicoba lagi nanti tanpa mengganggu menyelamatkan pengguna, dan menghindari kasus di mana pengguna dibuat tetapi kesalahan dikembalikan oleh situs.
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
_emailNotificationQueue.AddUserCreateNotification(user);
_emailNotificationQueue.Commit();
context.SaveChanges();
}
Lebih baik untuk melakukan antrian pemberitahuan terlebih dahulu karena konsumen antrian dapat memverifikasi bahwa pengguna ada sebelum mengirim e-mail, jika context.SaveChanges()
panggilan gagal. (Kalau tidak, Anda akan memerlukan strategi komitmen dua fase penuh untuk menghindari Heisenbugs.)
Intinya adalah bersikap praktis. Sebenarnya pikirkan konsekuensi (baik dari segi risiko dan manfaat) dari menulis kode dengan cara tertentu. Saya menemukan bahwa "prinsip tanggung jawab tunggal" tidak sering membantu saya melakukan itu, sementara "prinsip kejutan paling tidak" sering membantu saya masuk ke kepala pengembang lain (untuk berbicara) dan berpikir tentang apa yang mungkin terjadi.