Apakah saya membuat kelas saya terlalu granular? Bagaimana Prinsip Tanggung Jawab Tunggal diterapkan?

9

Saya menulis banyak kode yang melibatkan tiga langkah dasar.

  1. Dapatkan data dari suatu tempat.
  2. Ubah data itu.
  3. Letakkan data itu di suatu tempat.

Saya biasanya menggunakan tiga jenis kelas - terinspirasi oleh pola desain masing-masing.

  1. Pabrik - untuk membangun objek dari sumber daya tertentu.
  2. Mediator - untuk menggunakan pabrik, melakukan transformasi, lalu menggunakan komandan.
  3. Komandan - untuk meletakkan data itu di tempat lain.

Kelas saya cenderung sangat kecil, seringkali metode tunggal (publik), misalnya mendapatkan data, mengubah data, melakukan pekerjaan, menyimpan data. Ini mengarah pada proliferasi kelas, tetapi secara umum bekerja dengan baik.

Di mana saya berjuang adalah ketika saya datang ke pengujian, saya akhirnya akan erat dengan tes. Sebagai contoh;

  • Factory - membaca file dari disk.
  • Commander - menulis file ke disk.

Saya tidak bisa menguji satu tanpa yang lain. Saya bisa menulis kode 'test' tambahan untuk melakukan baca / tulis disk, tetapi kemudian saya mengulangi sendiri.

Melihat. Net, kelas File mengambil pendekatan yang berbeda, menggabungkan tanggung jawab (dari saya) pabrik dan komandan bersama. Ini memiliki fungsi untuk Buat, Hapus, Ada, dan Baca semua di satu tempat.

Haruskah saya melihat untuk mengikuti contoh. Net dan menggabungkan - terutama ketika berhadapan dengan sumber daya eksternal - kelas saya bersama? Kode itu masih digabungkan, tetapi lebih disengaja - itu terjadi pada implementasi asli, bukan dalam tes.

Apakah masalah saya di sini bahwa saya telah menerapkan Prinsip Tanggung Jawab Tunggal agak terlalu bersemangat? Saya memiliki kelas terpisah yang bertanggung jawab untuk membaca dan menulis. Ketika saya dapat memiliki kelas gabungan yang bertanggung jawab untuk berurusan dengan sumber daya tertentu, misalnya disk sistem.

James Wood
sumber
6
Looking at .Net, the File class takes a different approach, it combines the responsibilities (of my) factory and commander together. It has functions for Create, Delete, Exists, and Read all in one place.- Perhatikan bahwa Anda menyatukan "tanggung jawab" dengan "hal yang harus dilakukan." Tanggung jawab lebih seperti "bidang perhatian". Tanggung jawab kelas File adalah melakukan operasi file.
Robert Harvey
1
Menurut saya Anda dalam kondisi yang baik. Yang Anda butuhkan adalah mediator uji (atau satu untuk setiap jenis konversi jika Anda suka itu lebih baik). Mediator tes dapat membaca file untuk memverifikasi kebenarannya, menggunakan kelas File .net. Tidak ada masalah dengan itu dari perspektif SOLID.
Martin Maat
1
Seperti yang disebutkan oleh @Robert Harvey, SRP memiliki nama jelek karena ini bukan tentang Tanggung Jawab. Ini adalah tentang "merangkum dan mengabstraksi satu masalah rumit / sulit yang mungkin berubah". Saya kira STDACMC terlalu panjang. :-) Itu mengatakan, saya pikir pembagian Anda menjadi tiga bagian tampaknya masuk akal.
user949300
1
Poin penting dalam Filepustaka Anda dari C # adalah, untuk semua yang kita tahu Filekelas hanya bisa menjadi fasad, menempatkan semua operasi file ke satu tempat - ke dalam kelas, tetapi bisa secara internal menggunakan kelas baca / tulis yang sama dengan Anda yang akan sebenarnya mengandung logika yang lebih rumit untuk penanganan file. Kelas semacam itu (the File) masih akan mematuhi SRP, karena proses benar-benar bekerja dengan sistem berkas akan diabstraksi di belakang lapisan lain - kemungkinan besar dengan antarmuka pemersatu. Bukan mengatakan itu masalahnya, tapi bisa jadi begitu. :)
Andy

Jawaban:

5

