Prinsip Tanggung Jawab Tunggal: Resep untuk Kode Hebat
Diterbitkan: 2022-03-11Terlepas dari apa yang kami anggap sebagai kode yang hebat, itu selalu membutuhkan satu kualitas sederhana: kode harus dapat dipelihara. Indentasi yang tepat, nama variabel yang rapi, cakupan pengujian 100%, dan sebagainya hanya dapat membawa Anda sejauh ini. Kode apa pun yang tidak dapat dipelihara dan tidak dapat beradaptasi dengan perubahan persyaratan dengan relatif mudah adalah kode yang menunggu untuk menjadi usang. Kita mungkin tidak perlu menulis kode yang bagus saat mencoba membangun prototipe, bukti konsep, atau produk minimum yang layak, tetapi dalam semua kasus lain, kita harus selalu menulis kode yang dapat dipelihara. Ini adalah sesuatu yang harus dianggap sebagai kualitas mendasar dari rekayasa dan desain perangkat lunak.
Dalam artikel ini, saya akan membahas bagaimana Prinsip Tanggung Jawab Tunggal dan beberapa teknik yang berputar di sekitarnya dapat memberikan kode Anda kualitas yang sangat baik. Menulis kode yang hebat adalah sebuah seni, tetapi beberapa prinsip selalu dapat membantu pengembangan Anda ke arah yang dibutuhkan untuk menghasilkan perangkat lunak yang kuat dan dapat dipelihara.
Model Adalah Segalanya
Hampir setiap buku tentang beberapa kerangka kerja MVC (MVP, MVVM, atau M**) baru dipenuhi dengan contoh kode yang buruk. Contoh-contoh ini mencoba menunjukkan apa yang ditawarkan kerangka kerja. Tetapi mereka juga akhirnya memberikan saran yang buruk untuk pemula. Contoh seperti "katakanlah kita memiliki ORM X ini untuk model kita, mesin templating Y untuk tampilan kita dan kita akan memiliki pengontrol untuk mengelola semuanya" tidak menghasilkan apa-apa selain pengontrol yang sangat besar.
Meskipun untuk membela buku-buku ini, contoh-contoh dimaksudkan untuk menunjukkan kemudahan di mana Anda dapat memulai dengan kerangka kerja mereka. Mereka tidak dimaksudkan untuk mengajarkan desain perangkat lunak. Tetapi pembaca yang mengikuti contoh-contoh ini menyadari, hanya setelah bertahun-tahun, betapa kontraproduktifnya memiliki potongan kode yang monolitik dalam proyek mereka.
Model adalah jantung dari aplikasi Anda. Jika Anda memiliki model yang terpisah dari logika aplikasi lainnya, pemeliharaan akan jauh lebih mudah, terlepas dari betapa rumitnya aplikasi Anda. Bahkan untuk aplikasi yang rumit, implementasi model yang baik dapat menghasilkan kode yang sangat ekspresif. Dan untuk mencapainya, mulailah dengan memastikan bahwa model Anda hanya melakukan apa yang seharusnya mereka lakukan, dan tidak peduli dengan apa yang dilakukan aplikasi yang dibangun di sekitarnya. Lebih jauh lagi, itu tidak memperhatikan apa yang mendasari lapisan penyimpanan data: apakah aplikasi Anda bergantung pada database SQL, atau apakah itu menyimpan semuanya dalam file teks?
Saat kami melanjutkan artikel ini, Anda akan menyadari betapa hebatnya kode tentang pemisahan perhatian.
Prinsip Tanggung Jawab Tunggal
Anda mungkin pernah mendengar tentang prinsip SOLID: tanggung jawab tunggal, buka-tutup, substitusi liskov, segregasi antarmuka, dan inversi ketergantungan. Huruf pertama, S, mewakili Prinsip Tanggung Jawab Tunggal (SRP) dan pentingnya tidak dapat dilebih-lebihkan. Saya bahkan berpendapat bahwa itu adalah kondisi yang diperlukan dan cukup untuk kode yang baik. Faktanya, dalam kode apa pun yang ditulis dengan buruk, Anda selalu dapat menemukan kelas yang memiliki lebih dari satu tanggung jawab - form1.cs atau index.php yang berisi beberapa ribu baris kode bukanlah sesuatu yang jarang ditemukan dan kita semua mungkin pernah melihat atau melakukannya.
Mari kita lihat contoh di C# (ASP.NET MVC dan Entity framework). Bahkan jika Anda bukan pengembang C#, dengan beberapa pengalaman OOP Anda akan dapat mengikuti dengan mudah.
public class OrderController { ... public ActionResult CreateForm() { /* * View data preparations */ return View(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } using (var context = new DataContext()) { var order = new Order(); // Create order from request context.Orders.Add(order); // Reserve ordered goods …(Huge logic here)... context.SaveChanges(); //Send email with order details for customer } return RedirectToAction("Index"); } ... (many more methods like Create here) }
Ini adalah kelas OrderController biasa, metode Create-nya ditampilkan. Pada controller seperti ini, saya sering melihat kasus dimana class Order sendiri digunakan sebagai parameter request. Tapi saya lebih suka menggunakan kelas permintaan khusus. Sekali lagi, SRP!
Perhatikan dalam potongan kode di atas bagaimana controller tahu terlalu banyak tentang "menempatkan pesanan", termasuk tetapi tidak terbatas pada menyimpan objek Order, mengirim email, dll. Itu terlalu banyak pekerjaan untuk satu kelas. Untuk setiap perubahan kecil, pengembang perlu mengubah seluruh kode pengontrol. Dan untuk berjaga-jaga jika Pengendali lain juga perlu membuat pesanan, lebih sering daripada tidak, pengembang akan menggunakan salin-tempel kode. Pengontrol seharusnya hanya mengontrol keseluruhan proses, dan tidak benar-benar menampung setiap bit logika proses.
Tapi hari ini adalah hari dimana kita berhenti menulis controller yang sangat besar ini!
Mari kita ekstrak semua logika bisnis dari controller dan pindahkan ke kelas OrderService:
public class OrderService { public void Create(OrderCreateRequest request) { // all actions for order creating here } } public class OrderController { public OrderController() { this.service = new OrderService(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } this.service.Create(request); return RedirectToAction("Index"); }
Dengan ini dilakukan, pengontrol sekarang hanya melakukan apa yang dimaksudkan untuk dilakukan: mengontrol proses. Ia hanya tahu tentang tampilan, kelas OrderService dan OrderRequest - kumpulan informasi paling sedikit yang diperlukan untuk melakukan tugasnya, yaitu mengelola permintaan dan mengirim tanggapan.
Dengan cara ini Anda akan jarang mengubah kode pengontrol. Komponen lain seperti tampilan, objek permintaan, dan layanan masih dapat berubah karena terkait dengan persyaratan bisnis, tetapi bukan pengontrol.
Inilah yang dimaksud dengan SRP, dan ada banyak teknik untuk menulis kode yang memenuhi prinsip ini. Salah satu contohnya adalah injeksi ketergantungan (sesuatu yang juga berguna untuk menulis kode yang dapat diuji).
Injeksi Ketergantungan
Sulit membayangkan proyek besar berdasarkan Prinsip Tanggung Jawab Tunggal tanpa Injeksi Ketergantungan. Mari kita lihat kelas OrderService kita lagi:
public class OrderService { public void Create(...) { // Creating the order(and let's forget about reserving here, it's not important for following examples) // Sending an email to client with order details var smtp = new SMTP(); // Setting smtp.Host, UserName, Password and other parameters smtp.Send(); } }
Kode ini berfungsi, tetapi tidak cukup ideal. Untuk memahami cara kerja metode create class OrderService, mereka dipaksa untuk memahami seluk-beluk SMTP. Dan, sekali lagi, salin-tempel adalah satu-satunya jalan keluar untuk mereplikasi penggunaan SMTP ini di mana pun dibutuhkan. Tetapi dengan sedikit refactoring, itu bisa berubah:
public class OrderService { private SmtpMailer mailer; public OrderService() { this.mailer = new SmtpMailer(); } public void Create(...) { // Creating the order // Sending an email to client with order details this.mailer.Send(...); } } public class SmtpMailer { public void Send(string to, string subject, string body) { // SMTP stuff will be only here } }
Sudah jauh lebih baik! Tapi, kelas OrderService masih tahu banyak tentang pengiriman email. Itu membutuhkan kelas SmtpMailer yang tepat untuk mengirim email. Bagaimana jika kita ingin mengubahnya di masa depan? Bagaimana jika kita ingin mencetak konten email yang dikirim ke file log khusus alih-alih benar-benar mengirimkannya di lingkungan pengembangan kita? Bagaimana jika kita ingin menguji unit kelas OrderService kita? Mari kita lanjutkan dengan refactoring dengan membuat antarmuka IMailer:

public interface IMailer { void Send(string to, string subject, string body); }
SmtpMailer akan mengimplementasikan antarmuka ini. Selain itu, aplikasi kita akan menggunakan penampung IoC dan kita dapat mengonfigurasinya agar IMailer diimplementasikan oleh kelas SmtpMailer. OrderService kemudian dapat diubah sebagai berikut:
public sealed class OrderService: IOrderService { private IOrderRepository repository; private IMailer mailer; public OrderService(IOrderRepository repository, IMailer mailer) { this.repository = repository; this.mailer = mailer; } public void Create(...) { var order = new Order(); // fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.) this.repository.Save(order); this.mailer.Send(<orders user email>, <subject>, <body with order details>); } }
Sekarang kita menuju suatu tempat! Saya mengambil kesempatan ini juga untuk membuat perubahan lain. OrderService sekarang bergantung pada antarmuka IOrderRepository untuk berinteraksi dengan komponen yang menyimpan semua pesanan kami. Itu tidak lagi peduli tentang bagaimana antarmuka itu diimplementasikan dan teknologi penyimpanan apa yang mendukungnya. Sekarang kelas OrderService hanya memiliki kode yang berhubungan dengan logika bisnis pesanan.
Dengan cara ini, jika penguji menemukan sesuatu yang berperilaku tidak benar dengan mengirim email, pengembang tahu persis di mana mencarinya: kelas SmtpMailer. Jika ada yang salah dengan diskon, pengembang, sekali lagi, tahu di mana mencarinya: OrderService (atau jika Anda telah mengingat SRP, maka itu mungkin kode kelas DiscountService).
Arsitektur Berbasis Acara
Namun, saya masih tidak menyukai metode OrderService.Create:
public void Create(...) { var order = new Order(); ... this.repository.Save(order); this.mailer.Send(<orders user email>, <subject>, <body with order details>); }
Mengirim email bukanlah bagian dari alur pembuatan pesanan utama. Meskipun aplikasi gagal mengirim email, pesanan tetap dibuat dengan benar. Juga, bayangkan situasi di mana Anda harus menambahkan opsi baru di area pengaturan pengguna yang memungkinkan mereka untuk tidak menerima email setelah berhasil melakukan pemesanan. Untuk memasukkan ini ke dalam kelas OrderService kami, kami perlu memperkenalkan dependensi, IUserParametersService. Tambahkan lokalisasi ke dalam campuran, dan Anda memiliki ketergantungan lain, ITranslator (untuk menghasilkan pesan email yang benar dalam bahasa pilihan pengguna). Beberapa dari tindakan ini tidak diperlukan, terutama gagasan untuk menambahkan banyak dependensi ini dan berakhir dengan konstruktor yang tidak muat di layar. Saya menemukan contoh yang bagus tentang ini di basis kode Magento (CMS e-niaga populer yang ditulis dalam PHP) di kelas yang memiliki 32 dependensi!
Terkadang sulit untuk mengetahui bagaimana memisahkan logika ini, dan kelas Magento mungkin adalah korban dari salah satu kasus tersebut. Itu sebabnya saya menyukai cara yang digerakkan oleh peristiwa:
namespace <base namespace>.Events { [Serializable] public class OrderCreated { private readonly Order order; public OrderCreated(Order order) { this.order = order; } public Order GetOrder() { return this.order; } } }
Setiap kali pesanan dibuat, alih-alih mengirim email langsung dari kelas OrderService, kelas acara khusus OrderCreated dibuat dan acara dibuat. Di suatu tempat di aplikasi event handler akan dikonfigurasi. Salah satunya akan mengirim email ke klien.
namespace <base namespace>.EventHandlers { public class OrderCreatedEmailSender : IEventHandler<OrderCreated> { public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator) { // this class depend on all stuff which it need to send an email. } public void Handle(OrderCreated event) { this.mailer.Send(...); } } }
Kelas OrderCreated ditandai sebagai Serializable dengan sengaja. Kami dapat menangani acara ini segera, atau menyimpannya secara serial dalam antrian (Redis, ActiveMQ atau yang lainnya) dan memprosesnya dalam proses/utas terpisah dari yang menangani permintaan web. Dalam artikel ini penulis menjelaskan secara rinci apa itu arsitektur yang digerakkan oleh peristiwa (harap tidak memperhatikan logika bisnis dalam OrderController).
Beberapa orang mungkin berpendapat bahwa sekarang sulit untuk memahami apa yang terjadi saat Anda membuat pesanan. Tapi itu tidak bisa lebih jauh dari kebenaran. Jika Anda merasa seperti itu, cukup manfaatkan fungsionalitas IDE Anda. Dengan menemukan semua penggunaan kelas OrderCreated di IDE, kita dapat melihat semua tindakan yang terkait dengan acara tersebut.
Tetapi kapan saya harus menggunakan Injeksi Ketergantungan dan kapan saya harus menggunakan pendekatan yang digerakkan oleh Peristiwa? Tidak selalu mudah untuk menjawab pertanyaan ini, tetapi satu aturan sederhana yang dapat membantu Anda adalah dengan menggunakan Injeksi Ketergantungan untuk semua aktivitas utama Anda dalam aplikasi, dan pendekatan berbasis peristiwa untuk semua tindakan sekunder. Misalnya, gunakan Dependecy Injection dengan hal-hal seperti membuat pesanan dalam kelas OrderService dengan IOrderRepository, dan mendelegasikan pengiriman email, sesuatu yang bukan merupakan bagian penting dari alur pembuatan pesanan utama, ke beberapa event handler.
Kesimpulan
Kami memulai dengan pengontrol yang sangat berat, hanya satu kelas, dan berakhir dengan kumpulan kelas yang rumit. Keuntungan dari perubahan ini cukup jelas dari contoh. Namun, masih banyak cara untuk memperbaiki contoh-contoh ini. Misalnya, metode OrderService.Create dapat dipindahkan ke kelasnya sendiri: OrderCreator. Karena pembuatan pesanan adalah unit logika bisnis independen yang mengikuti Prinsip Tanggung Jawab Tunggal, wajar saja jika ia memiliki kelasnya sendiri dengan rangkaian dependensinya sendiri. Demikian juga, penghapusan pesanan dan pembatalan pesanan masing-masing dapat diimplementasikan di kelas mereka sendiri.
Ketika saya menulis kode yang sangat digabungkan, sesuatu yang mirip dengan contoh pertama di artikel ini, setiap perubahan kecil pada persyaratan dapat dengan mudah menyebabkan banyak perubahan di bagian kode lainnya. SRP membantu pengembang menulis kode yang dipisahkan, di mana setiap kelas memiliki tugasnya sendiri. Jika spesifikasi pekerjaan ini berubah, pengembang membuat perubahan pada kelas tertentu saja. Perubahan cenderung tidak merusak seluruh aplikasi karena kelas lain masih harus melakukan pekerjaan mereka seperti sebelumnya, kecuali tentu saja mereka rusak di tempat pertama.
Mengembangkan kode di muka menggunakan teknik-teknik ini dan mengikuti Prinsip Tanggung Jawab Tunggal bisa tampak seperti tugas yang menakutkan, tetapi upaya tersebut pasti akan membuahkan hasil saat proyek tumbuh dan pengembangan berlanjut.