Apa cara praktis untuk menerapkan SRP?


11

Apa saja teknik praktis yang digunakan orang untuk memeriksa apakah suatu kelas melanggar prinsip tanggung jawab tunggal?

Saya tahu bahwa sebuah kelas seharusnya hanya memiliki satu alasan untuk berubah, tetapi kalimat itu agak kurang praktis untuk benar-benar mengimplementasikannya.

Satu-satunya cara yang saya temukan adalah dengan menggunakan kalimat "The ......... seharusnya ......... itu sendiri." di mana ruang pertama adalah nama kelas dan yang terakhir adalah nama metode (tanggung jawab).

Namun, terkadang sulit untuk mengetahui apakah suatu tanggung jawab benar-benar melanggar SRP.

Apakah ada lebih banyak cara untuk memeriksa SRP?

catatan:

Pertanyaannya bukan tentang apa arti SRP, melainkan metodologi praktis atau serangkaian langkah untuk memeriksa dan mengimplementasikan SRP.

MEMPERBARUI

Laporkan kelas

Saya telah menambahkan kelas sampel yang jelas-jelas melanggar SRP. Alangkah baiknya jika orang dapat menggunakannya sebagai contoh untuk menjelaskan bagaimana mereka mendekati prinsip tanggung jawab tunggal.

Contohnya dari sini .


Ini adalah aturan yang menarik, tetapi Anda masih bisa menulis: "Kelas Seseorang Dapat Membuatnya Sendiri". Ini dapat dianggap sebagai pelanggaran untuk SRP, karena termasuk GUI di kelas yang sama yang berisi aturan bisnis dan kegigihan data tidak OK. Jadi saya pikir Anda perlu menambahkan konsep domain arsitektur (tingkatan dan lapisan) dan memastikan bahwa pernyataan ini hanya berlaku untuk 1 domain saja (seperti GUI, Akses Data, dll.)
NoChance

@EmmadKareem Aturan ini disebutkan dalam Analisis dan Desain Berorientasi Objek Kepala Pertama dan itulah yang saya pikirkan tentang ini. Agak kurang cara praktis untuk mengimplementasikannya. Mereka menyebutkan bahwa kadang-kadang tanggung jawab tidak akan terlihat oleh perancang dan dia harus menggunakan banyak akal sehat untuk menilai apakah metode tersebut benar-benar ada di kelas ini atau tidak.
Songo

Jika Anda benar-benar ingin memahami SRP, bacalah beberapa tulisan Paman Bob Martin. Kodenya adalah beberapa yang tercantik yang pernah saya lihat, dan saya percaya bahwa apa pun yang dia katakan tentang SRP tidak hanya nasihat yang baik, tetapi juga lebih dari sekadar melambaikan tangan.
Robert Harvey

Dan apakah pemilih yang turun tolong jelaskan mengapa memperbaiki pos?
Songo

Jawaban:


7

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 Reportyang 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 Reportkelas.

  • 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.

  • formatReporthanya 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 IncomeStatementPrinterkelas 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 Reportkelas dasar dengan printmetode 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 IncomeStatementadalah 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 IncomeStatementkelas 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 formatdan printmetode, 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.


+1 memang jawaban yang bagus. Namun, saya hanya bingung tentang kelas IncomeStatement. Apakah desain yang diusulkan Anda berarti bahwa IncomeStatementakan memiliki contoh IncomeStatementPrinter& IncomeStatementRenderersehingga ketika saya sebut print()di IncomeStatementakan mendelegasikan panggilan untuk IncomeStatementPrinterbukan?
Songo

@Songo: Sama sekali tidak! Anda seharusnya tidak memiliki ketergantungan siklik jika Anda mengikuti SOLID. Ternyata jawaban saya tidak membuatnya cukup jelas bahwa IncomeStatementkelas tidak memiliki sebuah printmetode, atau formatmetode, atau cara lain yang tidak langsung berhubungan dengan memeriksa atau memanipulasi data laporan itu sendiri. Untuk itulah kelas-kelas itu diperuntukkan. Jika Anda ingin mencetaknya, maka Anda bergantung pada IPrintable<IncomeStatement>antarmuka yang terdaftar dalam wadah.
Aaronaught

