SRP menyatakan, tanpa syarat yang tidak pasti, bahwa suatu kelas seharusnya hanya memiliki satu alasan untuk berubah.
Mendekonstruksi kelas "laporan" dalam pertanyaan, ia memiliki tiga metode:
printReport
getReportData
formatReport
Mengabaikan pemborosan Report
yang digunakan dalam setiap metode, mudah untuk melihat mengapa ini melanggar SRP:
Istilah "cetak" menyiratkan semacam UI, atau printer yang sebenarnya. Kelas ini karenanya mengandung sejumlah UI atau logika presentasi. Perubahan pada persyaratan UI akan mengharuskan perubahan ke Report
kelas.
Istilah "data" menyiratkan struktur data dari beberapa jenis, tetapi tidak benar-benar menentukan apa (XML? JSON? CSV?). Apapun, jika "isi" dari laporan tersebut pernah berubah, maka metode ini juga akan berubah. Ada sambungan ke database atau domain.
formatReport
hanya nama yang mengerikan untuk suatu metode secara umum, tetapi saya berasumsi dengan melihatnya bahwa sekali lagi ada hubungannya dengan UI, dan mungkin aspek yang berbeda dari UI daripada printReport
. Jadi, alasan lain yang tidak terkait untuk berubah.
Jadi kelas yang satu ini mungkin digabungkan dengan database, perangkat layar / printer, dan beberapa logika format internal untuk log atau output file atau yang lainnya. Dengan memiliki ketiga fungsi dalam satu kelas, Anda mengalikan jumlah dependensi dan melipattigakan probabilitas bahwa setiap dependensi atau perubahan persyaratan akan memecah kelas ini (atau sesuatu yang bergantung padanya).
Bagian dari masalah di sini adalah bahwa Anda telah mengambil contoh yang sangat sulit. Anda mungkin tidak boleh memiliki kelas yang dipanggil Report
, bahkan jika itu hanya melakukan satu hal , karena ... laporan apa ? Bukankah semua "laporan" binatang yang sama sekali berbeda, berdasarkan data dan persyaratan yang berbeda? Dan bukankah laporan sesuatu yang sudah diformat, baik untuk layar atau untuk cetak?
Tapi, melihat masa lalu itu, dan membuat nama konkret hipotetis - sebut saja IncomeStatement
(satu laporan yang sangat umum) - arsitektur "SRPed" yang tepat akan memiliki tiga jenis:
IncomeStatement
- domain dan / atau kelas model yang berisi dan / atau menghitung informasi yang muncul pada laporan yang diformat.
IncomeStatementPrinter
, yang mungkin akan mengimplementasikan beberapa antarmuka standar seperti IPrintable<T>
. Memiliki satu metode utama Print(IncomeStatement)
,, dan mungkin beberapa metode atau properti lain untuk mengonfigurasi pengaturan khusus-cetak.
IncomeStatementRenderer
, yang menangani rendering layar dan sangat mirip dengan kelas printer.
Anda juga pada akhirnya dapat menambahkan lebih banyak kelas khusus fitur seperti IncomeStatementExporter
/ IExportable<TReport, TFormat>
.
Ini menjadi jauh lebih mudah dalam bahasa modern dengan diperkenalkannya obat generik dan wadah IoC. Sebagian besar kode aplikasi Anda tidak perlu bergantung pada IncomeStatementPrinter
kelas tertentu , itu dapat menggunakan IPrintable<T>
dan dengan demikian beroperasi pada segala jenis laporan yang dapat dicetak, yang memberi Anda semua manfaat yang dirasakan dari Report
kelas dasar dengan print
metode dan tidak ada pelanggaran SRP yang biasa . Implementasi aktual hanya perlu dinyatakan sekali, dalam registrasi wadah IoC.
Beberapa orang, ketika dihadapkan dengan desain di atas, merespons dengan sesuatu seperti: "tetapi ini terlihat seperti kode prosedural, dan seluruh poin OOP adalah untuk membuat kita - melarikan diri - dari pemisahan data dan perilaku!" Yang saya katakan: salah .
The IncomeStatement
adalah tidak hanya "data", dan kesalahan tersebut adalah apa yang menyebabkan banyak orang OOP merasa mereka melakukan sesuatu yang salah dengan menciptakan kelas seperti "transparan" dan kemudian mulai nge-jam semua jenis fungsi yang tidak terkait ke dalam IncomeStatement
(baik, yang dan kemalasan umum). Kelas ini dapat dimulai hanya sebagai data tetapi, seiring waktu, dijamin, akan berakhir sebagai lebih banyak model .
Misalnya, laporan laba rugi riil memiliki total pendapatan , total pengeluaran , dan garis laba bersih . Sistem keuangan yang dirancang dengan baik kemungkinan besar tidak akan menyimpan ini karena mereka bukan data transaksional - pada kenyataannya, mereka berubah berdasarkan pada penambahan data transaksional baru. Namun, perhitungan baris-baris ini akan selalu sama persis, tidak peduli apakah Anda mencetak, merender, atau mengekspor laporan. Jadi Anda IncomeStatement
kelas akan memiliki cukup banyak perilaku untuk itu dalam bentuk getTotalRevenues()
, getTotalExpenses()
dan getNetIncome()
metode, dan mungkin beberapa orang lain. Ini adalah objek bergaya OOP asli dengan perilakunya sendiri, meskipun tampaknya tidak terlalu "melakukan".
Namun format
dan print
metode, mereka tidak ada hubungannya dengan informasi itu sendiri. Bahkan, tidak terlalu tidak mungkin Anda ingin memiliki beberapa implementasi metode ini, misalnya pernyataan terperinci untuk manajemen dan pernyataan tidak terlalu rinci untuk pemegang saham. Memisahkan fungsi-fungsi independen ini ke dalam kelas yang berbeda memberi Anda kemampuan untuk memilih implementasi yang berbeda saat runtime tanpa beban metode satu ukuran cocok untuk semua print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)
. Huek!
Mudah-mudahan Anda dapat melihat di mana metode di atas, parameter-masif besar-besaran salah, dan di mana implementasi yang terpisah berjalan benar; dalam kasus objek tunggal, setiap kali Anda menambahkan kerutan baru ke logika pencetakan, Anda harus mengubah model domain Anda ( Tim di keuangan menginginkan nomor halaman, tetapi hanya pada laporan internal, dapatkah Anda menambahkan itu? ) sebagai lawan dari hanya menambahkan properti konfigurasi ke satu atau dua kelas satelit saja.
Menerapkan SRP dengan benar adalah tentang mengelola dependensi . Singkatnya, jika suatu kelas sudah melakukan sesuatu yang bermanfaat, dan Anda sedang mempertimbangkan menambahkan metode lain yang akan memperkenalkan ketergantungan baru (seperti UI, printer, jaringan, file, apa pun), jangan . Pikirkan bagaimana Anda bisa menambahkan fungsi ini di kelas baru , dan bagaimana Anda bisa membuat kelas baru ini sesuai dengan keseluruhan arsitektur Anda (cukup mudah ketika Anda mendesain injeksi ketergantungan). Itu adalah prinsip / proses umum.
Catatan: Seperti Robert, saya dengan terang-terangan menolak anggapan bahwa kelas yang mematuhi SRP seharusnya hanya memiliki satu atau dua variabel keadaan. Pembungkus tipis semacam itu jarang bisa diharapkan melakukan sesuatu yang benar-benar bermanfaat. Jadi jangan berlebihan dengan ini.