Menambahkan kompleksitas untuk menghapus kode duplikat

24

Saya memiliki beberapa kelas yang semuanya diwarisi dari kelas dasar generik. Kelas dasar berisi kumpulan beberapa objek bertipe T.

Setiap kelas anak harus dapat menghitung nilai yang diinterpolasi dari koleksi objek, tetapi karena kelas anak menggunakan tipe yang berbeda, perhitungannya sedikit bervariasi dari kelas ke kelas.

Sejauh ini saya telah menyalin / menempelkan kode saya dari kelas ke kelas dan membuat sedikit modifikasi untuk masing-masing kode. Tapi sekarang saya mencoba untuk menghapus kode duplikat dan menggantinya dengan satu metode interpolasi generik di kelas dasar saya. Namun itu terbukti sangat sulit, dan semua solusi yang saya anggap terlalu rumit.

Saya mulai berpikir bahwa prinsip KERING tidak berlaku banyak dalam situasi seperti ini, tetapi itu terdengar seperti penghujatan. Berapa banyak kerumitan yang terlalu banyak ketika mencoba untuk menghapus duplikasi kode?

EDIT:

Solusi terbaik yang bisa saya dapatkan adalah dengan cara seperti ini:

Kelas dasar:

protected T GetInterpolated(int frame)
{
    var index = SortedFrames.BinarySearch(frame);
    if (index >= 0)
        return Data[index];

    index = ~index;

    if (index == 0)
        return Data[index];
    if (index >= Data.Count)
        return Data[Data.Count - 1];

    return GetInterpolatedItem(frame, Data[index - 1], Data[index]);
}

protected abstract T GetInterpolatedItem(int frame, T lower, T upper);

Kelas anak A:

public IGpsCoordinate GetInterpolatedCoord(int frame)
{
    ReadData();
    return GetInterpolated(frame);
}

protected override IGpsCoordinate GetInterpolatedItem(int frame, IGpsCoordinate lower, IGpsCoordinate upper)
{
    double ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);

    var x = GetInterpolatedValue(lower.X, upper.X, ratio);
    var y = GetInterpolatedValue(lower.Y, upper.Y, ratio);
    var z = GetInterpolatedValue(lower.Z, upper.Z, ratio);

    return new GpsCoordinate(frame, x, y, z);
}

Kelas anak B:

public double GetMph(int frame)
{
    ReadData();
    return GetInterpolated(frame).MilesPerHour;
}

protected override ISpeed GetInterpolatedItem(int frame, ISpeed lower, ISpeed upper)
{
    var ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);
    var mph = GetInterpolatedValue(lower.MilesPerHour, upper.MilesPerHour, ratio);
    return new Speed(frame, mph);
}
Phil
sumber
9
Penerapan konsep yang terlalu mikro seperti KERING dan Penggunaan Kembali Kode mengarah pada dosa yang jauh lebih besar.
Affe
1
Anda mendapatkan jawaban umum yang bagus. Mengedit untuk menyertakan fungsi contoh mungkin membantu kami menentukan apakah Anda terlalu jauh dalam contoh khusus ini.
Karl Bielefeldt
Ini tidak benar-benar jawaban, lebih dari sebuah pengamatan: Jika Anda tidak dapat dengan mudah menjelaskan apa kelas dasar terfaktor-out tidak , mungkin akan lebih baik untuk tidak memiliki satu. Cara lain untuk melihatnya adalah (saya berasumsi Anda terbiasa dengan SOLID?) 'Apakah ada kemungkinan konsumen dari fungsi ini memerlukan penggantian Liskov'? Jika tidak ada kemungkinan kasus bisnis untuk konsumen umum dari fungsi interpolasi, kelas dasar tidak ada nilainya.
Tom W
1
Hal pertama adalah mengumpulkan triplet X, Y, Z ke dalam tipe Posisi, dan menambahkan interpolasi ke tipe itu sebagai anggota atau mungkin metode statis: Posisi interpolasi (Posisi lain, rasio).
kevin cline