aah aku mengerti maksudmu. Namun, di mana ketergantungan siklik jika saya menyuntikkan Printerinstance di IncomeStatementkelas? seperti yang saya bayangkan ketika saya menyebutnya IncomeStatement.print()akan mendelegasikannya IncomeStatementPrinter.print(this, format). Apa yang salah dengan pendekatan ini? ... Pertanyaan lain, Anda menyebutkan bahwa IncomeStatementharus berisi informasi yang muncul pada laporan yang diformat jika saya ingin itu dibaca dari database atau dari file XML, haruskah saya mengekstrak metode yang memuat data ke dalam kelas yang terpisah dan mendelegasikan panggilan ke dalamnya IncomeStatement?
Songo

@Songo: Anda telah IncomeStatementPrinterbergantung IncomeStatementdan IncomeStatementbergantung pada IncomeStatementPrinter. Itu ketergantungan siklik. Dan itu hanya desain yang buruk; tidak ada alasan sama sekali untuk IncomeStatementmengetahui apa-apa tentang Printeratau IncomeStatementPrinter- ini adalah model domain, tidak peduli dengan pencetakan, dan delegasi tidak ada gunanya karena kelas lain dapat membuat atau memperoleh IncomeStatementPrinter. Tidak ada alasan bagus untuk memiliki gagasan mencetak dalam model domain.
Aaronaught

Adapun cara Anda memuat IncomeStatementdari database (atau file XML) - biasanya, yang ditangani oleh repositori dan / atau mapper, bukan domain, dan sekali lagi, Anda tidak mendelegasikan ini dalam domain; jika beberapa kelas lain perlu membaca salah satu model ini maka ia meminta repositori itu secara eksplisit . Kecuali Anda menerapkan pola Rekaman Aktif, saya kira, tapi saya benar-benar bukan penggemar.
Aaronaught

2

Cara saya memeriksa SRP adalah memeriksa setiap metode (tanggung jawab) kelas dan mengajukan pertanyaan berikut:

"Apakah saya perlu mengubah cara saya mengimplementasikan fungsi ini?"

Jika saya menemukan fungsi yang harus saya terapkan dengan cara yang berbeda (tergantung pada beberapa jenis konfigurasi atau kondisi) maka saya tahu pasti bahwa saya memerlukan kelas tambahan untuk menangani tanggung jawab ini.


1

Berikut adalah kutipan dari aturan 8 dari Object Calisthenics :

Sebagian besar kelas seharusnya hanya bertanggung jawab untuk menangani satu variabel keadaan tunggal, tetapi ada beberapa yang akan membutuhkan dua. Menambahkan variabel instance baru ke kelas segera mengurangi kohesi kelas itu. Secara umum, saat memprogram di bawah aturan ini, Anda akan menemukan bahwa ada dua jenis kelas, kelas yang mempertahankan status variabel instance tunggal, dan kelas yang mengoordinasikan dua variabel terpisah. Secara umum, jangan mencampur dua jenis tanggung jawab

Dengan tampilan (agak idealistc) ini, Anda bisa mengatakan bahwa setiap kelas yang hanya berisi satu atau dua variabel status tidak mungkin melanggar SRP. Anda juga bisa mengatakan bahwa kelas apa pun yang berisi lebih dari dua variabel status dapat melanggar SRP.


2
Pandangan ini sangat sederhana. Bahkan persamaan Einstein yang terkenal, tetapi sederhana membutuhkan dua variabel.
Robert Harvey

Pertanyaan OP adalah "Apakah ada lebih banyak cara untuk memeriksa SRP?" - ini adalah salah satu indikator yang memungkinkan. Ya itu sederhana, dan tidak berlaku dalam setiap kasus, tetapi itu adalah salah satu cara yang mungkin untuk memeriksa apakah SRP telah dilanggar.
MattDavey

1
Saya menduga keadaan berubah-ubah vs tidak berubah juga merupakan pertimbangan penting
jk.

Aturan 8 menjelaskan proses sempurna untuk membuat desain yang memiliki ribuan dan ribuan kelas yang membuat sistem menjadi sangat rumit, tidak bisa dipahami, dan tidak dapat dipertahankan. Tetapi sisi positifnya adalah Anda harus mengikuti SRP.
Dunk

@Dunk Saya tidak setuju dengan Anda, tapi diskusi itu sepenuhnya di luar topik untuk pertanyaan.
MattDavey

1

Satu kemungkinan implementasi (di Jawa). Saya mengambil kebebasan dengan tipe yang dikembalikan, tetapi saya pikir semua itu menjawab pertanyaan. TBH Saya tidak berpikir antarmuka ke kelas Laporan seburuk itu, meskipun nama yang lebih baik mungkin dalam urutan. Saya meninggalkan pernyataan penjaga dan pernyataan singkat.

