SOLID vs metode statis


11

Inilah masalah yang sering saya temui: Biarkan ada proyek toko web yang memiliki kelas Produk. Saya ingin menambahkan fitur yang memungkinkan pengguna untuk mengirim ulasan ke suatu produk. Jadi saya memiliki kelas Ulasan yang mereferensikan suatu produk. Sekarang saya membutuhkan metode yang mencantumkan semua ulasan untuk suatu produk. Ada dua kemungkinan:

(SEBUAH)

public class Product {
  ...
  public Collection<Review> getReviews() {...}
}

(B)

public class Review {
  ...
  static public Collection<Review> forProduct( Product product ) {...}
}

Dari melihat kode, saya akan memilih (A): Ini tidak statis dan tidak perlu parameter. Namun, saya merasa bahwa (A) melanggar Prinsip Tanggung Jawab Tunggal (SRP) dan Prinsip Terbuka-Tertutup (OCP) sedangkan (B) tidak:

  • (SRP) Ketika saya ingin mengubah cara ulasan dikumpulkan untuk suatu produk, saya harus mengubah kelas Produk. Tetapi seharusnya hanya ada satu alasan mengapa mengubah kelas Produk. Dan itu tentu bukan ulasan. Jika saya mengemas setiap fitur yang ada hubungannya dengan produk dalam Produk, itu akan segera berantakan.

  • (OCP) Saya harus mengubah kelas Produk untuk memperluasnya dengan fitur ini. Saya pikir ini melanggar bagian prinsip 'Tertutup untuk perubahan'. Sebelum saya mendapat permintaan pelanggan untuk menerapkan ulasan, saya menganggap Produk sudah selesai, dan "menutup" itu.

Apa yang lebih penting: mengikuti prinsip SOLID, atau memiliki antarmuka yang lebih sederhana?

Atau apakah saya melakukan sesuatu yang salah di sini?

Hasil

Wow, terima kasih atas semua jawaban Anda yang luar biasa! Sulit untuk memilih satu sebagai jawaban resmi.

Biarkan saya meringkas argumen utama dari jawaban:

  • pro (A): OCP juga bukan hukum dan keterbacaan masalah kode.
  • pro (A): hubungan entitas harus dinavigasi. Kedua kelas mungkin tahu tentang hubungan ini.
  • pro (A) + (B): lakukan keduanya dan delegasikan dalam (A) ke (B) sehingga Produk kecil kemungkinannya untuk diubah lagi.
  • pro (C): memasukkan metode finder ke kelas tiga (layanan) yang tidak statis.
  • contra (B): menghambat mengejek dalam tes.

Beberapa hal tambahan yang berkontribusi di perguruan tinggi saya:

  • pro (B): kerangka kerja ORM kami dapat secara otomatis menghasilkan kode untuk (B).
  • pro (A): untuk alasan teknis kerangka ORM kami, akan diperlukan untuk mengubah entitas "tertutup" dalam beberapa kasus, secara independen dari tempat penemu pergi. Jadi saya tidak akan selalu bisa tetap pada SOLID, sih.
  • contra (C): terlalu ribut ;-)

Kesimpulan

Saya menggunakan keduanya (A) + (B) dengan delegasi untuk proyek saya saat ini. Namun, dalam lingkungan yang berorientasi layanan, saya akan menggunakan (C).


2
Selama itu bukan variabel statis semuanya keren. Metode statis yang mudah diuji, dan mudah dilacak.
Coder

3
Mengapa tidak hanya memiliki kelas ProductsReviews? Kemudian Produk dan Ulasan tetap sama. Atau mungkin saya salah paham.
ElGringoGrande

2
@Coder "Metode statis mudah untuk diuji", benarkah? Mereka tidak dapat diejek, lihat: googletesting.blogspot.com/2008/12/… untuk lebih jelasnya.
StuperUser

1
@StuperUser: Tidak ada yang perlu diejek. Assert(5 = Math.Abs(-5));
Coder