Jawaban:

30

Di satu sisi, Anda menjawab pertanyaan Anda sendiri dengan komentar di paragraf terakhir:

Saya mulai berpikir bahwa prinsip KERING tidak berlaku banyak dalam situasi seperti ini, tetapi kedengarannya seperti penghujatan .

Kapan pun Anda menemukan beberapa latihan yang tidak terlalu praktis untuk menyelesaikan masalah Anda, jangan mencoba menggunakan praktik itu secara religius (kata penistaan adalah semacam peringatan untuk ini). Kebanyakan praktik memiliki mereka whens dan mengapa dan bahkan jika mereka menutupi 99% dari semua kasus yang mungkin, masih ada yang 1% di mana Anda mungkin perlu pendekatan yang berbeda.

Khususnya, berkenaan dengan KERING , saya juga menemukan bahwa kadang-kadang lebih baik bahkan memiliki beberapa keping kode yang digandakan tetapi sederhana daripada satu monster raksasa yang membuat Anda merasa mual ketika Anda melihatnya.

Yang sedang berkata, keberadaan kasus tepi ini tidak boleh digunakan sebagai alasan untuk sloppy copy & paste coding atau benar-benar kekurangan modul yang dapat digunakan kembali. Sederhananya, jika Anda tidak tahu bagaimana menulis kode generik dan mudah dibaca untuk beberapa masalah dalam beberapa bahasa, maka mungkin kurang buruk untuk memiliki redundansi. Pikirkan siapa pun yang harus menjaga kode. Apakah mereka akan lebih mudah hidup dengan redundansi atau kebingungan?

Saran yang lebih spesifik tentang contoh khusus Anda. Anda mengatakan bahwa perhitungan ini serupa namun sedikit berbeda . Anda mungkin ingin mencoba memecah formula perhitungan Anda menjadi subformula yang lebih kecil dan kemudian meminta semua perhitungan Anda yang sedikit berbeda memanggil fungsi pembantu ini untuk melakukan sub perhitungan. Anda akan menghindari situasi di mana setiap perhitungan tergantung pada beberapa kode yang digeneralisasi secara berlebihan dan Anda masih akan memiliki beberapa tingkat penggunaan kembali.

Goran Jovic
sumber
10
Poin lain tentang yang serupa namun sedikit berbeda adalah bahwa meskipun mereka terlihat serupa dalam kode, tidak berarti mereka harus serupa dalam "bisnis". Tergantung pada apa itu tentu saja, tetapi kadang-kadang itu ide yang baik untuk menjaga hal-hal terpisah karena walaupun mereka terlihat sama, mereka mungkin didasarkan pada keputusan / persyaratan bisnis yang berbeda. Jadi, Anda mungkin ingin melihatnya sebagai perhitungan yang sangat berbeda walaupun kode mereka mungkin terlihat serupa. (Bukan aturan atau apa pun, tetapi hanya sesuatu yang perlu diingat ketika memutuskan apakah hal-hal harus digabungkan atau di-refactored :)
Svish
@Svish Poin menarik. Tidak pernah berpikir seperti itu.
Phil
17

kelas anak menggunakan tipe yang berbeda, perhitungannya sedikit berbeda dari kelas ke kelas.

Hal pertama dan terpenting adalah: Pertama-tama dengan jelas mengidentifikasi bagian mana yang berubah dan bagian mana yang TIDAK berubah di antara kelas-kelas. Setelah Anda mengidentifikasi itu, masalah Anda terpecahkan.

Lakukan itu sebagai latihan pertama sebelum memulai anjak piutang. Semua yang lain akan jatuh pada tempatnya secara otomatis setelah itu.

java_mouse
sumber
2
Baik. Ini mungkin hanya masalah fungsi berulang yang terlalu besar.
Karl Bielefeldt
8

Saya percaya bahwa hampir semua pengulangan lebih dari beberapa baris kode dapat difaktorkan dalam beberapa cara, dan hampir selalu harus demikian.

