Terlalu banyak abstraksi membuat kode sulit untuk diperluas

9

Saya menghadapi masalah dengan apa yang saya rasa terlalu banyak abstraksi dalam basis kode (atau setidaknya menghadapinya). Sebagian besar metode dalam basis kode telah diabstraksi untuk mengambil dalam induk tertinggi A dalam basis kode, tetapi anak B dari orangtua ini memiliki atribut baru yang memengaruhi logika beberapa metode tersebut. Masalahnya adalah bahwa atribut tersebut tidak dapat diperiksa dalam metode tersebut karena input diabstraksi menjadi A, dan A tentu saja tidak memiliki atribut ini. Jika saya mencoba membuat metode baru untuk menangani B secara berbeda, itu dipanggil untuk duplikasi kode. Saran dari pimpinan teknologi saya adalah membuat metode bersama yang menggunakan parameter boolean, tetapi masalah dengan ini adalah bahwa beberapa orang melihat ini sebagai "aliran kontrol tersembunyi," di mana metode bersama memiliki logika yang mungkin tidak terlihat oleh pengembang masa depan , dan juga metode bersama ini akan tumbuh terlalu rumit / berbelit-belit sekali jika atribut masa depan perlu ditambahkan, bahkan jika dipecah menjadi metode bersama yang lebih kecil. Ini juga meningkatkan sambungan, mengurangi kekompakan, dan melanggar prinsip tanggung jawab tunggal, yang ditunjukkan oleh seseorang di tim saya.

Pada dasarnya, banyak abstraksi dalam basis kode ini membantu mengurangi duplikasi kode, tetapi itu membuat metode perluasan / perubahan lebih sulit ketika mereka dibuat untuk mengambil abstraksi tertinggi. Apa yang harus saya lakukan dalam situasi seperti ini? Saya berada di pusat untuk disalahkan, meskipun semua orang tidak bisa menyetujui apa yang mereka anggap baik, jadi pada akhirnya itu menyakitkan saya.

YamizGers
sumber
10
menambahkan contoh kode untuk "" masalah "" yang buta huruf akan membantu untuk memahami situasi lebih banyak
Seabizkit
Saya pikir ada dua prinsip SOLID yang dilanggar di sini. Tanggung Jawab Tunggal - jika Anda menyerahkan Boolean ke suatu fungsi yang seharusnya mengendalikan perilaku, fungsi tersebut tidak lagi memiliki tanggung jawab tunggal. Yang lain adalah prinsip Pergantian Liskov. Bayangkan ada fungsi yang mengambil di kelas A sebagai parameter. Jika Anda lulus di kelas B alih-alih A, apakah fungsi dari fungsi itu akan rusak?
bobek
Saya menduga metode A cukup panjang dan melakukan lebih dari satu hal. Apakah itu masalahnya?
Rad80

Jawaban:

27

Jika saya mencoba membuat metode baru untuk menangani B secara berbeda, itu dipanggil untuk duplikasi kode.

Tidak semua duplikasi kode dibuat sama.

Katakanlah Anda memiliki metode yang mengambil dua parameter dan menambahkannya bersama-sama disebut total(). Katakanlah Anda memiliki satu lagi yang dipanggil add(). Implementasinya terlihat sangat identik. Haruskah mereka digabung menjadi satu metode? TIDAK!!!

Prinsip Jangan-Ulangi-Sendiri atau KERING bukan tentang pengulangan kode. Ini tentang menyebarkan keputusan, ide, sehingga jika Anda pernah mengubah ide Anda, Anda harus menulis ulang di mana-mana Anda menyebarkan ide itu. Blegh. Itu buruk. Jangan lakukan itu. Alih-alih gunakan KERING untuk membantu Anda membuat keputusan di satu tempat .

Prinsip KERING (Jangan Ulangi Diri Sendiri) menyatakan:

Setiap pengetahuan harus memiliki representasi tunggal, tidak ambigu, otoritatif dalam suatu sistem.

wiki.c2.com - Jangan Ulangi Diri Anda Sendiri