Mengikuti prinsip Tanggung Jawab Tunggal mungkin merupakan petunjuk Anda di sini, tetapi di mana Anda memiliki nama yang berbeda.

Segregasi Tanggung Jawab Permintaan Perintah

Pergi belajar itu dan saya pikir Anda akan menemukannya mengikuti pola yang sudah dikenal dan bahwa Anda tidak sendirian dalam bertanya seberapa jauh untuk mengambil ini. Tes asam adalah jika mengikuti ini memberi Anda manfaat nyata atau jika itu hanya mantra buta yang Anda ikuti sehingga Anda tidak perlu berpikir.

Anda telah menyatakan keprihatinan tentang pengujian. Saya tidak berpikir mengikuti CQRS menghalangi penulisan kode yang dapat diuji. Anda mungkin hanya mengikuti CQRS dengan cara yang membuat kode Anda tidak dapat diuji.

Ini membantu untuk mengetahui bagaimana menggunakan polimorfisme untuk membalikkan dependensi kode sumber tanpa perlu mengubah aliran kontrol. Saya tidak begitu yakin di mana keahlian Anda pada tes menulis.

Sepatah kata hati, mengikuti kebiasaan yang Anda temukan di perpustakaan tidak optimal. Perpustakaan memiliki kebutuhan mereka sendiri dan sudah tua. Jadi, bahkan contoh terbaik hanyalah contoh terbaik dari waktu itu.

Ini bukan untuk mengatakan tidak ada contoh yang benar-benar valid yang tidak mengikuti CQRS. Mengikutinya akan selalu sedikit menyakitkan. Tidak selalu layak untuk dibayar. Tetapi jika Anda membutuhkannya, Anda akan senang menggunakannya.

Jika Anda menggunakannya perhatikan kata peringatan ini:

Khususnya CQRS hanya boleh digunakan pada bagian-bagian tertentu dari suatu sistem (BoundedContext dalam istilah DDD) dan bukan sistem secara keseluruhan. Dengan cara berpikir ini, setiap Konteks Terikat membutuhkan keputusannya sendiri tentang bagaimana itu harus dimodelkan.

Martin Flowler: CQRS

candied_orange
sumber
Menarik tidak melihat CQRS sebelumnya. Kode dapat diuji, ini lebih tentang mencoba menemukan cara yang lebih baik. Saya menggunakan tiruan, dan injeksi ketergantungan ketika saya bisa (yang saya pikir adalah apa yang Anda maksudkan).
James Wood
Pertama kali membaca tentang ini, saya memang mengidentifikasi sesuatu yang serupa melalui aplikasi saya: menangani pencarian yang fleksibel, bidang mutiple dapat disaring / diurutkan, (Java / JPA) adalah sakit kepala dan mengarah ke banyak kode boilerplate, kecuali jika Anda membuat mesin pencarian dasar yang akan menangani hal ini untuk Anda (saya menggunakan rsql-jpa). Meskipun saya memiliki model yang sama (katakanlah Entitas JPA yang sama untuk keduanya), pencarian diekstraksi pada layanan generik khusus dan lapisan model tidak harus menanganinya lagi.
Walfrat
3

Anda memerlukan perspektif yang lebih luas untuk menentukan apakah kode tersebut sesuai dengan Prinsip Tanggung Jawab Tunggal. Tidak dapat dijawab hanya dengan menganalisis kode itu sendiri, Anda harus mempertimbangkan kekuatan atau aktor apa yang dapat menyebabkan persyaratan berubah di masa depan.

Katakanlah Anda menyimpan data aplikasi dalam file XML. Faktor apa yang dapat menyebabkan Anda mengubah kode yang terkait dengan membaca atau menulis? Beberapa kemungkinan:

  • Model data aplikasi dapat berubah ketika fitur baru ditambahkan ke aplikasi.
  • Jenis data baru - misalnya gambar - dapat ditambahkan ke model
  • Format penyimpanan dapat berubah secara independen dari logika aplikasi: Katakan dari XML ke JSON atau ke format biner, karena interoperabilitas atau masalah kinerja.

Dalam semua kasus ini, Anda harus mengubah baik membaca dan logika menulis. Dengan kata lain, mereka bukan tanggung jawab yang terpisah.

