Apakah konstruktor yang memvalidasi argumennya melanggar SRP?

66

Saya berusaha mematuhi Prinsip Tanggung Jawab Tunggal (SRP) sebanyak mungkin dan terbiasa dengan pola tertentu (untuk SRP tentang metode) sangat bergantung pada delegasi. Saya ingin tahu apakah pendekatan ini masuk akal atau ada masalah parah dengan itu.

Misalnya, untuk memeriksa input ke konstruktor, saya bisa memperkenalkan metode berikut ( Streaminputnya acak, bisa apa saja)

private void CheckInput(Stream stream)
{
    if(stream == null)
    {
        throw new ArgumentNullException();
    }

    if(!stream.CanWrite)
    {
        throw new ArgumentException();
    }
}

Metode ini (bisa dibilang) melakukan lebih dari satu hal

  • Periksa input
  • Lempar pengecualian yang berbeda

Untuk mematuhi SRP saya mengubah logika menjadi

private void CheckInput(Stream stream, 
                        params (Predicate<Stream> predicate, Action action)[] inputCheckers)
{
    foreach(var inputChecker in inputCheckers)
    {
        if(inputChecker.predicate(stream))
        {
            inputChecker.action();
        }
    }
}

Yang seharusnya hanya melakukan satu hal (bukan?): Periksa input. Untuk pengecekan aktual input dan melemparkan pengecualian saya telah memperkenalkan metode seperti

bool StreamIsNull(Stream s)
{
    return s == null;
}

bool StreamIsReadonly(Stream s)
{
    return !s.CanWrite;
}

void Throw<TException>() where TException : Exception, new()
{
    throw new TException();
}

dan bisa memanggil CheckInputseperti

CheckInput(stream,
    (this.StreamIsNull, this.Throw<ArgumentNullException>),
    (this.StreamIsReadonly, this.Throw<ArgumentException>))

Apakah ini lebih baik daripada opsi pertama, atau apakah saya memperkenalkan kompleksitas yang tidak perlu? Apakah ada cara saya masih bisa memperbaiki pola ini, jika itu layak?

Paul Kertscher
sumber
26
Saya bisa berargumentasi bahwa CheckInputmasih melakukan banyak hal: Ia melakukan iterasi pada array dan memanggil fungsi predikat dan memanggil fungsi aksi. Apakah itu bukan pelanggaran terhadap SRP?
Bart van Ingen Schenau
8
Ya, itulah yang ingin saya sampaikan.
Bart van Ingen Schenau
135
penting untuk diingat bahwa itu adalah prinsip tanggung jawab tunggal ; bukan prinsip aksi tunggal . Ini memiliki satu tanggung jawab: memverifikasi aliran didefinisikan dan dapat ditulis.
David Arno
40
Ingatlah bahwa inti dari semua prinsip perangkat lunak ini adalah membuat kode lebih mudah dibaca dan dipelihara. CheckInput asli Anda jauh lebih mudah dibaca dan dikelola daripada versi refactored Anda. Bahkan, jika saya pernah menemukan metode CheckInput terakhir Anda dalam basis kode, saya akan memo semuanya dan menulis ulang agar sesuai dengan apa yang Anda miliki sebelumnya.
17 dari 26
17
"Prinsip-prinsip" ini praktis tidak berguna karena Anda hanya dapat mendefinisikan "tanggung jawab tunggal" dengan cara apa pun yang Anda inginkan untuk melanjutkan apa pun ide awal Anda. Tetapi jika Anda mencoba menerapkannya secara kaku, saya kira Anda akan berakhir dengan kode semacam ini yang, jujur ​​saja, sulit dimengerti.
Casey

Jawaban:

151

SRP mungkin merupakan prinsip perangkat lunak yang paling disalahpahami.

Aplikasi perangkat lunak dibangun dari modul, yang dibangun dari modul, yang dibangun dari ...

Di bagian bawah, fungsi tunggal seperti CheckInputhanya akan berisi sedikit logika, tetapi ketika Anda naik ke atas, setiap modul berturut-turut merangkum lebih banyak logika dan ini normal .