Tapi KERING bisa rusak menjadi kebiasaan memindai kode mencari implementasi yang serupa yang tampaknya seperti copy dan paste dari tempat lain. Ini adalah bentuk KERING mati otak. Sial, Anda bisa melakukan ini dengan alat analisis statis. Itu tidak membantu karena mengabaikan titik KERING yang membuat kode fleksibel.

Jika persyaratan total saya berubah, saya mungkin harus mengubah totalimplementasi saya . Itu tidak berarti saya perlu mengubah addimplementasi saya . Jika beberapa goober menghancurkan mereka bersama menjadi satu metode saya sekarang dalam sedikit rasa sakit yang tidak perlu.

Berapa banyak rasa sakit? Tentunya saya hanya bisa menyalin kode dan membuat metode baru ketika saya membutuhkannya. Jadi bukan masalah besar kan? Malarky! Jika tidak ada yang lain Anda membayar saya nama baik! Nama baik sulit didapat dan tidak merespons dengan baik ketika Anda mengutak-atik maknanya. Nama baik, yang memperjelas maksud, lebih penting daripada risiko Anda menyalin bug yang, sejujurnya, lebih mudah diperbaiki ketika metode Anda memiliki nama yang tepat.

Jadi saran saya adalah untuk berhenti membiarkan reaksi brengsek lutut ke kode serupa mengikat basis kode Anda di simpul. Saya tidak mengatakan Anda bebas untuk mengabaikan fakta bahwa ada metode dan bukannya copy dan paste mau tak mau. Tidak, masing-masing metode harus memiliki nama baik yang mendukung satu gagasan tentang itu. Jika implementasinya cocok dengan implementasi beberapa ide bagus lainnya, sekarang, hari ini, siapa yang peduli?

Di sisi lain, jika Anda memiliki sum()metode yang memiliki implementasi yang identik atau bahkan berbeda dari total(), namun semakin lama kebutuhan total Anda berubah, Anda harus berubah sum()maka ada kemungkinan bahwa ini adalah ide yang sama dengan dua nama berbeda. Tidak hanya kode akan lebih fleksibel jika mereka digabungkan, itu akan kurang membingungkan untuk digunakan.

Adapun parameter Boolean, ya itu bau kode jahat. Tidak hanya aliran kontrol itu masalah, lebih buruk itu menunjukkan bahwa Anda memotong abstraksi pada titik yang buruk. Abstraksi seharusnya membuat hal-hal lebih sederhana untuk digunakan, tidak lebih rumit. Melewati bools ke metode untuk mengontrol perilakunya seperti menciptakan bahasa rahasia yang memutuskan metode mana yang benar-benar Anda panggil. Ow! Jangan lakukan itu padaku. Beri setiap metode namanya masing-masing kecuali Anda memiliki polimorfisme yang jujur .

Sekarang, Anda tampak lelah dengan abstraksi. Itu terlalu buruk karena abstraksi adalah hal yang luar biasa ketika dilakukan dengan baik. Anda sering menggunakannya tanpa memikirkannya. Setiap kali Anda mengendarai mobil tanpa harus memahami sistem rack and pinion, setiap kali Anda menggunakan perintah cetak tanpa memikirkan interupsi OS, dan setiap kali Anda menyikat gigi tanpa memikirkan setiap bulu individu.

Tidak, masalah yang tampaknya Anda hadapi adalah abstraksi yang buruk. Abstraksi dibuat untuk melayani tujuan yang berbeda dari kebutuhan Anda. Anda perlu antarmuka sederhana menjadi objek kompleks yang memungkinkan Anda meminta agar kebutuhan Anda dipenuhi tanpa harus memahami objek tersebut.

Ketika Anda menulis kode klien yang menggunakan objek lain, Anda tahu apa kebutuhan Anda dan apa yang Anda butuhkan dari objek itu. Tidak. Itu sebabnya kode klien memiliki antarmuka. Ketika Anda klien, tidak ada yang bisa memberi tahu Anda apa kebutuhan Anda selain Anda. Anda membuat antarmuka yang menunjukkan apa kebutuhan Anda dan menuntut apa pun yang diserahkan kepada Anda memenuhi kebutuhan itu.