Namun, refactoring ini lebih mudah dalam beberapa bahasa daripada yang lain. Sangat mudah dalam bahasa seperti LISP, Ruby, Python, Groovy, Javascript, Lua, dll. Biasanya tidak terlalu sulit dalam C ++ menggunakan templat. Lebih menyakitkan pada C, di mana satu-satunya alat mungkin makro preprosesor. Seringkali menyakitkan di Jawa, dan kadang-kadang tidak mungkin, misalnya mencoba menulis kode generik untuk menangani beberapa tipe numerik bawaan.

Dalam bahasa yang lebih ekspresif, tidak ada pertanyaan: refactor apa pun lebih dari beberapa baris kode. Dengan bahasa yang kurang ekspresif Anda harus menyeimbangkan rasa sakit dari refactoring terhadap panjang dan stabilitas kode yang diulang. Jika kode yang diulang panjang, atau cenderung sering berubah, saya cenderung menolak bahkan jika kode yang dihasilkan agak sulit dibaca.

Saya akan menerima kode berulang hanya jika pendek, stabil, dan refactoring terlalu jelek. Pada dasarnya saya memperhitungkan hampir semua duplikasi kecuali saya menulis Java.

Tidak mungkin untuk memberikan rekomendasi spesifik untuk kasus Anda karena Anda belum memposting kode, atau bahkan menunjukkan bahasa yang Anda gunakan.

kevin cline
sumber
Lua bukan akronim.
DeadMG
@DeadMG: tercatat; merasa bebas untuk mengedit. Itu sebabnya kami memberi Anda semua reputasi itu.
kevin cline
5

Ketika Anda mengatakan kelas dasar harus melakukan algoritme, tetapi algoritme bervariasi untuk setiap sub kelas, ini terdengar seperti kandidat yang sempurna untuk Pola Templat .

Dengan ini, kelas dasar melakukan algoritma, dan ketika datang variasi untuk setiap sub kelas, itu mengacu pada metode abstrak yang merupakan tanggung jawab sub kelas untuk diimplementasikan. Pikirkan cara dan halaman ASP.NET menolak kode Anda untuk mengimplementasikan Page_Load misalnya.

Paul T Davies
sumber
4

Kedengarannya seperti "satu metode interpolasi generik" Anda di setiap kelas yang melakukan terlalu banyak dan harus dibuat menjadi metode yang lebih kecil.

Bergantung pada seberapa rumit perhitungan Anda, mengapa masing-masing "keping" perhitungan tidak dapat menjadi metode virtual seperti ini

Public Class Fraction
{
     public virtual Decimal GetNumerator(params?)
     public virtual Decimal GetDenominator(params?)
     //Some concrete method to actually compute GetNumerator / GetDenominator
}

Dan timpa potongan individual ketika Anda perlu membuat "sedikit variasi" dalam logika perhitungan.

(Ini adalah contoh yang sangat kecil dan tidak berguna tentang bagaimana Anda dapat menambahkan banyak fungsi sambil mengganti metode kecil)

brian
sumber
3

Menurut pendapat saya, Anda benar, dalam arti tertentu, KERING dapat diambil terlalu jauh. Jika dua keping kode yang serupa cenderung berevolusi ke arah yang sangat berbeda maka Anda dapat menyebabkan masalah sendiri dengan mencoba untuk tidak mengulangi diri Anda pada awalnya.

Namun, Anda juga cukup benar untuk mewaspadai pemikiran yang menghujat itu. Berusaha sangat keras untuk memikirkan pilihan Anda sebelum Anda memutuskan untuk membiarkannya.

Apakah itu, misalnya, lebih baik untuk meletakkan kode yang diulang dalam kelas utilitas / metode, daripada di kelas dasar? Lihat Memilih Komposisi daripada Warisan .

pdr
sumber
2