SRP bukan tentang melakukan aksi atom tunggal . Ini tentang memiliki satu tanggung jawab, bahkan jika tanggung jawab itu memerlukan beberapa tindakan ... dan pada akhirnya ini tentang pemeliharaan dan pengujian :

  • mempromosikan enkapsulasi (menghindari Objek Tuhan),
  • itu mempromosikan pemisahan kekhawatiran (menghindari perubahan beriak melalui seluruh basis kode),
  • ini membantu testabilitas dengan mempersempit ruang lingkup tanggung jawab.

Fakta yang CheckInputdiimplementasikan dengan dua pemeriksaan dan menaikkan dua pengecualian yang berbeda tidak relevan sampai batas tertentu.

CheckInputmemiliki tanggung jawab yang sempit: memastikan bahwa input sesuai dengan persyaratan. Ya, ada banyak persyaratan, tetapi ini tidak berarti ada banyak tanggung jawab. Ya, Anda dapat membagi cek, tetapi bagaimana itu membantu? Pada titik tertentu cek harus didaftar dengan cara tertentu.

Mari kita bandingkan:

Constructor(Stream stream) {
    CheckInput(stream);
    // ...
}

melawan:

Constructor(Stream stream) {
    CheckInput(stream,
        (this.StreamIsNull, this.Throw<ArgumentNullException>),
        (this.StreamIsReadonly, this.Throw<ArgumentException>));
    // ...
}

Sekarang, CheckInputtidak banyak ... tetapi peneleponnya berbuat lebih banyak!

Anda telah menggeser daftar persyaratan dari CheckInput, di mana mereka dienkapsulasi, ke di Constructormana mereka terlihat.

Apakah ini perubahan yang baik? Tergantung:

  • Jika CheckInputhanya disebut di sana: itu masih bisa diperdebatkan, di satu sisi itu membuat persyaratan terlihat, di sisi lain itu mengacaukan kode;
  • Jika CheckInputdipanggil beberapa kali dengan persyaratan yang sama , maka itu melanggar KERING dan Anda memiliki masalah enkapsulasi.

Penting untuk menyadari bahwa satu tanggung jawab dapat menyiratkan banyak pekerjaan. "Otak" mobil yang dapat mengendalikan diri memiliki satu tanggung jawab:

Mengemudi mobil ke tujuannya.

Ini adalah tanggung jawab tunggal, tetapi membutuhkan koordinasi satu ton sensor dan aktor, mengambil banyak keputusan, dan bahkan mungkin memiliki persyaratan yang saling bertentangan 1 ...

... Namun, itu semua dikemas. Jadi klien tidak peduli.

1 keselamatan penumpang, keselamatan orang lain, menghormati peraturan, ...

Matthieu M.
sumber
2
Saya pikir cara Anda menggunakan kata "enkapsulasi" dan turunannya membingungkan. Selain itu, jawaban yang bagus!
Fabio mengatakan Reinstate Monica
4
Saya setuju dengan jawaban Anda, tetapi argumen otak mobil yang mandiri sering menggoda orang untuk melanggar SRP. Seperti yang Anda katakan, modul itu terbuat dari modul yang terbuat dari modul. Anda dapat mengidentifikasi tujuan dari keseluruhan sistem itu, tetapi sistem itu harus dipecah dengan sendirinya. Anda dapat memecahkan hampir semua masalah.
Sava B.
13
@ SavaB .: Tentu, tetapi prinsipnya tetap sama. Modul harus memiliki tanggung jawab tunggal, meskipun memiliki cakupan yang lebih besar daripada konstituennya.
Matthieu M.
3
@ user949300 Oke, bagaimana kalau hanya "mengemudi." Sungguh, "mengemudi" adalah tanggung jawab dan "aman" dan "secara hukum" adalah persyaratan tentang bagaimana memenuhi tanggung jawab itu. Dan kami sering mencantumkan persyaratan saat menyatakan tanggung jawab.
Brian McCutchon
1
"SRP mungkin adalah prinsip perangkat lunak yang paling disalahpahami." Sebagaimana dibuktikan oleh jawaban ini :)
Michael
41