Itu adalah abstraksi. Sebagai klien saya bahkan tidak tahu apa yang saya bicarakan. Saya hanya tahu apa yang saya butuhkan darinya. Jika itu berarti Anda harus membungkus sesuatu untuk mengubah antarmuka sebelum menyerahkannya kepada saya baik-baik saja. Saya tidak peduli. Lakukan saja apa yang perlu saya lakukan. Berhentilah membuatnya menjadi rumit.

Jika saya harus melihat ke dalam abstraksi untuk memahami bagaimana menggunakannya abstraksi telah gagal. Saya seharusnya tidak perlu tahu cara kerjanya. Itu berhasil. Berikan nama yang bagus dan jika saya melihat ke dalam, saya seharusnya tidak terkejut dengan apa yang saya temukan. Jangan membuat saya terus mencari ke dalam untuk mengingat bagaimana menggunakannya.

Ketika Anda bersikeras bahwa abstraksi bekerja dengan cara ini, jumlah level di belakangnya tidak masalah. Selama Anda tidak mencari di belakang abstraksi. Anda bersikeras bahwa abstraksi sesuai dengan kebutuhan Anda agar tidak beradaptasi dengannya. Agar ini berfungsi, ia harus mudah digunakan, memiliki nama baik, dan tidak bocor .

Itulah sikap yang menelurkan Ketergantungan Injeksi (atau hanya lewat referensi jika Anda sekolah tua seperti saya). Ia bekerja dengan baik dengan lebih memilih komposisi dan delegasi daripada warisan . Sikap itu berjalan dengan banyak nama. Yang favorit saya adalah kirim, jangan tanya .

Saya bisa menenggelamkan Anda dalam prinsip-prinsip sepanjang hari. Dan sepertinya rekan kerja Anda sudah seperti itu. Tapi ada satu hal: tidak seperti bidang teknik lainnya, perangkat lunak ini berusia kurang dari 100 tahun. Kita semua masih mencari tahu. Jadi jangan biarkan seseorang dengan banyak buku terdengar mengintimidasi belajar menggertak Anda menjadi menulis sulit untuk membaca kode. Dengarkan mereka tetapi bersikeras mereka masuk akal. Jangan mengambil apa pun dengan iman. Orang-orang yang mengkode dengan cara tertentu hanya karena mereka diberitahu ini-itu-cara-tanpa tahu mengapa membuat kekacauan terbesar dari semua.

candied_orange
sumber
Saya sepenuh hati setuju. KERING adalah akronim tiga huruf untuk frasa tiga kata Jangan Ulangi Diri Anda, yang pada gilirannya merupakan artikel 14 halaman di wiki . Jika semua yang Anda lakukan adalah membisikkan tiga huruf secara membabi buta tanpa membaca dan memahami artikel 14 halaman, Anda akan mendapat masalah. Ini juga terkait erat dengan Once And Only Once (OAOO) dan lebih longgar terkait dengan Single Point Of Truth (SPOT) / Single Source Of Truth (SSOT) .
Jörg W Mittag
"Implementasinya terlihat sangat identik. Haruskah mereka digabung menjadi satu metode? TIDAK !!!" - Kebalikannya juga benar: hanya karena dua potong kode berbeda tidak berarti mereka bukan duplikat. Ada kutipan hebat oleh Ron Jeffries di halaman wiki OAOO : "Saya pernah melihat Beck mendeklarasikan dua tambalan kode yang hampir sama sekali berbeda menjadi" duplikasi ", ubahlah sehingga mereka ADA duplikasi, dan kemudian hapus duplikasi yang baru dimasukkan untuk muncul dengan sesuatu yang jelas lebih baik. "
Jörg W Mittag
@ JörgWMittag tentu saja. Yang penting adalah idenya. Jika Anda menduplikasi ide dengan kode yang terlihat berbeda, Anda masih melanggar kering.
candied_orange
Saya harus membayangkan artikel 14 halaman tentang tidak mengulangi diri Anda sendiri akan cenderung terulang.
Chuck Adams
7

Pepatah yang biasa kita semua baca di sini dan di sana adalah:

Semua masalah dapat diselesaikan dengan menambahkan lapisan abstraksi lainnya.

Ya, ini tidak benar! Contoh Anda menunjukkannya. Karena itu saya akan mengusulkan pernyataan yang sedikit dimodifikasi (jangan ragu untuk menggunakan kembali ;-)):