2
Pengujian Abs()bukanlah masalahnya, menguji sesuatu yang bergantung padanya. Anda tidak memiliki jahitan untuk mengisolasi Code-Under-Test (CUT) yang dependen untuk menggunakan tiruan. Ini berarti Anda tidak dapat mengujinya sebagai unit atom dan semua tes Anda menjadi tes integrasi yang menguji logika unit. Kegagalan dalam suatu tes bisa dalam CUT atau dalam Abs()(atau kode ketergantungannya) dan menghilangkan manfaat diagnosis dari tes unit.
StuperUser

Jawaban:


7

Apa yang lebih penting: mengikuti prinsip SOLID, atau memiliki antarmuka yang lebih sederhana?

Antarmuka berhadapan SOLID

Ini tidak saling eksklusif. Antarmuka harus menyatakan sifat model bisnis Anda secara ideal dalam istilah model bisnis. Prinsip-prinsip SOLID adalah Koan untuk memaksimalkan pemeliharaan kode Berorientasi Objek (dan maksud saya "pemeliharaan" dalam arti luas). Yang pertama mendukung penggunaan dan manipulasi model bisnis Anda dan yang terakhir mengoptimalkan pemeliharaan kode.

Prinsip Terbuka / Tertutup

"jangan sentuh itu!" interpretasi yang terlalu sederhana. Dan dengan asumsi kita maksudkan "kelas" adalah arbitrer, dan belum tentu benar. Sebaliknya, OCP berarti bahwa Anda telah merancang kode Anda sehingga mengubah perilaku itu tidak (seharusnya tidak) mengharuskan Anda untuk secara langsung memodifikasi kode yang ada dan berfungsi. Lebih jauh, tidak menyentuh kode di tempat pertama adalah cara yang ideal untuk menjaga integritas antarmuka yang ada; ini adalah akibat wajar dari OCP dalam pandangan saya.

Akhirnya saya melihat OCP sebagai indikator kualitas desain yang ada. Jika saya mendapati diri saya terlalu sering membuka kelas terbuka (atau metode), dan / atau tanpa alasan (ha, ha) yang benar-benar solid untuk melakukannya maka ini mungkin memberitahu saya bahwa saya punya beberapa desain yang buruk (dan / atau saya tidak punya) tahu cara membuat kode OO).

Berikan yang terbaik, kami memiliki tim dokter yang siap siaga

Jika analisis persyaratan Anda memberi tahu Anda bahwa Anda perlu mengekspresikan hubungan Tinjauan Produk dari kedua perspektif, maka lakukanlah.

Karenanya, Wolfgang, Anda mungkin punya alasan bagus untuk memodifikasi kelas-kelas yang ada. Dengan adanya persyaratan baru, jika suatu Tinjauan sekarang menjadi bagian mendasar dari suatu Produk, jika setiap perluasan dari Produk membutuhkan Tinjauan, jika melakukannya membuat kode klien ekspresif secara tepat, kemudian mengintegrasikannya ke dalam Produk.


1
+1 untuk mencatat bahwa OCP lebih merupakan indikator kualitas yang baik, bukan aturan cepat untuk kode apa pun. Jika Anda merasa sulit atau tidak mungkin untuk mengikuti OCP dengan benar, maka itu pertanda model Anda perlu di refactored untuk memungkinkan penggunaan kembali yang lebih fleksibel.
CodexArcanum

Saya memilih contoh Produk dan Ulasan untuk menunjukkan bahwa Produk adalah entitas inti dari proyek sedangkan Tinjauan hanyalah tambahan. Jadi Produk sudah ada, selesai, dan Tinjauan muncul kemudian dan harus diperkenalkan tanpa membuka kode yang ada (termasuk Produk).
Wolfgang

10

SOLID adalah pedoman, jadi pengaruhi pengambilan keputusan alih-alih mendiktekannya.