Tetapi mari kita bayangkan skenario yang berbeda: Aplikasi Anda adalah bagian dari pipa pemrosesan data. Bunyinya beberapa file CSV yang dihasilkan oleh sistem yang terpisah, melakukan beberapa analisis dan pemrosesan, kemudian mengeluarkan file yang berbeda untuk diproses oleh sistem ketiga. Dalam hal ini membaca dan menulis adalah tanggung jawab independen dan harus dipisahkan.

Intinya: Anda secara umum tidak dapat mengatakan jika membaca dan menulis file adalah tanggung jawab yang terpisah, itu tergantung pada peran dalam aplikasi. Tetapi berdasarkan petunjuk Anda tentang pengujian, saya kira itu adalah tanggung jawab tunggal dalam kasus Anda.

JacquesB
sumber
2

Secara umum Anda memiliki ide yang tepat.

Dapatkan data dari suatu tempat. Ubah data itu. Letakkan data itu di suatu tempat.

Sepertinya Anda memiliki tiga tanggung jawab. IMO "Mediator" mungkin melakukan banyak hal. Saya pikir Anda harus mulai dengan memodelkan tiga tanggung jawab Anda:

interface Reader[T] {
    def read(): T
}

interface Transformer[T, U] {
    def transform(t: T): U
}

interface Writer[T] {
    def write(t: T): void
}

Kemudian suatu program dapat dinyatakan sebagai:

def program[T, U](reader: Reader[T], 
                  transformer: Transformer[T, U], 
                  writer: Writer[U]): void =
    writer.write(transformer.transform(reader.read()))

Ini mengarah pada proliferasi kelas

Saya rasa ini bukan masalah. Banyak IMO dari kohesif kecil, kelas yang dapat diuji lebih baik daripada kelas besar, kurang kohesif.

Di mana saya berjuang adalah ketika saya datang ke pengujian, saya akhirnya akan erat dengan tes. Saya tidak bisa menguji satu tanpa yang lain.

Setiap bagian harus dapat diuji secara independen. Dimodelkan di atas, Anda dapat mewakili membaca / menulis ke file sebagai:

class FileReader(fileName: String) implements Reader[String] {
    override read(): String = // read file into string
}

class FileWriter(fileName: String) implements Writer[String] {
    override write(str: String) = // write str to file
}

Anda dapat menulis tes integrasi untuk menguji kelas-kelas ini untuk memverifikasi mereka membaca dan menulis ke sistem file. Sisa dari logika dapat ditulis sebagai transformasi. Sebagai contoh jika file adalah format JSON, Anda dapat mengubah Strings.

class JsonParser implements Transformer[String, Json] {
    override transform(str: String): Json = // parse as json
}

Kemudian Anda bisa berubah menjadi objek yang tepat:

class FooParser implements Transformer[Json, Foo] {
    override transform(json: Json): Foo = // ...
}

Masing-masing dapat diuji secara independen. Anda juga dapat menguji unit programdi atas dengan mengejek reader, transformerdan writer.

Samuel
sumber
Di situlah saya berada sekarang. Saya dapat menguji setiap fungsi secara individual, namun dengan mengujinya mereka menjadi berpasangan. Misalnya untuk FileWriter untuk diuji, maka sesuatu yang lain harus membaca apa yang ditulis, solusi yang jelas adalah menggunakan FileReader. Namun, mediator sering melakukan hal lain seperti menerapkan logika bisnis atau mungkin diwakili oleh fungsi utama aplikasi dasar.
James Wood
1
@ JamesWood itulah yang sering terjadi dengan tes integrasi. Anda tidak harus memasangkan kelas dalam ujian. Anda dapat menguji FileWriterdengan membaca langsung dari sistem file daripada menggunakan FileReader. Terserah kepada Anda apa tujuan Anda dalam ujian. Jika Anda menggunakan FileReader, tes ini akan rusak jika salah satu FileReaderatau FileWriterrusak - yang mungkin memerlukan lebih banyak waktu untuk debug.
Samuel
Lihat juga stackoverflow.com/questions/1087351/... ini dapat membantu membuat tes Anda lebih baik
Samuel
Itu cukup banyak di mana saya sekarang - itu tidak 100% benar. Anda bilang Anda menggunakan pola Mediator. Saya pikir ini tidak berguna di sini; pola ini digunakan ketika Anda memiliki banyak objek yang berbeda berinteraksi satu sama lain dalam aliran yang sangat membingungkan; Anda menempatkan mediator di sana untuk memfasilitasi semua hubungan dan mengimplementasikannya di satu tempat. Ini tampaknya bukan kasus Anda; Anda memiliki unit kecil yang didefinisikan dengan sangat baik. Juga, seperti komentar di atas oleh @Samuel, Anda harus menguji satu unit, dan melakukan konfirmasi tanpa menelepon unit lain
Emerson Cardoso
@EmersonCardoso; Saya agak menyederhanakan skenario dalam pertanyaan saya. Sementara beberapa mediator saya cukup sederhana, yang lain lebih rumit dan sering menggunakan beberapa pabrik / komandan. Saya mencoba untuk menghindari detail dari satu skenario, saya lebih tertarik pada arsitektur desain tingkat tinggi yang dapat diterapkan ke beberapa skenario.
James Wood
2