Setiap masalah dapat diselesaikan dengan menggunakan tingkat abstraksi yang BENAR.

Ada dua masalah berbeda dalam kasus Anda:

  • yang over-generalisasi yang disebabkan dengan menambahkan setiap metode di tingkat abstrak;
  • yang fragmentasi dari perilaku beton yang mengarah pada kesan tidak mendapatkan gambaran besar dan merasa kehilangan. Sedikit seperti di loop acara windows.

Keduanya terkait:

  • jika Anda abstrak metode di mana setiap spesialisasi melakukannya secara berbeda, semuanya baik-baik saja. Tidak ada yang memiliki masalah memahami bahwa Shapedapat menghitungnya dengan surface()cara khusus.
  • Jika Anda abstrak beberapa operasi di mana ada pola perilaku umum yang umum, Anda memiliki dua pilihan:

    • Anda akan mengulangi perilaku umum dalam setiap spesialisasi: ini sangat berlebihan; dan sulit untuk dipertahankan, terutama untuk memastikan bahwa bagian yang umum tetap sejalan di seluruh spesialisasi:
    • Anda menggunakan beberapa varian dari pola metode templat : ini memungkinkan Anda untuk memperhitungkan perilaku umum dengan menggunakan metode abstrak tambahan yang dapat dengan mudah dikhususkan. Ini tidak terlalu berlebihan, tetapi perilaku tambahan cenderung menjadi sangat terpecah. Terlalu banyak berarti bahwa itu mungkin terlalu abstrak.

Selain itu, pendekatan ini dapat menghasilkan efek kopling abstrak di tingkat desain. Setiap kali Anda ingin menambahkan semacam perilaku khusus baru, Anda harus mengabstraksikannya, mengubah induk abstrak, dan memperbarui semua kelas lainnya. Itu bukan jenis perubahan propagasi yang diinginkan seseorang. Dan itu tidak benar-benar dalam semangat abstraksi tidak tergantung pada spesialisasi (setidaknya dalam desain).

Saya tidak tahu desain Anda dan tidak bisa membantu lagi. Mungkin itu benar-benar masalah yang sangat kompleks dan abstrak dan tidak ada cara yang lebih baik. Tapi apa kemungkinannya? Gejala-gejala overgeneralisasi ada di sini. Mungkin sudah waktunya untuk melihatnya lagi, dan mempertimbangkan komposisi daripada generalisasi ?

Christophe
sumber
5

Setiap kali saya melihat metode di mana perilaku mengaktifkan jenis parameternya, saya segera mempertimbangkan terlebih dahulu apakah metode itu benar-benar milik pada parameter metode. Misalnya, alih-alih memiliki metode seperti:

public void sort(List values) {
    if (values instanceof LinkedList) {
        // do efficient linked list sort
    } else { // ArrayList
        // do efficient array list sort
    }
}

Saya akan melakukan ini:

values.sort();

// ...

class ArrayList {
    public void sort() {
        // do efficient array list sort
    }
}

class LinkedList {
    public void sort() {
        // do efficient linked list sort
    }
}

Kami memindahkan perilaku ke tempat yang tahu kapan harus menggunakannya. Kami membuat abstraksi nyata di mana Anda tidak perlu tahu jenis atau detail implementasi. Untuk situasi Anda, mungkin lebih masuk akal untuk memindahkan metode ini dari kelas asli (yang akan saya panggil O) untuk mengetik Adan menimpanya dalam jenis B. Jika metode ini dipanggil doItpada beberapa objek, pindah doItke Adan timpa dengan perilaku yang berbeda di B. Jika ada bit data dari tempat doItawalnya disebut, atau jika metode ini digunakan di tempat yang cukup, Anda dapat meninggalkan metode asli dan mendelegasikan:

class O {
    int x;
    int y;

    public void doIt(A a) {
        a.doIt(this.x, this.y);
    }
}

Kita bisa menyelam lebih dalam lagi. Mari kita lihat saran untuk menggunakan parameter boolean dan melihat apa yang bisa kita pelajari tentang cara rekan kerja Anda berpikir. Usulannya adalah melakukan:

public void doIt(A a, boolean isTypeB) {
    if (isTypeB) {
        // do B stuff
    } else { 
        // do A stuff
    }
}