EDIT: Juga perhatikan bahwa kelas tidak dapat diubah. Jadi begitu itu dibuat Anda tidak dapat mengubah apa pun. Anda bisa menambahkan setFormatter () dan setPrinter () dan tidak terlalu banyak kesulitan. Kuncinya, IMHO, adalah untuk tidak mengubah data mentah setelah instantiation.

public class Report
{
    private ReportData data;
    private ReportDataDao dao;
    private ReportFormatter formatter;
    private ReportPrinter printer;


    /*
     *  Parameterized constructor for depndency injection, 
     *  there are better ways but this is explicit.
     */
    public Report(ReportDataDao dao, 
        ReportFormatter formatter, ReportPrinter printer)
    {
        super();
        this.dao = dao;
        this.formatter = formatter;
        this.printer = printer;
    }

    /*
     * Delegates to the injected printer.
     */
    public void printReport()
    {
        printer.print(formatReport());
    }


    /*
     * Lazy loading of data, delegates to the dao 
     * for the meat of the call.
     */
    public ReportData getReportData()
    {
        if (reportData == null)
        {
            reportData = dao.loadData();
        }
        return reportData;
    }

    /*
     * Delegate to the formatter for formatting 
     * (notice a pattern here).
     */
    public ReportData formatReport()
    {
        formatter.format(getReportData());
    }
}

Terima kasih untuk implementasinya. Saya punya 2 hal, sejalan if (reportData == null)saya kira maksud Anda datasebagai gantinya. Kedua, saya berharap tahu bagaimana Anda sampai pada implementasi ini. Seperti mengapa Anda memutuskan untuk mendelegasikan semua panggilan ke objek lain sebagai gantinya. Satu hal lagi yang selalu saya pikirkan, Apakah benar-benar tanggung jawab laporan untuk mencetak sendiri ?! Mengapa Anda tidak membuat printerkelas terpisah yang mengambil reportkonstruktornya?
Songo

Ya, reportData = data, maaf soal itu. Delegasi memungkinkan untuk kontrol dependensi yang halus. Saat runtime Anda dapat memberikan implementasi alternatif untuk setiap komponen. Sekarang Anda dapat memiliki HtmlPrinter, PdfPrinter, JsonPrinter, ... dll. Ini juga berguna untuk pengujian karena Anda dapat menguji komponen yang didelegasikan dalam isolasi serta terintegrasi dalam objek di atas. Anda tentu saja dapat membalikkan hubungan antara printer dan laporan, saya hanya ingin menunjukkan bahwa dimungkinkan untuk memberikan solusi dengan antarmuka kelas yang disediakan. Itu kebiasaan dari bekerja pada sistem warisan. :)
Heath Lilley

hmmmm ... Jadi jika Anda membangun sistem dari awal, opsi mana yang akan Anda ambil? Sebuah Printerkelas yang mengambil laporan atau Reportkelas yang mengambil printer? Saya telah mengalami masalah yang sama sebelumnya di mana saya harus mengurai laporan dan saya berdebat dengan TL saya jika kita harus membangun parser yang mengambil laporan atau apakah laporan harus memiliki parser di dalamnya dan parse()panggilan didelegasikan kepadanya.
Songo

Saya akan melakukan keduanya ... printer.print (report) untuk memulai dan report.print () jika diperlukan nanti. Hal yang hebat tentang pendekatan printer.print (laporan) adalah sangat dapat digunakan kembali. Ini memisahkan tanggung jawab dan memungkinkan Anda untuk memiliki metode kenyamanan di mana Anda membutuhkannya. Mungkin Anda tidak ingin benda lain di sistem Anda harus tahu tentang ReportPrinter, jadi dengan memiliki metode print () pada kelas Anda mencapai tingkat abstaksi yang melindungi logika pencetakan laporan Anda dari dunia luar. Ini masih memiliki vektor perubahan yang sempit dan mudah digunakan.
Heath Lilley

0

Dalam contoh Anda, tidak jelas SRP dilanggar. Mungkin laporan harus dapat memformat dan mencetak sendiri, jika relatif sederhana:

class Report {
  void format() {
     text = text.trim();
  }

  void print() {
     new Printer().write(text);
  }
}