Mengutip Paman Bob tentang SRP ( https://8thlight.com/blog/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html ):

Prinsip Tanggung Jawab Tunggal (SRP) menyatakan bahwa setiap modul perangkat lunak harus memiliki satu dan hanya satu alasan untuk berubah.

... Prinsip ini tentang orang.

... Ketika Anda menulis modul perangkat lunak, Anda ingin memastikan bahwa ketika perubahan diminta, perubahan itu hanya dapat berasal dari satu orang, atau lebih tepatnya, satu kelompok orang yang sangat erat yang mewakili satu fungsi bisnis yang didefinisikan secara sempit.

... Ini adalah alasan kami tidak menempatkan SQL di JSP. Inilah alasan kami tidak menghasilkan HTML dalam modul yang menghitung hasil. Ini adalah alasan bahwa aturan bisnis tidak boleh mengetahui skema basis data. Ini adalah alasan kami memisahkan masalah.

Dia menjelaskan bahwa modul perangkat lunak harus mengatasi kekhawatiran pemangku kepentingan tertentu. Karena itu, jawablah pertanyaan Anda:

Apakah ini lebih baik daripada opsi pertama, atau apakah saya memperkenalkan kompleksitas yang tidak perlu? Apakah ada cara saya masih bisa memperbaiki pola ini, jika itu layak?

IMO, Anda hanya melihat satu metode, ketika Anda harus melihat level yang lebih tinggi (level kelas dalam hal ini). Mungkin kita harus melihat apa yang sedang dilakukan kelas Anda (dan ini membutuhkan lebih banyak penjelasan tentang skenario Anda). Untuk saat ini, kelas Anda masih melakukan hal yang sama. Misalnya, jika besok ada beberapa permintaan perubahan tentang beberapa validasi (misalnya: "sekarang streaming dapat menjadi nol"), maka Anda masih perlu pergi ke kelas ini dan mengubah hal-hal di dalamnya.

Emerson Cardoso
sumber
4
Jawaban Terbaik. Untuk menguraikan tentang OP, jika pemeriksaan penjaga datang dari dua pemangku kepentingan / departemen yang berbeda, maka checkInputs()harus dibagi, katakan menjadi checkMarketingInputs()dan checkRegulatoryInputs(). Kalau tidak apa-apa, menggabungkan semuanya menjadi satu metode.
user949300
36

Tidak, perubahan ini tidak diinformasikan oleh SRP.

Tanyakan pada diri Anda mengapa tidak ada check di checker Anda untuk "objek yang dilewati adalah aliran" . Jawabannya jelas: bahasa mencegah penelepon menyusun program yang lewat di non-stream.

Sistem tipe C # tidak cukup untuk memenuhi kebutuhan Anda; cek Anda menerapkan penegakan invarian yang tidak dapat diungkapkan dalam sistem jenis hari ini . Jika ada cara untuk mengatakan bahwa metode ini mengambil aliran yang tidak dapat ditulisi, Anda akan menulis itu, tetapi tidak ada, jadi Anda melakukan hal terbaik berikutnya: Anda menerapkan pembatasan jenis saat runtime. Semoga Anda juga mendokumentasikannya, sehingga pengembang yang menggunakan metode Anda tidak harus melanggarnya, gagal dalam kasus pengujian mereka, dan kemudian memperbaiki masalahnya.

Menempatkan tipe pada suatu metode bukanlah pelanggaran terhadap Prinsip Tanggung Jawab Tunggal; tidak ada metode yang menegakkan prasyaratnya atau menegaskan prasyarat kondisinya.

Eric Lippert
sumber
1
Juga, meninggalkan objek yang dibuat dalam keadaan valid adalah satu tanggung jawab yang selalu dimiliki oleh konstruktor. Jika, seperti yang Anda sebutkan, memerlukan pemeriksaan tambahan yang tidak dapat diberikan oleh runtime dan / atau kompiler, maka sebenarnya tidak ada jalan lain.
SBI
23

Tidak semua tanggung jawab diciptakan sama.

masukkan deskripsi gambar di sini

masukkan deskripsi gambar di sini

Ini ada dua laci. Mereka berdua memiliki satu tanggung jawab. Mereka masing-masing memiliki nama yang memberi tahu Anda apa yang termasuk di dalamnya. Salah satunya adalah laci perak. Yang lainnya adalah laci sampah.

Jadi apa bedanya? Laci perak menjelaskan apa yang bukan miliknya. Laci sampah bagaimanapun menerima apapun yang cocok. Mengambil sendok dari laci alat makan tampaknya sangat salah. Namun saya sulit sekali memikirkan apa pun yang akan terlewatkan jika dihapus dari laci sampah. Yang benar adalah Anda dapat mengklaim sesuatu memiliki tanggung jawab tunggal tetapi menurut Anda mana yang memiliki tanggung jawab tunggal yang lebih terfokus?

Objek yang memiliki satu tanggung jawab tidak berarti hanya satu hal yang dapat terjadi di sini. Tanggung jawab bisa bersarang. Tetapi tanggung jawab bersarang itu masuk akal, mereka seharusnya tidak mengejutkan Anda ketika Anda menemukannya di sini dan Anda harus merindukan mereka jika mereka pergi.

Jadi saat Anda menawarkan

CheckInput(Stream stream);

Saya tidak merasa khawatir bahwa keduanya memeriksa input dan melempar pengecualian. Saya akan khawatir jika keduanya memeriksa input dan menyimpan input juga. Itu kejutan yang tidak menyenangkan. Satu aku tidak akan ketinggalan jika itu hilang.

candied_orange
sumber
21

Ketika Anda mengikat diri Anda dalam simpul dan menulis kode aneh untuk menyesuaikan dengan Prinsip Perangkat Lunak Penting, biasanya Anda telah salah memahami prinsip tersebut (meskipun kadang-kadang prinsipnya salah). Seperti yang ditunjukkan oleh Matthieu, seluruh makna SRP tergantung pada definisi "tanggung jawab".

Pemrogram berpengalaman melihat prinsip-prinsip ini dan mengaitkannya dengan ingatan kode yang kami buat; programmer kurang berpengalaman melihat mereka dan mungkin tidak ada hubungannya sama sekali. Ini adalah abstraksi yang mengambang di ruang angkasa, semuanya tersenyum dan tidak ada kucing. Jadi mereka menebak, dan biasanya berjalan buruk. Sebelum Anda mengembangkan pemrograman akal sehat, perbedaan antara kode rumit berlebih yang aneh dan kode normal sama sekali tidak jelas.

Ini bukan perintah agama yang harus Anda patuhi terlepas dari konsekuensi pribadi. Ini lebih dari aturan praktis yang dimaksudkan untuk memformalkan satu elemen pemrograman akal kuda, dan membantu Anda menjaga kode Anda sesederhana dan sejelas mungkin. Jika memiliki efek sebaliknya, Anda benar untuk mencari beberapa masukan dari luar.

Dalam pemrograman, Anda tidak dapat melakukan kesalahan lebih banyak daripada mencoba menyimpulkan makna pengidentifikasi dari prinsip pertama hanya dengan melihatnya, dan itu berlaku untuk pengidentifikasi dalam menulis tentang pemrograman sama halnya dengan pengidentifikasi dalam kode aktual.

Ed Plunkett
sumber
14

Peran CheckInput

Pertama, biarkan aku meletakkan jelas di luar sana, CheckInput adalah melakukan satu hal, bahkan jika itu adalah memeriksa berbagai aspek. Pada akhirnya memeriksa input . Orang bisa berpendapat bahwa itu bukan satu hal jika Anda berurusan dengan metode yang disebut DoSomething, tapi saya pikir aman untuk mengasumsikan bahwa memeriksa input tidak terlalu kabur.

Menambahkan pola ini untuk predikat dapat berguna jika Anda ingin tidak ingin logika untuk memeriksa input ditempatkan ke dalam kelas Anda, tetapi pola ini agak bertele-tele untuk apa yang ingin Anda capai. Mungkin jauh lebih langsung untuk hanya melewati antarmuka IStreamValidatordengan metode tunggal isValid(Stream)jika itu yang ingin Anda peroleh. Setiap kelas implementasi IStreamValidatordapat menggunakan predikat seperti StreamIsNullatau StreamIsReadonlyjika mereka inginkan, tetapi kembali ke titik pusat, itu adalah perubahan yang agak konyol untuk kepentingan mempertahankan prinsip tanggung jawab tunggal.

Cek kewarasan

Adalah ide saya bahwa kita semua diizinkan melakukan "pemeriksaan kewarasan" untuk memastikan bahwa Anda setidaknya berurusan dengan Stream yang tidak nol dan dapat ditulisi, dan pemeriksaan dasar ini tidak membuat kelas Anda validator stream. Pikiran Anda, pemeriksaan yang lebih canggih akan lebih baik dibiarkan di luar kelas Anda, tetapi di situlah garis ditarik. Setelah Anda perlu mulai mengubah keadaan aliran Anda dengan membaca darinya atau mendedikasikan sumber daya ke arah validasi, Anda sudah mulai melakukan validasi formal atas aliran Anda dan inilah yang harus ditarik ke dalam kelasnya sendiri.

Kesimpulan

Pikiranku adalah bahwa jika Anda menerapkan suatu pola untuk mengatur lebih baik suatu aspek dari kelas Anda, itu pantas untuk berada di kelasnya sendiri. Karena suatu pola tidak cocok, Anda juga harus mempertanyakan apakah pola itu memang termasuk dalam kelasnya sendiri. Pikiran saya adalah bahwa kecuali Anda yakin validasi aliran kemungkinan akan berubah di masa mendatang, dan terutama jika Anda yakin validasi ini mungkin bersifat dinamis, maka pola yang Anda gambarkan adalah ide yang bagus, bahkan jika itu mungkin awalnya sepele. Kalau tidak, tidak perlu dengan sewenang-wenang membuat program Anda lebih kompleks. Mari kita sebut sekop sekop. Validasi adalah satu hal, tetapi memeriksa input nol bukan validasi, dan oleh karena itu saya pikir Anda bisa aman menyimpannya di kelas Anda tanpa melanggar prinsip tanggung jawab tunggal.

Neil
sumber
4

Prinsipnya dengan tegas tidak menyatakan sepotong kode harus "hanya melakukan satu hal".

"Tanggung jawab" dalam SRP harus dipahami pada tingkat persyaratan. Tanggung jawab kode adalah untuk memenuhi persyaratan bisnis. SRP dilanggar jika suatu objek memenuhi lebih dari satu persyaratan bisnis independen . Dengan mandiri itu berarti bahwa satu persyaratan dapat berubah sementara persyaratan lainnya tetap ada.

Bisa dibayangkan bahwa persyaratan bisnis baru diperkenalkan yang berarti objek khusus ini tidak harus memeriksa dapat dibaca, sementara persyaratan bisnis lain masih memerlukan objek untuk memeriksa dapat dibaca? Tidak, karena persyaratan bisnis tidak menentukan detail implementasi di tingkat itu.

Contoh aktual pelanggaran SRP adalah kode seperti ini:

var message = "Your package will arrive before " + DateTime.Now.AddDays(14);

Kode ini sangat sederhana, tetapi masih dapat dibayangkan bahwa teks akan berubah secara independen dari tanggal pengiriman yang diharapkan, karena ini diputuskan oleh berbagai bagian bisnis.

JacquesB
sumber
Kelas yang berbeda untuk hampir setiap kebutuhan terdengar seperti mimpi buruk yang tidak suci.
whatsisname
@whatsisname: Maka mungkin SRP bukan untuk Anda. Tidak ada prinsip desain yang berlaku untuk semua jenis dan ukuran proyek. (Tetapi ketahuilah bahwa kita hanya berbicara tentang persyaratan independen (yaitu dapat berubah secara independen), bukan hanya persyaratan apa pun sejak saat itu, itu hanya akan bergantung pada seberapa rinci persyaratan itu ditetapkan.)
JacquesB
Saya pikir lebih dari SRP membutuhkan elemen penilaian situasional yang sulit untuk dijelaskan dalam satu frase yang menarik.
whatsisname
@whatsisname: Saya setuju sepenuhnya.
JacquesB
+1 untuk SRP dilanggar jika suatu objek memenuhi lebih dari satu persyaratan bisnis independen. Dengan independen itu berarti bahwa satu persyaratan dapat berubah sementara persyaratan lainnya tetap berlaku
Juzer Ali
3

Saya suka intinya dari jawaban @ EricLippert :

Tanyakan pada diri Anda mengapa tidak ada cek di checker Anda untuk objek yang dilewati adalah aliran . Jawabannya jelas: bahasa mencegah penelepon menyusun program yang lewat di non-stream.

Sistem tipe C # tidak cukup untuk memenuhi kebutuhan Anda; cek Anda menerapkan penegakan invarian yang tidak dapat diungkapkan dalam sistem jenis hari ini . Jika ada cara untuk mengatakan bahwa metode ini mengambil aliran yang tidak dapat ditulisi, Anda akan menulis itu, tetapi tidak ada, jadi Anda melakukan hal terbaik berikutnya: Anda menerapkan pembatasan jenis saat runtime. Semoga Anda juga mendokumentasikannya, sehingga pengembang yang menggunakan metode Anda tidak harus melanggarnya, gagal dalam kasus pengujian mereka, dan kemudian memperbaiki masalahnya.

EricLippert benar bahwa ini adalah masalah untuk sistem tipe. Dan karena Anda ingin menggunakan prinsip tanggung jawab tunggal (SRP), maka Anda pada dasarnya membutuhkan sistem tipe untuk bertanggung jawab atas pekerjaan ini.

Sebenarnya mungkin untuk melakukan ini di C #. Kita dapat menangkap literal nullpada waktu kompilasi, kemudian menangkap non-literal nullpada saat run-time. Itu tidak sebagus pemeriksaan waktu kompilasi penuh, tetapi ini merupakan peningkatan yang ketat agar tidak tertangkap pada waktu kompilasi.

Jadi, Anda tahu bagaimana C # Nullable<T>? Mari kita balikkan itu dan buat NonNullable<T>:

public struct NonNullable<T> where T : class
{
    public T Value { get; private set; }
    public NonNullable(T value)
    {
        if (value == null) { throw new NullArgumentException(); }
        this.Value = value;
    }
    //  Ease-of-use:
    public static implicit operator T(NonNullable<T> value) { return value.Value; }
    public static implicit operator NonNullable<T>(T value) { return new NonNullable<T>(value); }

    //  Hack-ish overloads that prevent null-literals from being implicitly converted into NonNullable<T>'s.
    public static implicit operator NonNullable<T>(Tuple<T> value) { return new NonNullable<T>(value.Item1); }
    public static implicit operator NonNullable<T>(Tuple<T, T> value) { return new NonNullable<T>(value.Item1); }
}

Sekarang, alih-alih menulis

public void Foo(Stream stream)
{
  if (stream == null) { throw new NullArgumentException(); }

  // ...method code...
}

, cukup tulis:

public void Foo(NonNullable<Stream> stream)
{
  // ...method code...
}

Lalu, ada tiga use case:

  1. Panggilan pengguna Foo()dengan non-null Stream:

    Stream stream = new Stream();
    Foo(stream);

    Ini adalah use case yang diinginkan, dan ini berfungsi dengan atau tanpa NonNullable<>.

  2. Panggilan pengguna Foo()dengan nol Stream:

    Stream stream = null;
    Foo(stream);

    Ini adalah kesalahan panggilan. Di sini NonNullable<>membantu memberi tahu pengguna bahwa mereka seharusnya tidak melakukan ini, tetapi itu tidak benar-benar menghentikan mereka. Either way, ini menghasilkan run-time NullArgumentException.

  3. Panggilan pengguna Foo()dengan null:

    Foo(null);

    nulltidak akan secara implisit dikonversi menjadi NonNullable<>, sehingga pengguna mendapat kesalahan dalam IDE, sebelum waktu-berjalan. Ini mendelegasikan pemeriksaan-nol ke sistem tipe, seperti yang disarankan oleh SRP.

Anda dapat memperluas metode ini untuk menegaskan hal-hal lain tentang argumen Anda juga. Misalnya, karena Anda ingin aliran yang dapat ditulisi, Anda dapat menentukan suatu struct WriteableStream<T> where T:Streamyang memeriksa keduanya nulldan stream.CanWritedalam konstruktor. Ini masih akan menjadi pemeriksaan jenis run-time, tetapi:

  1. Itu menghiasi jenis dengan WriteableStreamkualifikasi, menandakan perlunya penelepon.

  2. Itu melakukan pemeriksaan di satu tempat dalam kode, sehingga Anda tidak perlu mengulangi pemeriksaan dan throw InvalidArgumentExceptionsetiap kali.

  3. Ini lebih sesuai dengan SRP dengan mendorong tugas-tugas pengecekan tipe ke sistem tipe (seperti yang diperpanjang oleh dekorator generik).

Nat
sumber
3

Pendekatan Anda saat ini bersifat prosedural. Anda memecah Streamobjek dan memvalidasinya dari luar. Jangan lakukan itu - itu merusak enkapsulasi. Biarkan yang Streambertanggung jawab atas validasinya sendiri. Kami tidak dapat mencoba menerapkan SRP sampai kami memiliki beberapa kelas untuk menerapkannya.

Inilah Streamyang melakukan tindakan hanya jika melewati validasi:

class Stream
{
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }

        System.out.println("My action");
    }
}