Saya akhirnya akan erat-erat ditambah tes. Sebagai contoh;

  • Factory - membaca file dari disk.
  • Commander - menulis file ke disk.

Jadi fokus di sini adalah pada apa yang menyatukan mereka . Apakah Anda melewati objek di antara keduanya (seperti a File?) Lalu itu adalah File yang mereka gabungkan, bukan satu sama lain.

Dari apa yang Anda katakan, Anda telah memisahkan kelas Anda. Perangkapnya adalah bahwa Anda menguji mereka bersama karena lebih mudah atau 'masuk akal' .

Mengapa Anda memerlukan input Commanderuntuk berasal dari disk? Yang dipedulikan adalah menulis menggunakan input tertentu, maka Anda dapat memverifikasi itu menulis file dengan benar menggunakan apa yang ada dalam tes .

Bagian sebenarnya yang Anda uji Factoryadalah 'apakah ia akan membaca file ini dengan benar dan menampilkan hal yang benar'? Jadi mengejek file sebelum membacanya dalam ujian .

Atau, pengujian bahwa Pabrik dan Komandan berfungsi ketika digabungkan bersama baik-baik saja - itu sejalan dengan Pengujian Integrasi dengan cukup bahagia. Pertanyaan di sini lebih merupakan masalah apakah Unit Anda dapat mengujinya secara terpisah atau tidak.

Erdrik Ironrose
sumber
Dalam contoh khusus itu hal yang mengikat mereka bersama adalah sumber daya - misalnya disk sistem. Kalau tidak, tidak ada interaksi antara kedua kelas.
James Wood
1

Dapatkan data dari suatu tempat. Ubah data itu. Letakkan data itu di suatu tempat.

Ini adalah pendekatan prosedural yang khas, yang ditulis oleh David Parnas pada tahun 1972. Anda berkonsentrasi pada bagaimana segala sesuatunya berjalan. Anda mengambil solusi konkret dari masalah Anda sebagai pola level yang lebih tinggi, yang selalu salah.

Jika Anda mengejar pendekatan berorientasi objek, saya lebih suka berkonsentrasi pada domain Anda . Tentang apa semua ini? Apa tanggung jawab utama sistem Anda? Apa konsep utama yang hadir dalam bahasa pakar domain Anda? Jadi, pahami domain Anda, dekomposisikan, perlakukan bidang tanggung jawab tingkat tinggi sebagai modul Anda , perlakukan konsep tingkat rendah yang direpresentasikan sebagai kata benda sebagai objek Anda. Ini adalah contoh yang saya berikan pada pertanyaan terakhir, sangat relevan.

Dan ada masalah nyata dengan keterpaduan, Anda telah menyebutkannya sendiri. Jika Anda membuat beberapa modifikasi sebagai input logika dan menulis tes di atasnya, itu sama sekali tidak membuktikan bahwa fungsi Anda berfungsi, karena Anda bisa lupa untuk meneruskan data itu ke lapisan berikutnya. Lihat, lapisan-lapisan ini secara intrinsik digabungkan. Dan decoupling buatan membuat segalanya lebih buruk. Saya tahu itu sendiri: Proyek 7 tahun dengan 100 tahun pria di belakang pundak saya ditulis sepenuhnya dengan gaya ini. Lari dari itu jika Anda bisa.