Satu hal yang harus diperhatikan ketika menggunakan metode statis adalah dampaknya pada testabilitas .


Pengujian forProduct(Product product)tidak akan menjadi masalah.

Menguji sesuatu yang tergantung padanya.

Anda tidak akan memiliki jahitan untuk mengisolasi Code-Under-Test (CUT) yang dependen untuk menggunakan tiruan, karena ketika aplikasi sedang berjalan, metode statis harus ada.

Mari kita memiliki metode yang disebut CUT()panggilan ituforProduct()

Jika forProduct()ini staticAnda tidak dapat menguji CUT()sebagai unit atom dan semua tes Anda menjadi tes integrasi unit tes logika.

Kegagalan dalam tes untuk CUT, dapat disebabkan oleh masalah dalam CUT()atau dalam forProduct()(atau salah satu kode dependennya) yang menghilangkan manfaat diagnosis dari tes unit.

Lihat posting blog yang luar biasa ini untuk informasi lebih rinci: http://googletesting.blogspot.com/2008/12/static-methods-are-death-to-testability.html


Hal ini dapat menyebabkan frustrasi dengan kegagalan tes dan ditinggalkannya praktik dan manfaat yang baik di sekitarnya.


1
Itu poin yang sangat bagus. Secara umum, bukan untuk saya. ;-) Saya tidak begitu ketat tentang penulisan unit test yang mencakup lebih dari satu kelas. Mereka semua pergi ke database, tidak apa-apa bagi saya. Jadi saya tidak akan mengejek objek bisnis atau itu adalah penemunya.
Wolfgang

+1, terima kasih telah menetapkan bendera merah untuk pengujian! Saya setuju dengan @ Wolfgang, biasanya saya tidak begitu ketat juga, tetapi ketika saya membutuhkan tes semacam itu saya benar-benar benci metode statis. Biasanya jika metode statis berinteraksi terlalu banyak dengan parameternya atau jika berinteraksi dengan metode statis lainnya, saya lebih suka menjadikannya metode instan.
Adriano Repetti

Tidakkah ini sepenuhnya bergantung pada bahasa yang digunakan? OP menggunakan contoh Java, tetapi tidak pernah menyebutkan bahasa dalam pertanyaan, juga tidak disebutkan dalam tag.
Izkata

1
@StuperUser If forProduct() is static you can't test CUT() as an atomic unit and all of your tests become integration tests that test unit logic.- Saya percaya Javascript dan Python keduanya memungkinkan metode statis ditimpa / dihina. Saya tidak 100% yakin.
Izkata

1
@Izkata JS diketik secara dinamis, jadi tidak perlu static, Anda mensimulasikannya dengan penutupan dan pola singleton. Membaca di Python (terutama stackoverflow.com/questions/893015/… ), Anda harus mewarisi dan memperluas. Mengganti tidak mengejek; Sepertinya Anda masih belum memiliki jahitan untuk menguji kode sebagai unit atom.
StuperUser

4

Jika Anda berpikir bahwa Produk adalah tempat yang tepat untuk menemukan Ulasan produk, Anda selalu dapat memberikan Produk kelas membantu untuk melakukan pekerjaan untuk itu. (Anda dapat mengetahui karena bisnis Anda tidak akan pernah membicarakan ulasan kecuali dalam hal suatu produk).

Sebagai contoh, saya akan tergoda untuk menyuntikkan sesuatu yang memainkan peran sebagai ulasan retriever. Saya mungkin akan memberikan antarmuka IRetrieveReviews. Anda dapat menempatkan ini di konstruktor produk (Injeksi Ketergantungan). Jika Anda ingin mengubah cara ulasan diambil, Anda dapat melakukannya dengan mudah dengan menyuntikkan kolaborator lain - a TwitterReviewRetrieveratau AmazonReviewRetrieveratau MultipleSourceReviewRetrieveratau apa pun yang Anda butuhkan.