KERING adalah pedoman yang harus diikuti, bukan aturan yang tidak bisa dilanggar. Pada titik tertentu Anda perlu memutuskan bahwa itu tidak layak memiliki tingkat X warisan dan template Y di setiap kelas yang Anda gunakan hanya untuk mengatakan tidak ada pengulangan dalam kode ini. Beberapa pertanyaan yang bagus untuk ditanyakan adalah akankah saya membutuhkan waktu lebih lama untuk mengekstrak metode yang serupa ini dan menerapkannya sebagai satu, kemudian akan mencari semua dari mereka jika kebutuhan untuk perubahan muncul atau apakah potensi mereka untuk perubahan terjadi yang akan batalkan pekerjaan saya mengekstraksi metode ini di tempat pertama? Apakah saya pada titik di mana abstraksi tambahan mulai membuat memahami di mana atau apa yang dilakukan kode ini merupakan tantangan?

Jika Anda dapat menjawab ya untuk salah satu dari pertanyaan itu maka Anda memiliki alasan kuat untuk meninggalkan kode yang berpotensi digandakan

Ryathal
sumber
0

Anda harus bertanya pada diri sendiri pertanyaan, "mengapa saya harus menolaknya"? Dalam kasus Anda ketika Anda memiliki kode "serupa tetapi berbeda" jika Anda membuat perubahan ke satu algoritma, Anda perlu memastikan bahwa Anda juga mencerminkan perubahan itu di tempat lain. Ini biasanya resep untuk bencana, selalu orang lain akan kehilangan satu tempat dan memperkenalkan bug lain.

Dalam hal ini, refactoring algoritma menjadi satu yang besar akan membuatnya terlalu rumit, sehingga terlalu sulit untuk pemeliharaan di masa depan. Jadi, jika Anda tidak dapat mempertimbangkan hal-hal umum secara masuk akal, sederhana:

// this code is similar to class x function b

Komentar sudah cukup. Masalah terpecahkan.

Rocklan
sumber
0

Dalam memutuskan apakah lebih baik memiliki satu metode yang lebih besar atau dua metode yang lebih kecil dengan fungsionalitas yang tumpang tindih, pertanyaan pertama $ 50.000 adalah apakah porsi perilaku yang tumpang tindih dapat berubah, dan apakah setiap perubahan harus diterapkan pada metode yang lebih kecil secara sama. Jika jawaban untuk pertanyaan pertama adalah ya tetapi jawaban untuk pertanyaan kedua adalah tidak, maka metode harus tetap terpisah . Jika jawaban untuk kedua pertanyaan adalah ya, maka sesuatu harus dilakukan untuk memastikan bahwa setiap versi kode tetap sinkron; dalam banyak kasus, cara termudah untuk melakukannya adalah dengan hanya memiliki satu versi.

Ada beberapa tempat di mana Microsoft tampaknya telah melanggar prinsip KERING. Sebagai contoh, Microsoft secara eksplisit berkecil hati memiliki metode menerima parameter yang akan menunjukkan apakah kegagalan harus mengeluarkan pengecualian. Meskipun benar bahwa parameter "kegagalan melempar pengecualian" jelek di API "penggunaan umum" metode, parameter tersebut dapat sangat membantu dalam kasus di mana metode Try / Do perlu terdiri dari metode Try / Do lainnya. Jika metode luar diharapkan untuk melemparkan pengecualian ketika kegagalan terjadi, maka setiap panggilan metode dalam yang gagal harus membuang pengecualian yang metode luar dapat membiarkan merambat. Jika metode luar tidak seharusnya melempar pengecualian, maka metode dalam tidak. Jika parameter digunakan untuk membedakan antara coba / lakukan, maka metode luar dapat meneruskannya ke metode batin. Jika tidak, maka akan diperlukan metode luar untuk memanggil metode "coba" ketika itu seharusnya berperilaku sebagai "coba", dan metode "lakukan" ketika itu seharusnya berperilaku sebagai "lakukan".

supercat
sumber