Tapi sekarang kita melanggar SRP! "Kelas seharusnya hanya punya satu alasan untuk berubah." Kami mendapat campuran 1) validasi dan 2) logika aktual. Kami punya dua alasan yang mungkin perlu diubah.

Kita bisa menyelesaikan ini dengan memvalidasi dekorator . Pertama, kita perlu mengubah Streamantarmuka kita menjadi dan mengimplementasikannya sebagai kelas konkret.

interface Stream
{
    void someAction();
}

class DefaultStream implements Stream
{
    @Override
    public void someAction()
    {
        System.out.println("My action");
    }
}

Kita sekarang dapat menulis dekorator yang membungkus a Stream, melakukan validasi dan menolak diberikan Streamuntuk logika tindakan yang sebenarnya.

class WritableStream implements Stream
{
    private final Stream stream;

    public WritableStream(final Stream stream)
    {
        this.stream = stream;
    }

    @Override
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }
        stream.someAction();
    }
}

Kita sekarang dapat menyusun ini dengan cara apa pun yang kita suka:

final Stream myStream = new WritableStream(
    new DefaultStream()
);

Ingin validasi tambahan? Tambahkan dekorator lain.

Michael
sumber
1

Pekerjaan kelas adalah menyediakan layanan yang memenuhi kontrak . Kelas selalu memiliki kontrak: seperangkat persyaratan untuk menggunakannya, dan berjanji akan membuat tentang negara dan output asalkan persyaratan terpenuhi. Kontrak ini mungkin eksplisit, melalui dokumentasi dan / atau pernyataan, atau implisit, tetapi selalu ada.