Keduanya sekarang memiliki satu tanggung jawab (menjadi pilihan untuk semua hal yang terkait dengan produk, dan masing-masing mengambil ulasan), dan di masa depan perilaku produk sehubungan dengan ulasan dapat dimodifikasi tanpa benar-benar mengubah produk (Anda dapat memperpanjangnya) sebagai ProductWithReviewsjika Anda benar-benar ingin bertele-tele tentang prinsip-prinsip PADAT Anda, tetapi ini akan cukup baik bagi saya).


Kedengarannya seperti pola DAO yang sangat umum dalam perangkat lunak berorientasi layanan / komponen. Saya suka ide ini karena itu menyatakan bahwa mengambil objek bukan tanggung jawab objek-objek ini. Namun, karena saya lebih suka cara berorientasi objek daripada berorientasi layanan.
Wolfgang

1
Menggunakan antarmuka seperti IRetrieveReviewsmenghentikannya berorientasi layanan - tidak menentukan apa yang mendapat ulasan, atau bagaimana, atau kapan. Mungkin ini layanan dengan banyak metode untuk hal-hal seperti ini. Mungkin kelas yang melakukan satu hal itu. Mungkin repositori, atau permintaan HTTP ke server. Kamu tidak tahu. Anda seharusnya tidak tahu. Itulah intinya.
Lunivore

Ya, jadi itu akan menjadi implementasi dari pola strategi. Yang akan menjadi argumen untuk kelas ketiga. (A) dan (B) tidak akan mendukung ini. Pencari pasti akan menggunakan ORM, jadi tidak ada alasan untuk mengganti algoritma. Maaf jika saya tidak jelas tentang ini dalam pertanyaan saya.
Wolfgang

3

Saya akan memiliki kelas ProductsReview. Anda mengatakan Ulasan itu baru. Itu tidak berarti itu bisa saja apa saja. Masih harus memiliki satu alasan untuk berubah. Jika Anda mengubah cara Anda mendapatkan ulasan untuk alasan apa pun, Anda harus mengubah kelas Review.

Itu tidak benar.

Anda meletakkan metode statis di kelas Peninjauan karena ... mengapa? Bukankah itu yang sedang Anda perjuangkan? Bukankah itu keseluruhan masalahnya?

Kalau begitu jangan. Buat kelas yang satu-satunya tanggung jawab adalah mendapatkan ulasan produk. Anda kemudian dapat subklas ke ProductReviewsByStartRating apa pun. Atau subklas untuk mendapatkan ulasan untuk kelas produk.


Saya tidak setuju bahwa memiliki metode Ulasan akan melanggar SRP. Pada Produk, itu akan tetapi tidak pada Review. Masalah saya di sana adalah statis. Jika saya memindahkan metode ke beberapa kelas ketiga masih statis dan memiliki parameter produk.
Wolfgang

Jadi Anda mengatakan bahwa satu-satunya tanggung jawab untuk kelas Ulasan, satu-satunya alasan untuk mengubahnya, adalah apakah untuk metode statis Produk perlu diubah? Tidak ada fungsi lain di kelas Ulasan?
ElGringoGrande

Ada banyak hal di Ulasan. Tapi saya pikir bahwa forProduct (Produk) akan cocok untuk itu. Menemukan Ulasan sangat tergantung pada atribut dan struktur Tinjauan (pengidentifikasi unik mana, yang mana atribut perubahan lingkup?). Namun, Produk tidak tahu dan tidak boleh tahu tentang Ulasan.
Wolfgang

3

Saya tidak akan menempatkan fungsionalitas 'Dapatkan Ulasan untuk Produk' baik di Productkelas maupun di Reviewkelas ...

Anda memiliki tempat untuk mengambil Produk Anda, bukan? Sesuatu dengan GetProductById(int productId)dan mungkin GetProductsByCategory(int categoryId)dan seterusnya.

Demikian juga, Anda harus memiliki tempat untuk mengambil Ulasan Anda, dengan GetReviewbyId(int reviewId)dan mungkin a GetReviewsForProduct(int productId).