Dan secara keseluruhan hal SRP. Ini semua tentang kohesi yang diterapkan pada ruang masalah Anda, yaitu domain. Itulah prinsip mendasar di balik SRP. Ini menghasilkan objek yang pintar dan menerapkan tanggung jawab mereka untuk diri mereka sendiri. Tidak ada yang mengendalikan mereka, tidak ada yang memberi mereka data. Mereka menggabungkan data dan perilaku, hanya memperlihatkan yang terakhir. Jadi objek Anda menggabungkan validasi data mentah, transformasi data (yaitu, perilaku) dan kegigihan. Itu bisa terlihat seperti berikut:

class FinanceTransaction
{
    private $id;
    private $storage;

    public function __construct(UUID $id, DataStorage $storage)
    {
        $this->id = $id;
        $this->storage = $storage;
    }

    public function perform(
        Order $order,
        Customer $customer,
        Merchant $merchant
    )
    {
        if ($order->isExpired()) {
            throw new Exception('Order expired');
        }

        if ($customer->canNotPurchase($order)) {
            throw new Exception('It is not legal to purchase this kind of stuff by this customer');
        }

        $this->storage->save($this->id, $order, $customer, $merchant);
    }
}

(new FinanceTransaction())
    ->perform(
        new Order(
            new Product(
                $_POST['product_id']
            ),
            new Card(
                new CardNumber(
                    $_POST['card_number'],
                    $_POST['cvv'],
                    $_POST['expires_at']
                )
            )
        ),
        new Customer(
            new Name(
                $_POST['customer_name']
            ),
            new Age(
                $_POST['age']
            )
        ),
        new Merchant(
            new MerchantId($_POST['merchant_id'])
        )
    )
;

Akibatnya ada beberapa kelas kohesif yang mewakili beberapa fungsi. Perhatikan bahwa validasi biasanya pergi ke objek-nilai - setidaknya dalam pendekatan DDD .

Vadim Samokhin
sumber
1

Di mana saya berjuang adalah ketika saya datang ke pengujian, saya akhirnya akan erat dengan tes. Sebagai contoh;

  • Factory - membaca file dari disk.
  • Commander - menulis file ke disk.

Hati-hati dengan abstraksi yang bocor ketika bekerja dengan sistem file - Saya melihatnya terlalu sering diabaikan, dan ia memiliki gejala yang telah Anda jelaskan.

Jika kelas beroperasi pada data yang berasal dari / masuk ke file-file ini maka sistem file menjadi detail implementasi (I / O) dan harus dipisahkan darinya. Kelas-kelas ini (pabrik / komandan / mediator) seharusnya tidak mengetahui sistem file kecuali tugas mereka hanyalah menyimpan / membaca data yang disediakan. Kelas yang berhubungan dengan sistem file harus merangkum parameter konteks khusus seperti jalur (mungkin dilewatkan melalui konstruktor), sehingga antarmuka tidak mengungkapkan sifatnya (kata "File" dalam nama antarmuka adalah bau sebagian besar waktu).

merasa ngeri
sumber
"Kelas-kelas ini (pabrik / komandan / mediator) seharusnya tidak mengetahui sistem file kecuali satu-satunya tugas mereka adalah menyimpan / membaca data yang disediakan." Dalam contoh khusus ini, hanya itu yang mereka lakukan.
James Wood
0

Menurut pendapat saya, itu terdengar seperti Anda sudah mulai menuju jalan yang benar tetapi Anda belum mengambilnya cukup jauh. Saya pikir memecah fungsionalitas menjadi kelas yang berbeda yang melakukan satu hal dan melakukannya dengan baik adalah benar.

Untuk melangkah lebih jauh, Anda harus membuat antarmuka untuk kelas Factory, Mediator, dan Commander Anda. Kemudian Anda dapat menggunakan versi-kelas yang mengejek dari kelas-kelas tersebut ketika menulis tes unit Anda untuk implementasi konkret yang lain. Dengan mengolok-olok Anda dapat memvalidasi bahwa metode dipanggil dalam urutan yang benar dan dengan parameter yang benar dan bahwa kode yang diuji berperilaku baik dengan nilai pengembalian yang berbeda.

Anda juga bisa melihat abstrak pengambilan / penulisan data. Anda akan pergi ke sistem file sekarang tetapi mungkin ingin pergi ke database atau bahkan socket di masa depan. Kelas mediator Anda tidak perlu berubah jika sumber / tujuan data berubah.

Richard Wells
sumber
1
YAGNI adalah sesuatu yang harus Anda pikirkan.
whatsisname