Bagian dari kontrak kelas Anda adalah bahwa penelepon memberikan argumen kepada konstruktor yang tidak boleh nol. Menerapkan kontrak adalah tanggung jawab kelas, jadi untuk memeriksa bahwa penelepon telah memenuhi bagian kontraknya, dapat dengan mudah dianggap berada dalam lingkup tanggung jawab kelas.

Gagasan bahwa suatu kelas mengimplementasikan kontrak adalah karena Bertrand Meyer , perancang bahasa pemrograman Eiffel dan gagasan desain berdasarkan kontrak . Bahasa Eiffel membuat spesifikasi dan memeriksa bagian kontrak bahasa.

Wayne Conrad
sumber
0

Seperti yang telah ditunjukkan dalam jawaban lain, SRP sering disalahpahami. Ini bukan tentang memiliki kode atom yang hanya melakukan satu fungsi. Ini tentang memastikan bahwa objek dan metode Anda hanya melakukan satu hal, dan bahwa satu hal hanya dilakukan di satu tempat.

Mari kita lihat contoh yang buruk dalam kode pseudo.

class Math
    private int a;
    private int b;
    def constructor(int x, int y) 
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Dalam contoh kami yang agak absurd, "tanggung jawab" konstruktor Math # adalah membuat objek matematika dapat digunakan. Ia melakukannya dengan terlebih dahulu membersihkan input, kemudian dengan memastikan nilainya tidak -1.