Ketika saya ingin mengubah cara ulasan dikumpulkan untuk suatu produk, saya harus mengubah kelas Produk.

Jika Anda memisahkan akses data Anda dari kelas domain Anda, Anda tidak akan perlu mengubah baik kelas domain ketika Anda mengubah cara review dikumpulkan.


Ini adalah bagaimana saya akan menanganinya, dan saya bingung (dan khawatir tentang apakah ide-ide saya sendiri tepat) oleh kurangnya jawaban ini sampai posting Anda. Jelas bahwa kelas yang mewakili laporan atau representasi suatu produk tidak boleh bertanggung jawab untuk benar-benar mengambil yang lain. Beberapa jenis layanan penyedia data harus menangani ini.
CodexArcanum

@ CodexArcanum Ya, Anda tidak sendirian. :-)
Eric King

2

Pola dan prinsip adalah pedoman, bukan aturan yang tertulis di batu. Menurut pendapat saya pertanyaannya bukan apakah lebih baik untuk mengikuti prinsip-prinsip SOLID atau untuk menjaga antarmuka yang lebih sederhana. Apa yang harus Anda tanyakan pada diri sendiri adalah apa yang lebih mudah dibaca dan dipahami oleh sebagian besar orang. Seringkali ini berarti harus sedekat mungkin dengan domain.

Dalam hal ini saya lebih suka solusi (B) karena bagi saya titik awalnya adalah Produk, bukan Ulasan tetapi bayangkan Anda sedang menulis perangkat lunak untuk mengelola ulasan. Dalam hal ini pusat adalah Tinjauan sehingga solusi (A) mungkin lebih disukai.

Ketika saya memiliki banyak metode seperti ini ("koneksi" antar kelas), saya lepaskan semuanya di luar dan saya buat satu (atau lebih) kelas statis baru untuk mengaturnya. Biasanya Anda dapat melihatnya sebagai kueri atau jenis repositori.


Saya juga memikirkan ide semantik dari kedua ujung ini dan mana yang "lebih kuat" sehingga akan mengklaim sang pencari. Itu sebabnya saya datang dengan (B). Ini juga akan membantu menciptakan hierarki "abstraksi" kelas bisnis. Produk adalah objek dasar belaka di mana Tinjauan lebih tinggi. Jadi, Review dapat mereferensikan Produk tetapi tidak sebaliknya. Dengan cara ini, siklus referensi di antara kelas-kelas bisnis dapat dihindari yang akan menjadi masalah lain dalam daftar saya diselesaikan.
Wolfgang

Saya pikir untuk satu set kelas kecil itu bisa menyenangkan bahkan untuk memberikan KEDUA metode (A) dan (B). Terlalu banyak UI tidak dikenal pada saat penulisan logika ini.
Adriano Repetti

1

Produk Anda hanya dapat mendelegasikan ke metode Ulasan statis Anda, dalam hal ini Anda menyediakan antarmuka yang nyaman di lokasi alami (Product.getReviews) tetapi detail implementasi Anda ada di Review.getForProduct.

SOLID adalah pedoman dan harus menghasilkan antarmuka yang sederhana dan masuk akal. Atau, Anda bisa mendapatkan SOLID dari antarmuka yang sederhana dan masuk akal. Ini semua tentang manajemen ketergantungan dalam kode. Tujuannya adalah untuk meminimalkan ketergantungan yang menciptakan gesekan dan menciptakan hambatan untuk perubahan yang tak terhindarkan.


Saya tidak yakin saya menginginkan ini. Dengan cara ini, saya memiliki metode di kedua tempat yang bagus karena pengembang lain tidak perlu mencari di kedua kelas untuk melihat di mana saya meletakkannya. Di sisi lain, saya akan memiliki kedua kelemahan dari metode statis dan perubahan kelas Produk "tertutup". Saya tidak yakin delegasi membantu keluar dari masalah gesekan. Jika ada alasan untuk mengubah pencari, itu pasti akan menghasilkan perubahan tanda tangan dan karenanya perubahan dalam Produk lagi.
Wolfgang