Ini terlihat sangat mengerikan seperti yang instanceofsaya gunakan pada contoh pertama saya, kecuali bahwa kita mengeksternalkan cek itu. Ini berarti bahwa kita harus menyebutnya dengan salah satu dari dua cara:

o.doIt(a, a instanceof B);

atau:

o.doIt(a, true); //or false

Pertama-tama, titik panggilan tidak tahu jenisnya A. Karena itu, haruskah kita melewati boolean sepanjang jalan? Apakah itu benar-benar pola yang kita inginkan di seluruh basis kode? Apa yang terjadi jika ada tipe ketiga yang perlu kita pertanggungjawabkan? Jika ini adalah bagaimana metode ini dipanggil, kita harus memindahkannya ke tipe dan membiarkan sistem memilih implementasi untuk kita secara polimorfis.

Dengan cara kedua, kita harus sudah tahu jenis adi titik panggilan. Biasanya itu berarti kita membuat instance di sana, atau mengambil instance dari tipe itu sebagai parameter. Membuat metode Oyang dibutuhkan di Bsini akan berhasil. Kompiler akan tahu metode mana yang harus dipilih. Ketika kita mengemudi melalui perubahan seperti ini, duplikasi lebih baik daripada menciptakan abstraksi yang salah , setidaknya sampai kita mengetahui ke mana kita benar-benar pergi. Tentu saja, saya menyarankan agar kita tidak benar-benar melakukan apa pun yang telah kita ubah ke titik ini.

Kita perlu melihat lebih dekat hubungan antara Adan B. Secara umum, kita diberitahu bahwa kita harus memilih komposisi daripada warisan . Ini tidak benar dalam setiap kasus, tetapi itu benar dalam jumlah kasus yang mengejutkan begitu kita menggali. BWarisan dari A, artinya kita percaya Badalah A. Bharus digunakan seperti A, kecuali bahwa itu bekerja sedikit berbeda. Tetapi apa perbedaan itu? Bisakah kita memberi perbedaan nama yang lebih konkret? Bukankah Bini sebuah A, tetapi benar-benar Amemiliki Xyang bisa A'atau B'? Akan seperti apa kode kita jika kita melakukan itu?

Jika kami memindahkan metode ke Aseperti yang disarankan sebelumnya, kami dapat menyuntikkan instance Xke A, dan mendelegasikan metode itu ke X:

class A {
    X x;
    A(X x) {
        this.x = x;
    }

    public void doIt(int x, int y) {
        x.doIt(x, y);
    }
}

Kita dapat menerapkan A'dan B', serta menyingkirkan B. Kami telah memperbaiki kode dengan memberikan nama pada konsep yang mungkin lebih implisit, dan memungkinkan kami untuk mengatur perilaku itu saat runtime alih-alih waktu kompilasi. Asebenarnya menjadi kurang abstrak juga. Alih-alih hubungan warisan yang diperpanjang, itu memanggil metode pada objek yang didelegasikan. Objek itu abstrak, tetapi lebih fokus hanya pada perbedaan implementasi.

Ada satu hal terakhir yang harus dilihat. Mari kembali ke proposal rekan kerja Anda. Jika pada semua situs panggilan kita secara eksplisit mengetahui jenis yang Akita miliki, maka kita harus membuat panggilan seperti:

B b = new B();
o.doIt(b, true);

Kami berasumsi sebelumnya ketika menulis yang Amemiliki salah Xsatu A'atau B'. Tetapi mungkin bahkan anggapan ini tidak benar. Apakah ini satu-satunya tempat di mana perbedaan antara Adan Bhal-hal? Jika ya, maka mungkin kita bisa mengambil pendekatan yang sedikit berbeda. Kami masih memiliki Xyang salah A'atau B', tetapi bukan milik A. Hanya O.doItpeduli tentang itu, jadi mari kita serahkan saja ke O.doIt:

class O {
    int x;
    int y;

    public void doIt(A a, X x) {
        x.doIt(a, x, y);
    }
}

Sekarang situs panggilan kami terlihat seperti:

A a = new A();
o.doIt(a, new B'());

Sekali lagi, Bmenghilang, dan abstraksi bergerak ke yang lebih fokus X. Namun, kali Aini bahkan lebih sederhana dengan mengetahui lebih sedikit. Itu bahkan kurang abstrak.

Penting untuk mengurangi duplikasi dalam basis kode, tetapi kita harus mempertimbangkan mengapa duplikasi terjadi di tempat pertama. Duplikasi bisa menjadi tanda abstraksi yang lebih dalam yang berusaha keluar.

cbojar
sumber
1
Saya terkejut bahwa contoh kode "buruk" yang Anda berikan di sini mirip dengan apa yang akan saya lakukan dalam bahasa non-OO. Saya bertanya-tanya apakah mereka mempelajari pelajaran yang salah dan membawanya ke dunia OO sebagai cara mereka berkode?
Baldrickk
1
@Baldrickk Setiap paradigma membawa cara berpikirnya sendiri, dengan kelebihan dan kekurangannya yang unik. Dalam Haskell fungsional, pencocokan pola akan menjadi pendekatan yang lebih baik. Meskipun dalam bahasa seperti itu, beberapa aspek dari masalah aslinya juga tidak mungkin terjadi.
cbojar
1
Ini jawaban yang benar. Metode yang mengubah implementasi berdasarkan pada jenis operasinya harus menjadi metode jenis itu.
Roman Reiner
0

Abstraksi oleh Waris bisa menjadi sangat jelek. Hirarki kelas paralel dengan pabrik-pabrik khas. Refactoring bisa menjadi sakit kepala. Dan juga perkembangan selanjutnya, tempat di mana Anda berada.

Ada alternatif: titik ekstensi , abstraksi yang ketat, dan kustomisasi berjenjang. Katakanlah satu kustomisasi pelanggan pemerintah, berdasarkan kustomisasi itu untuk kota tertentu.

Peringatan: Sayangnya ini bekerja paling baik ketika semua (atau sebagian besar) kelas dibuat memanjang. Tidak ada pilihan untuk Anda, mungkin dalam jumlah kecil.

Ekstensibilitas ini berfungsi dengan memiliki kelas basis objek yang dapat diperpanjang menampung ekstensi:

void f(CreditorBO creditor) {
    creditor.as(AllowedCreditorBO.class).ifPresent(allowedCreditor -> ...);
}

Secara internal ada pemetaan malas objek ke objek diperluas oleh kelas ekstensi.

Untuk kelas dan komponen GUI, ekstensibilitasnya sama, sebagian dengan warisan. Menambahkan tombol dan semacamnya.

Dalam kasus Anda, validasi harus melihat apakah diperpanjang dan memvalidasi dirinya terhadap ekstensi. Memperkenalkan poin ekstensi hanya untuk satu kasus, tambahkan kode yang tidak bisa dimengerti, tidak bagus.

Jadi tidak ada solusi selain berusaha bekerja dalam konteks saat ini.

Joop Eggen
sumber
0

'kontrol aliran tersembunyi' terdengar terlalu mudah bagi saya.
Setiap konstruk atau elemen yang diambil di luar konteks mungkin memiliki karakteristik itu.

Abstraksi itu baik. Saya marahi mereka dengan dua pedoman:

  • Lebih baik tidak abstrak terlalu cepat. Tunggu lebih banyak contoh pola sebelum abstracing. 'Lebih' tentu saja subyektif dan spesifik untuk situasi yang sulit.

  • Hindari terlalu banyak level abstraksi hanya karena abstraksi itu baik. Seorang programmer harus menjaga level-level itu di kepala mereka untuk kode baru atau yang diubah ketika mereka menyelami basis kode dan pergi 12 level dalam. Keinginan untuk kode yang disarikan dengan baik dapat menyebabkan begitu banyak level sehingga sulit bagi banyak orang untuk mengikuti. Ini juga mengarah pada basis kode 'ninja maintained only'.

Dalam kedua kasus 'lebih banyak dan' terlalu banyak 'bukan angka tetap. Tergantung. Itu yang membuatnya sulit.

Saya juga suka artikel ini dari Sandi Metz

https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction

duplikasi jauh lebih murah daripada abstraksi yang salah
dan
lebih memilih duplikasi daripada abstraksi yang salah

Michael Durrant
sumber