Ini adalah SRP yang valid karena konstruktor hanya melakukan satu hal. Ini mempersiapkan objek matematika. Namun itu tidak terlalu terpelihara. Itu melanggar KERING.

Jadi mari kita lewati lagi

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        cleanX(x)
        cleanY(y)
    end
    def cleanX(int x)
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
   end
   def cleanY(int y)
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Dalam lulus ini kami mendapat sedikit lebih baik tentang KERING, tetapi kami masih memiliki cara untuk pergi dengan KERING. SRP di sisi lain sepertinya agak mati. Kami sekarang memiliki dua fungsi dengan pekerjaan yang sama. CleanX dan cleanY membersihkan input.

Mari kita coba lagi

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i != null)
          return i
        else if(i < 0)
          return abs(i)
        else if (i == -1)
          throw "Some Silly Error"
        else
          return 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Sekarang akhirnya lebih baik tentang KERING, dan SRP tampaknya setuju. Kami hanya memiliki satu tempat yang melakukan pekerjaan "membersihkan".

Secara teori kode ini lebih mudah dikelola dan lebih baik ketika kita memperbaiki bug dan memperketat kodenya, kita hanya perlu melakukannya di satu tempat.

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i == null)
          return 0
        else if (i == -1)
          throw "Some Silly Error"
        else
          return abs(i)
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Dalam kebanyakan kasus dunia nyata objek akan lebih kompleks dan SRP akan diterapkan di banyak objek. Misalnya usia mungkin milik Ayah, Ibu, Putra, Putri, jadi alih-alih memiliki 4 kelas yang mengetahui usia sejak tanggal lahir Anda memiliki kelas Person yang melakukan itu dan 4 kelas mewarisi dari itu. Tapi saya harap contoh ini membantu menjelaskan. SRP bukan tentang aksi atom, tetapi tentang "pekerjaan" yang dilakukan.

kapas
sumber
-3

Berbicara tentang SRP, Paman Bob tidak suka cek nol ditaburkan di mana-mana. Secara umum Anda, sebagai tim, harus menghindari menggunakan parameter nol untuk konstruktor bila memungkinkan. Ketika Anda mempublikasikan kode Anda di luar tim Anda, hal-hal dapat berubah.

Menegakkan non-nullability parameter konstruktor tanpa terlebih dahulu memastikan keterpaduan kelas yang bersangkutan menghasilkan mengasapi kode panggilan, terutama tes.

Jika Anda benar-benar ingin menegakkan kontrak seperti itu pertimbangkan untuk menggunakan Debug.Assertatau sesuatu yang serupa untuk mengurangi kekacauan:

public AClassThatDefinitelyNeedsAWritableStream(Stream stream)
{
   Assert.That(stream.CanWrite, "Put crucial information here, and not inane bloat.");

   // Go on normal operation.
}
abuzittin gillifirca
sumber