Kunci OCP adalah Anda dapat mengganti implementasi tanpa mengubah kode Anda. Dalam praktiknya ini terkait erat dengan Pergantian Liskov. Satu implementasi harus dapat digantikan dengan yang lain. Anda mungkin memiliki DatabaseReviews dan SoapReviews sebagai kelas yang berbeda yang mengimplementasikan IGetReviews.getReviews dan getReviewsForProducts. Kemudian sistem Anda Terbuka untuk ekstensi (mengubah cara mendapatkan ulasan) dan Ditutup untuk modifikasi (ketergantungan pada IGetReviews tidak rusak). Itulah yang saya maksud dengan manajemen ketergantungan. Dalam kasus Anda, Anda harus memodifikasi kode Anda untuk mengubah perilakunya.
pfries

Bukankah itu Prinsip Ketergantungan Pembalikan (DIP) yang Anda gambarkan?
Wolfgang

Itu prinsip yang terkait. Titik substitusi Anda dicapai oleh DIP.
pfries

1

Saya memiliki pandangan yang sedikit berbeda dari sebagian besar jawaban lainnya. Saya pikir itu Productdan Reviewpada dasarnya adalah objek transfer data (DTO). Dalam kode saya, saya mencoba membuat DTO / Entitas saya menghindari perilaku. Itu hanyalah API yang bagus untuk menyimpan keadaan model saya saat ini.

Ketika Anda berbicara tentang OO dan SOLID Anda biasanya berbicara tentang "objek" yang tidak mewakili negara (tentu saja), tetapi sebaliknya mewakili beberapa jenis layanan yang menjawab pertanyaan untuk Anda, atau yang Anda dapat mendelegasikan beberapa pekerjaan Anda . Misalnya:

interface IProductRepository
{
    void SaveNewProduct(IProduct product);
    IProduct GetProductById(ProductId productId);
    bool TryGetProductByName(string name, out IProduct product);
}

interface IProduct
{
    ProductId Id { get; }
    string Name { get; }
}

class ExistingProduct : IProduct
{
    public ProductId Id { get; private set; }
    public string Name { get; private set; }
}

Maka aktual Anda ProductRepositoryakan mengembalikan nilai ExistingProductuntuk GetProductByProductIdmetode, dll.

Sekarang Anda mengikuti prinsip tanggung jawab tunggal (apa pun yang mewarisi dari IProducthanya berpegang pada status, dan apa pun yang mewarisi dari IProductRepositorybertanggung jawab untuk mengetahui bagaimana bertahan dan rehidrasi model data Anda).

Jika Anda mengubah skema database Anda, Anda dapat mengubah implementasi repositori Anda tanpa mengubah DTO Anda, dll.

Jadi, singkatnya, saya kira saya tidak akan memilih pilihan Anda. :)


0

Metode statis mungkin lebih sulit untuk diuji, tetapi itu tidak berarti Anda tidak dapat menggunakannya - Anda hanya perlu mengaturnya sehingga Anda tidak perlu mengujinya.

Buat keduanya product.GetReviews dan Review.ForProduct metode satu baris yang mirip

new ReviewService().GetReviews(productID);

ReviewService berisi semua kode yang lebih kompleks dan memiliki antarmuka yang dirancang untuk diuji, tetapi tidak terpapar langsung ke pengguna.

Jika Anda harus memiliki cakupan 100%, mintalah tes integrasi Anda memanggil metode kelas produk / ulasan.

Mungkin membantu jika Anda berpikir tentang desain API publik daripada desain kelas - dalam konteks itu antarmuka sederhana dengan pengelompokan logis adalah yang terpenting - struktur kode aktual hanya penting bagi pengembang.

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.