Metode ini sangat sederhana sehingga tidak masuk akal untuk memiliki ReportFormatteratau ReportPrinterkelas. Satu-satunya masalah mencolok di antarmuka adalah getReportDatakarena melanggar tanya jangan katakan pada objek yang tidak bernilai.

Di sisi lain, jika metode ini sangat rumit atau ada banyak cara untuk memformat atau mencetak Reportmaka masuk akal untuk mendelegasikan tanggung jawab (juga lebih dapat diuji):

class Report {
  void format(ReportFormatter formatter) {
     text = formatter.format(text);
  }

  void print(ReportPrinter printer) {
     printer.write(text);
  }
}

SRP adalah prinsip desain bukan konsep filosofis dan karena itu didasarkan pada kode aktual yang Anda kerjakan. Secara semantik Anda dapat membagi atau mengelompokkan sebuah kelas menjadi sebanyak mungkin tanggung jawab yang Anda inginkan. Namun, sebagai prinsip praktis, SRP harus membantu Anda menemukan kode yang perlu Anda modifikasi . Tanda-tanda Anda melanggar SRP adalah:

  • Kelas sangat besar sehingga Anda membuang waktu untuk mencari atau mencari metode yang tepat.
  • Kelas sangat kecil dan banyak sehingga Anda membuang waktu untuk melompat di antara mereka atau menemukan yang benar.
  • Ketika Anda harus melakukan perubahan, hal itu memengaruhi banyak kelas sehingga sulit untuk dilacak.
  • Ketika Anda harus melakukan perubahan, tidak jelas kelas apa yang perlu diubah.

Anda dapat memperbaikinya melalui refactoring dengan meningkatkan nama, mengelompokkan kode yang sama bersamaan, menghilangkan duplikasi, menggunakan desain berlapis, dan memecah / menggabungkan kelas yang diperlukan. Cara terbaik untuk belajar SRP adalah dengan menyelam ke basis kode dan menghilangkan rasa sakit.


bisakah kamu periksa contoh yang saya lampirkan pada posting dan uraikan jawaban Anda berdasarkan itu.
Songo

Diperbarui. SRP tergantung pada konteks, jika Anda memposting seluruh kelas (dalam pertanyaan terpisah) akan lebih mudah untuk dijelaskan.
Garrett Hall

Terima kasih atas pembaruannya. Namun pertanyaannya, apakah benar-benar tanggung jawab laporan untuk mencetak sendiri ?! Mengapa Anda tidak membuat kelas printer terpisah yang mengambil laporan dalam konstruktornya?
Songo

Saya hanya mengatakan SRP tergantung pada kode itu sendiri Anda tidak boleh menerapkannya secara dogmatis.
Garrett Hall

ya saya mengerti maksud Anda. Tetapi jika Anda membangun sistem dari awal, opsi mana yang akan Anda ambil? Sebuah Printerkelas yang mengambil laporan atau Reportkelas yang mengambil printer? Sering kali saya dihadapkan dengan pertanyaan desain seperti itu sebelum mencari tahu apakah kode tersebut terbukti rumit atau tidak.
Songo

0

Prinsip Tanggung Jawab Tunggal sangat digabungkan dengan gagasan kohesi . Untuk memiliki kelas yang sangat kohesif, Anda harus memiliki ketergantungan bersama antara variabel instance kelas dan metode-metodenya; yaitu, masing-masing metode harus memanipulasi variabel instan sebanyak mungkin. Semakin banyak variabel yang digunakan metode, semakin kohesif kelasnya; kohesi maksimum biasanya tidak dapat diraih.

Juga, untuk menerapkan SRP dengan baik Anda memahami dengan baik domain logika bisnis; untuk mengetahui apa yang harus dilakukan setiap abstraksi. Layered architecture juga terkait dengan SRP, dengan meminta setiap layer melakukan hal tertentu (Data Source Layer harus menyediakan data dan sebagainya).

Kembali ke kohesi bahkan jika metode Anda tidak menggunakan semua variabel, mereka harus digabungkan:

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public Type1 method3() {
        //use var2 and var3
    }
}

Anda seharusnya tidak memiliki sesuatu seperti kode di bawah ini, di mana bagian dari variabel instan digunakan di bagian metode, dan bagian lain dari variabel digunakan di bagian lain dari metode (di sini Anda harus memiliki dua kelas untuk setiap bagian dari variabel).

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;
    private TypeA varA;
    private TypeB varB;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public TypeA methodA() {
        //use varA and varB
    }

    public TypeA methodB() {
        //use varA
    }
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.