Apa yang harus dilakukan tentang file sumber C ++ 11000 baris?

229

Jadi kita memiliki file sumber mainmodule.cpp yang besar (11.000 baris?) Dalam proyek kami dan setiap kali saya harus menyentuhnya saya merasa ngeri.

Karena file ini sangat sentral dan besar, ia terus mengumpulkan kode semakin banyak dan saya tidak bisa memikirkan cara yang baik untuk membuatnya benar-benar mulai menyusut.

File tersebut digunakan dan secara aktif diubah di beberapa (10) versi pemeliharaan produk kami sehingga sangat sulit untuk memperbaikinya. Jika saya "hanya" membaginya, katakan untuk permulaan, menjadi 3 file, maka menggabungkan kembali perubahan dari versi pemeliharaan akan menjadi mimpi buruk. Dan juga jika Anda membagi file dengan sejarah yang panjang dan kaya, melacak dan memeriksa perubahan lama dalam SCCsejarah tiba-tiba menjadi jauh lebih sulit.

File pada dasarnya berisi "kelas utama" (pengiriman dan koordinasi pekerjaan internal utama) dari program kami, sehingga setiap kali fitur ditambahkan, itu juga mempengaruhi file ini dan setiap kali tumbuh. :-(

Apa yang akan kamu lakukan dalam situasi ini? Adakah gagasan tentang cara memindahkan fitur baru ke file sumber terpisah tanpa mengacaukan SCCalur kerja?

(Catatan pada alat: Kami menggunakan C ++ dengan Visual Studio; Kami menggunakan AccuRevsebagai SCCtapi saya pikir jenis SCCtidak terlalu penting di sini; Kami menggunakan Araxis Mergeuntuk melakukan perbandingan aktual dan penggabungan file)

Martin Ba
sumber
15
@BoltClock: Sebenarnya Vim akan membukanya dengan cukup cepat.
ereOn
58
69305 garis dan terus bertambah. Sebuah file di aplikasi kami tempat rekan saya membuang sebagian besar kodenya. Tidak dapat menahan memposting ini di sini. Saya tidak memiliki siapa pun di perusahaan saya untuk melaporkan hal ini kepada.
Agnel Kurian
204
Saya tidak mengerti. Bagaimana komentar "keluar dari pekerjaan" itu dapat memperoleh begitu banyak upvotes? Beberapa orang tampaknya tinggal di negeri dongeng, tempat semua proyek ditulis dari awal dan / atau menggunakan 100% gesit, TDD, ... (letakkan kata kunci Anda di sini).
Stefan
39
@Stefan: Ketika dihadapkan dengan basis kode yang sama saya melakukan hal itu. Saya tidak suka menghabiskan 95% waktu saya bekerja di sekitar mentah-mentah dalam basis kode 10 tahun, dan 5% benar-benar menulis kode. Sebenarnya tidak mungkin untuk menguji beberapa aspek sistem (dan maksud saya bukan unit test, maksud saya benar-benar menjalankan kode untuk melihat apakah itu bekerja). Saya tidak bertahan selama 6 bulan masa percobaan, saya bosan berjuang kehilangan pertempuran dan menulis kode yang tidak bisa saya tahan.
Binary Worrier
50
sehubungan dengan aspek pelacakan riwayat pemisahan file: gunakan perintah salin sistem kontrol versi Anda untuk menyalin seluruh file berapa kali Anda ingin membaginya, dan kemudian hapus semua kode dari masing-masing salinan yang tidak Anda inginkan dalam file itu. Ini mempertahankan keseluruhan sejarah, karena masing-masing dari file split dapat melacak sejarahnya kembali melalui split (yang akan tampak seperti penghapusan raksasa sebagian besar isi file).
rmeador

Jawaban:

86
  1. Temukan beberapa kode dalam file yang relatif stabil (tidak berubah cepat, dan tidak banyak berbeda antar cabang) dan dapat berdiri sebagai unit independen. Pindahkan ini ke file-nya sendiri, dan untuk itu ke kelasnya sendiri, di semua cabang. Karena stabil, ini tidak akan menyebabkan (banyak) gabungan "canggung" yang harus diterapkan ke file yang berbeda dari yang semula mereka buat, ketika Anda menggabungkan perubahan dari satu cabang ke yang lain. Ulang.

  2. Temukan beberapa kode dalam file yang pada dasarnya hanya berlaku untuk sejumlah kecil cabang, dan bisa berdiri sendiri. Tidak masalah apakah itu berubah cepat atau tidak, karena jumlah cabang yang kecil. Pindahkan ini ke dalam kelas dan file sendiri. Ulang.

Jadi, kami telah menyingkirkan kode yang sama di mana-mana, dan kode yang khusus untuk cabang tertentu.

Ini memberi Anda inti kode yang dikelola dengan buruk - diperlukan di mana-mana, tetapi berbeda di setiap cabang (dan / atau berubah secara konstan sehingga beberapa cabang berjalan di belakang yang lain), namun dalam satu file Anda gagal mencoba menggabungkan antara cabang. Berhenti lakukan itu. Cabang file secara permanen , mungkin dengan mengganti nama di masing-masing cabang. Ini bukan "utama" lagi, ini "utama untuk konfigurasi X". OK, jadi Anda kehilangan kemampuan untuk menerapkan perubahan yang sama ke beberapa cabang dengan menggabungkan, tapi ini inti dari kode di mana penggabungan tidak bekerja dengan baik. Jika Anda harus mengelola penggabungan secara manual untuk menangani konflik, maka tidak ada salahnya untuk menerapkannya secara independen di setiap cabang.

Saya pikir Anda salah mengatakan bahwa jenis SCC tidak masalah, karena misalnya kemampuan penggabungan git mungkin lebih baik daripada alat penggabung yang Anda gunakan. Jadi masalah intinya, "penggabungan sulit" terjadi pada waktu yang berbeda untuk SCC yang berbeda. Namun, Anda tidak mungkin dapat mengubah SCC, jadi masalahnya mungkin tidak relevan.

Steve Jessop
sumber
Adapun penggabungan: Saya telah melihat GIT dan saya telah melihat SVN dan saya telah melihat Perforce dan biarkan saya memberi tahu Anda bahwa tidak ada yang saya lihat di mana pun yang mengalahkan AccuRev + Araxis untuk apa yang kami lakukan. :-) (Meskipun GIT bisa melakukan [ini stackoverflow.com/questions/1728922/... ] dan AccuRev tidak bisa - setiap orang harus memutuskan sendiri apakah ini adalah bagian dari penggabungan atau analisis sejarah.)
Martin Ba
Cukup adil - mungkin Anda sudah memiliki alat terbaik yang tersedia. Kemampuan Git untuk menggabungkan perubahan yang terjadi pada File A pada cabang X, menjadi File B pada cabang Y, seharusnya membuatnya lebih mudah untuk membagi file bercabang, tetapi mungkin sistem yang Anda gunakan memiliki kelebihan yang Anda sukai. Ngomong-ngomong, saya tidak mengusulkan Anda beralih ke git, hanya mengatakan bahwa SCC membuat perbedaan di sini, tetapi meskipun demikian saya setuju dengan Anda bahwa ini dapat didiskon :-)
Steve Jessop
129

Penggabungan tidak akan menjadi mimpi buruk besar seperti ketika Anda akan mendapatkan 30000 file LOC di masa depan. Begitu:

  1. Berhenti menambahkan lebih banyak kode ke file itu.
  2. Membaginya.

Jika Anda tidak bisa berhenti mengkode selama proses refactoring, Anda bisa membiarkan file besar ini untuk sementara waktu setidaknya tanpa menambahkan lebih banyak kode ke dalamnya: karena mengandung satu "kelas utama" Anda bisa mewarisi darinya dan tetap mewarisi kelas ( es) dengan fungsi kelebihan beban di beberapa file kecil dan dirancang dengan baik.

Kirill V. Lyadvinsky
sumber
@ Martin: untungnya Anda belum menempelkan file Anda di sini, jadi saya tidak tahu tentang strukturnya. Tetapi ide umum adalah untuk membaginya dalam bagian yang logis. Bagian logis semacam itu dapat berisi kelompok fungsi dari "kelas utama" Anda atau Anda dapat membaginya dalam beberapa kelas tambahan.
Kirill V. Lyadvinsky
3
Dengan 10 versi pemeliharaan dan banyak pengembang aktif, kecil kemungkinan file tersebut dapat dibekukan untuk waktu yang cukup lama.
Kobi
9
@ Martin, Anda memiliki beberapa pola GOF yang akan melakukan trik, satu Facade yang memetakan fungsi mainmodule.cpp itu, alternatif (Saya merekomendasikan bawah) membuat suite Command kelas yang masing-masing peta untuk fungsi / fitur mainmodule.app. (Saya telah memperluas ini pada jawaban saya.)
ocodo
2
Ya benar-benar setuju, pada titik tertentu Anda harus berhenti menambahkan kode untuk itu atau akhirnya akan menjadi 30k, 40k, 50k, mainmodule kaboom hanya seg rusak. :-)
Chris
67

Bagi saya sepertinya Anda menghadapi sejumlah kode yang berbau di sini. Pertama-tama kelas utama tampaknya melanggar prinsip terbuka / tertutup . Itu juga terdengar seperti menangani terlalu banyak tanggung jawab . Karena ini saya akan menganggap kode lebih rapuh daripada yang seharusnya.

Meskipun saya dapat memahami kekhawatiran Anda tentang keterlacakan setelah refactoring, saya berharap bahwa kelas ini agak sulit untuk dipertahankan dan ditingkatkan dan bahwa setiap perubahan yang Anda lakukan cenderung menyebabkan efek samping. Saya akan berasumsi bahwa biaya ini lebih besar daripada biaya refactoring kelas.

Bagaimanapun, karena kode bau hanya akan semakin buruk dengan waktu, setidaknya pada suatu titik biaya ini akan lebih besar daripada biaya refactoring. Dari uraian Anda, saya akan berasumsi bahwa Anda telah melewati titik kritis.

Refactoring ini harus dilakukan dalam langkah-langkah kecil. Jika mungkin, tambahkan tes otomatis untuk memverifikasi perilaku saat ini sebelum refactoring apa pun. Kemudian pilih area kecil fungsionalitas terisolasi dan ekstrak ini sebagai tipe untuk mendelegasikan tanggung jawab.

Bagaimanapun, itu terdengar seperti proyek besar, semoga sukses :)

Brian Rasmussen
sumber
18
Baunya sangat banyak: baunya seperti anti-pola Blob ada di da house ... en.wikipedia.org/wiki/God_object . Makanan favoritnya adalah kode spaghetti: en.wikipedia.org/wiki/Spaghetti_code :-)
jdehaan
@jdehaan: Saya mencoba bersikap diplomatis tentang hal itu :)
Brian Rasmussen
+1 Dari saya juga, saya tidak berani menyentuh kode rumit yang saya tulis tanpa tes untuk menutupinya.
Danny Thomas
49

Satu-satunya solusi yang pernah saya bayangkan untuk masalah seperti ini adalah sebagai berikut. Keuntungan aktual dengan metode yang dijelaskan adalah progresivitas evolusi. Tidak ada revolusi di sini, jika tidak, Anda akan berada dalam masalah dengan sangat cepat.

Masukkan kelas cpp baru di atas kelas utama asli. Untuk saat ini, pada dasarnya akan mengarahkan semua panggilan ke kelas utama saat ini, tetapi bertujuan membuat API dari kelas baru ini sejelas dan sesingkat mungkin.

Setelah ini dilakukan, Anda mendapatkan kemungkinan untuk menambahkan fungsionalitas baru di kelas baru.

Adapun fungsi yang ada, Anda harus secara progresif memindahkan mereka di kelas baru karena mereka menjadi cukup stabil. Anda akan kehilangan bantuan SCC untuk bagian kode ini, tetapi tidak banyak yang bisa dilakukan mengenai hal itu. Pilih waktu yang tepat.

Saya tahu ini tidak sempurna, meskipun saya harap ini bisa membantu, dan prosesnya harus disesuaikan dengan kebutuhan Anda!

Informasi tambahan

Perhatikan bahwa Git adalah SCC yang dapat mengikuti potongan kode dari satu file ke file lainnya. Saya telah mendengar hal-hal baik tentang itu, sehingga dapat membantu saat Anda secara progresif memindahkan pekerjaan Anda.

Git dibangun di sekitar gagasan gumpalan yang, jika saya mengerti benar, mewakili potongan file kode. Pindahkan potongan-potongan ini di dalam berbagai file dan Git akan menemukannya, bahkan jika Anda memodifikasinya. Terlepas dari video dari Linus Torvalds yang disebutkan dalam komentar di bawah, saya belum dapat menemukan sesuatu yang jelas tentang ini.

Benoît
sumber
Referensi tentang bagaimana GIT melakukan itu / bagaimana Anda melakukannya dengan GIT akan sangat disambut.
Martin Ba
@ Martin Git melakukannya secara otomatis.
Matius
4
@ Martin: Git melakukannya secara otomatis - karena ia tidak melacak file, ia melacak konten. Sebenarnya lebih sulit di git untuk hanya "mendapatkan histori dari satu file".
Arafangion
1
@Martin youtube.com/watch?v=4XpnKHJAok8 adalah pembicaraan di mana Torvalds berbicara tentang git. Dia menyebutkannya nanti dalam pembicaraan.
Matius
6
@ Martin, lihat pertanyaan ini: stackoverflow.com/questions/1728922/…
Benjol
30

Konfusius berkata: "langkah pertama untuk keluar dari lubang adalah berhenti menggali lubang."

fdasfasdfdas
sumber
25

Biarkan saya menebak: Sepuluh klien dengan set fitur yang berbeda dan manajer penjualan yang mempromosikan "penyesuaian"? Saya telah mengerjakan produk seperti itu sebelumnya. Kami pada dasarnya memiliki masalah yang sama.

Anda tahu bahwa memiliki file yang sangat besar adalah masalah, tetapi bahkan lebih banyak masalah adalah sepuluh versi yang harus Anda pertahankan. Itu banyak perawatan. SCC dapat membuatnya lebih mudah, tetapi tidak bisa memperbaikinya.

Sebelum Anda mencoba untuk memecah file menjadi beberapa bagian, Anda perlu membawa kesepuluh cabang kembali sinkron satu sama lain sehingga Anda dapat melihat dan membentuk semua kode sekaligus. Anda dapat melakukan ini satu cabang pada satu waktu, menguji kedua cabang terhadap file kode utama yang sama. Untuk menegakkan perilaku kustom, Anda dapat menggunakan #ifdef dan teman-teman, tetapi lebih baik sebisa mungkin menggunakan biasa jika / selain terhadap konstanta yang ditentukan. Dengan cara ini, kompiler Anda akan memverifikasi semua jenis dan kemungkinan besar menghilangkan kode objek "mati". (Anda mungkin ingin mematikan peringatan tentang kode mati.)

Setelah hanya ada satu versi file yang dibagikan secara implisit oleh semua cabang, maka agak mudah untuk memulai metode refactoring tradisional.

#Ifdefs terutama lebih baik untuk bagian-bagian di mana kode yang terpengaruh hanya masuk akal dalam konteks penyesuaian per-cabang lainnya. Orang mungkin berpendapat bahwa ini juga menghadirkan peluang untuk skema penggabungan cabang yang sama, tetapi jangan menjadi liar. Tolong, satu proyek kolosal sekaligus.

Dalam jangka pendek, file tersebut akan muncul untuk tumbuh. Ini bagus. Apa yang Anda lakukan adalah menyatukan berbagai hal yang perlu disatukan. Setelah itu, Anda akan mulai melihat area yang jelas sama tanpa memandang versi; ini dapat dibiarkan sendiri atau di refactored sesuka hati. Area lain jelas akan berbeda tergantung versi. Anda memiliki sejumlah opsi dalam hal ini. Salah satu metode adalah mendelegasikan perbedaan ke objek strategi per-versi. Lain adalah untuk mendapatkan versi klien dari kelas abstrak umum. Tetapi tidak satu pun dari transformasi ini dimungkinkan selama Anda memiliki sepuluh "tips" pengembangan di cabang yang berbeda.

Ian
sumber
2
Saya setuju bahwa tujuannya adalah memiliki satu versi perangkat lunak, tetapi apakah tidak akan lebih baik menggunakan file konfigurasi (runtime) dan tidak mengkompilasi custumization waktu
Esben Skov Pedersen
Atau bahkan "kelas konfigurasi" untuk setiap bangunan pelanggan.
tc.
Saya pikir konfigurasi waktu kompilasi atau run-time secara fungsional tidak relevan, tetapi saya tidak ingin membatasi kemungkinan. Konfigurasi waktu kompilasi memiliki keuntungan bahwa klien tidak dapat meretas dengan file konfigurasi untuk mengaktifkan fitur tambahan, karena menempatkan semua konfigurasi dalam struktur kode sumber alih-alih sebagai kode "objek teks" yang dapat digunakan. Sisi sebaliknya adalah bahwa Anda cenderung ke AlternateHardAndSoftLayers jika dijalankan.
Ian
22

Saya tidak tahu apakah ini menyelesaikan masalah Anda, tetapi yang saya kira ingin Anda lakukan adalah memigrasi konten file ke file yang lebih kecil yang tidak tergantung satu sama lain (disimpulkan). Apa yang saya dapatkan adalah bahwa Anda memiliki sekitar 10 versi berbeda dari perangkat lunak yang beredar dan Anda perlu mendukung semuanya tanpa mengacaukan semuanya.

Pertama-tama tidak ada cara yang mudah dan akan menyelesaikan sendiri dalam beberapa menit brainstorming. Semua fungsi yang ditautkan dalam file Anda semuanya vital untuk aplikasi Anda, dan hanya dengan memotongnya dan memindahkannya ke file lain tidak akan menyelamatkan masalah Anda.

Saya pikir Anda hanya memiliki opsi ini:

  1. Jangan bermigrasi dan tetap dengan apa yang Anda miliki. Mungkin keluar dari pekerjaan Anda dan mulai mengerjakan perangkat lunak serius dengan desain yang bagus. Pemrograman ekstrem tidak selalu merupakan solusi terbaik jika Anda bekerja pada proyek lama dengan dana yang cukup untuk bertahan satu atau dua crash.

  2. Buat tata letak bagaimana Anda ingin file Anda terlihat setelah itu terpecah. Buat file yang diperlukan dan integrasikan ke dalam aplikasi Anda. Ubah nama fungsi atau kelebihannya untuk mengambil parameter tambahan (mungkin hanya boolean sederhana?). Setelah Anda harus mengerjakan kode Anda, migrasikan fungsi-fungsi yang perlu Anda kerjakan ke file baru dan petakan panggilan fungsi dari fungsi-fungsi lama ke fungsi-fungsi baru. Anda harus tetap memiliki file utama Anda dengan cara ini, dan masih dapat melihat perubahan yang dibuat untuk itu, setelah itu datang ke fungsi tertentu Anda tahu persis kapan itu di-outsourcing dan seterusnya.

  3. Cobalah meyakinkan rekan kerja Anda dengan kue yang bagus bahwa alur kerja berlebihan dan bahwa Anda perlu menulis ulang beberapa bagian aplikasi untuk melakukan bisnis yang serius.

Robin
sumber
19

Persisnya masalah ini ditangani di salah satu bab buku "Bekerja Efektif dengan Kode Warisan" ( http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052 ).

Patrick
sumber
informit.com/store/product.aspx?isbn=0131177052 memungkinkan untuk melihat TOC buku ini (dan 2 bab contoh). Berapa lama bab 20? (Hanya untuk merasakan betapa bermanfaatnya itu.)
Martin Ba
17
bab 20 panjangnya 10.000 baris, tetapi penulis sedang mengusahakan bagaimana membaginya menjadi potongan-potongan yang dapat dicerna ... 8)
Tony Delroy
1
Ini sekitar 23 halaman, tetapi dengan 14 gambar. Saya pikir Anda harus mendapatkannya, Anda akan merasa jauh lebih percaya diri mencoba memutuskan apa yang harus dilakukan imho.
Emile Vrijdags
Buku yang bagus untuk masalah ini, tetapi rekomendasi yang dibuatnya (dan rekomendasi lain di utas ini) semuanya memiliki persyaratan yang sama: jika Anda ingin memperbaiki file ini untuk semua cabang Anda, maka satu-satunya cara Anda dapat melakukan ini adalah membekukan file untuk semua cabang dan membuat perubahan struktural awal. Tidak ada jalan lain untuk itu. Buku ini menguraikan pendekatan berulang untuk mengekstraksi subkelas dengan aman tanpa dukungan refactoring otomatis, dengan membuat metode duplikat dan mendelegasikan panggilan, tetapi semua ini diperdebatkan jika Anda tidak dapat memodifikasi file.
Dan Bryant
2
@ Martin, buku ini sangat bagus tetapi sangat bergantung pada tes, refactor, siklus tes yang mungkin cukup sulit dari tempat Anda sekarang. Saya pernah berada dalam situasi yang sama dan buku ini adalah yang paling bermanfaat yang saya temukan. Ada saran bagus untuk masalah jelek yang Anda miliki. Tetapi jika Anda tidak bisa mendapatkan semacam uji coba ke dalam gambar, semua saran refactoring di dunia tidak akan membantu Anda.
14

Saya pikir Anda akan lebih baik membuat satu set kelas perintah yang memetakan ke poin API dari mainmodule.cpp.

Setelah mereka berada di tempat, Anda perlu untuk memperbaiki basis kode yang ada untuk mengakses poin-poin API ini melalui kelas perintah, setelah selesai, Anda bebas untuk mengubah setiap implementasi perintah ke dalam struktur kelas baru.

Tentu saja, dengan satu kelas 11 KLOC kode di sana mungkin sangat berpasangan dan rapuh, tetapi membuat kelas perintah individu akan membantu lebih banyak daripada strategi proxy / fasad lainnya.

Saya tidak iri dengan tugas itu, tetapi seiring berjalannya waktu masalah ini hanya akan bertambah buruk jika tidak ditangani.

Memperbarui

Saya menyarankan bahwa pola Command lebih disukai daripada Facade.

Lebih baik menjaga / mengorganisasikan banyak kelas Komando yang berbeda melalui Fasad (relatif) monolitik. Memetakan satu fasad tunggal ke file 11 KLOC mungkin perlu dipecah menjadi beberapa grup yang berbeda.

Mengapa repot-repot mencoba mencari tahu kelompok-kelompok fasad ini? Dengan pola Command Anda akan dapat mengelompokkan dan mengatur kelas-kelas kecil ini secara organik, sehingga Anda memiliki lebih banyak fleksibilitas.

Tentu saja, kedua opsi lebih baik daripada 11 KLOC tunggal dan tumbuh, file.

Slomojo
sumber
Memberi +1 alternatif untuk solusi yang saya usulkan, dengan ide yang sama: ubah API untuk memisahkan masalah besar menjadi kecil.
Benoît
13

Salah satu saran penting: Jangan gabungkan refactoring dan perbaikan bug. Yang Anda inginkan adalah Versi program Anda yang identik dengan versi sebelumnya, kecuali bahwa kode sumbernya berbeda.

Salah satu caranya adalah dengan mulai membagi fungsi / bagian paling tidak besar ke dalam file itu sendiri dan kemudian memasukkannya dengan header (sehingga mengubah main.cpp menjadi daftar #includes, yang mengeluarkan bau kode pada dirinya sendiri * Saya tidak a C ++ Guru), tapi setidaknya sekarang dibagi menjadi file).

Anda kemudian dapat mencoba mengalihkan semua rilis pemeliharaan ke main.cpp "baru" atau apa pun struktur Anda. Lagi: Tidak ada perubahan atau perbaikan bug lain karena melacak itu membingungkan sekali.

Hal lain: Sebanyak yang Anda inginkan membuat satu umpan besar pada refactoring semuanya sekaligus, Anda mungkin menggigit lebih dari yang Anda bisa mengunyah. Mungkin hanya memilih satu atau dua "bagian", memasukkannya ke dalam semua rilis, lalu menambahkan beberapa nilai lebih untuk pelanggan Anda (setelah semua, Refactoring tidak menambah nilai langsung sehingga itu adalah biaya yang harus dibenarkan) dan kemudian pilih yang lain satu atau dua bagian.

Jelas itu membutuhkan beberapa disiplin dalam tim untuk benar-benar menggunakan file split dan tidak hanya menambahkan hal-hal baru ke main.cpp sepanjang waktu, tetapi sekali lagi, mencoba melakukan satu refactor besar mungkin bukan tindakan terbaik.

Michael Stum
sumber
1
1 untuk anjak keluar dan kembali # include dalam. Jika Anda melakukan ini untuk semua 10 cabang (sedikit pekerjaan di sana, tetapi dikelola) Anda masih akan memiliki masalah lain, bahwa posting perubahan di semua cabang Anda, tapi itu masalah wouldn' t telah berkembang (tentu saja). Apakah itu jelek? Ya memang masih, tetapi mungkin membawa sedikit rasionalitas untuk masalah ini. Setelah menghabiskan beberapa tahun melakukan pemeliharaan dan servis untuk produk yang benar-benar besar, saya tahu pemeliharaan itu melibatkan banyak rasa sakit. Paling tidak, belajarlah darinya, dan menjadi kisah peringatan bagi orang lain.
Jay
10

Rofl, ini mengingatkan saya pada pekerjaan lama saya. Tampaknya, sebelum saya bergabung, semuanya ada di dalam satu file besar (juga C ++). Kemudian mereka telah membaginya (pada titik yang benar-benar acak menggunakan termasuk) menjadi sekitar tiga (file masih besar). Kualitas perangkat lunak ini, seperti yang Anda duga, mengerikan. Proyek ini mencapai sekitar 40k LOC. (hampir tidak berisi komentar tetapi BANYAK kode duplikat)

Pada akhirnya saya melakukan penulisan ulang proyek secara lengkap. Saya mulai dengan mengulangi bagian terburuk dari proyek dari awal. Tentu saja saya memikirkan kemungkinan (kecil) antarmuka antara bagian baru ini dan yang lainnya. Kemudian saya memasukkan bagian ini ke dalam proyek lama. Saya tidak memperbaiki kode lama untuk membuat antarmuka yang diperlukan, tetapi hanya menggantinya. Kemudian saya mengambil langkah-langkah kecil dari sana, menulis ulang kode lama.

Saya harus mengatakan bahwa ini memakan waktu sekitar setengah tahun dan tidak ada pengembangan basis kode lama di samping perbaikan bug selama waktu itu.


edit:

Ukurannya tetap di sekitar 40k LOC tetapi aplikasi baru berisi lebih banyak fitur dan mungkin lebih sedikit bug dalam versi awal daripada perangkat lunak yang berusia 8 tahun. Salah satu alasan penulisan ulang itu adalah karena kami membutuhkan fitur-fitur baru dan memperkenalkannya di dalam kode lama hampir tidak mungkin.

Perangkat lunak ini untuk sistem tertanam, printer label.

Poin lain yang harus saya tambahkan adalah bahwa dalam teori proyek tersebut adalah C ++. Tapi itu bukan OO sama sekali, bisa saja C. Versi baru berorientasi objek.

ziggystar
sumber
9
Setiap kali saya mendengar "dari awal" dalam topik tentang refactoring, saya membunuh anak kucing!
Kugel
Saya telah berada dalam situasi yang sangat mirip, meskipun program utama yang harus saya tangani hanya ~ 9000 LOC. Dan itu sudah cukup buruk.
AndyUK
8

OK jadi untuk sebagian besar menulis ulang API kode produksi adalah ide yang buruk sebagai permulaan. Dua hal perlu terjadi.

Pertama, Anda harus membuat tim Anda memutuskan untuk melakukan pembekuan kode pada versi produksi file ini saat ini.

Dua, Anda perlu mengambil versi produksi ini dan membuat cabang yang mengelola build menggunakan arahan preprocessing untuk membagi file besar. Memisahkan kompilasi menggunakan arahan preprocessor JUST (#ifdefs, #includes, #endifs) lebih mudah daripada pengodean ulang API. Ini jelas lebih mudah untuk SLA Anda dan dukungan berkelanjutan.

Di sini Anda cukup memotong fungsi yang berhubungan dengan subsistem tertentu di dalam kelas dan meletakkannya di file say mainloop_foostuff.cpp dan memasukkannya ke mainloop.cpp di lokasi yang tepat.

ATAU

Cara yang lebih memakan waktu tetapi kuat adalah untuk merancang struktur dependensi internal dengan dua arah dalam bagaimana hal-hal dimasukkan. Ini akan memungkinkan Anda untuk membagi hal-hal dan masih mengurus co-dependensi. Perhatikan bahwa pendekatan ini membutuhkan pengkodean posisi dan oleh karena itu harus digabungkan dengan komentar yang sesuai.

Pendekatan ini akan mencakup komponen yang digunakan berdasarkan varian mana yang Anda kompilasi.

Struktur dasarnya adalah mainclass.cpp Anda akan menyertakan file baru bernama MainClassComponents.cpp setelah blok pernyataan seperti berikut:

#if VARIANT == 1
#  define Uses_Component_1
#  define Uses_Component_2
#elif VARIANT == 2
#  define Uses_Component_1
#  define Uses_Component_3
#  define Uses_Component_6
...

#endif

#include "MainClassComponents.cpp"

Struktur utama file MainClassComponents.cpp akan ada di sana untuk menyelesaikan dependensi dalam sub komponen seperti ini:

#ifndef _MainClassComponents_cpp
#define _MainClassComponents_cpp

/* dependencies declarations */

#if defined(Activate_Component_1) 
#define _REQUIRES_COMPONENT_1
#define _REQUIRES_COMPONENT_3 /* you also need component 3 for component 1 */
#endif

#if defined(Activate_Component_2)
#define _REQUIRES_COMPONENT_2
#define _REQUIRES_COMPONENT_15 /* you also need component 15 for this component  */
#endif

/* later on in the header */

#ifdef _REQUIRES_COMPONENT_1
#include "component_1.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_2
#include "component_2.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_3
#include "component_3.cpp"
#endif


#endif /* _MainClassComponents_h  */

Dan sekarang untuk setiap komponen Anda membuat file component_xx.cpp.

Tentu saja saya menggunakan angka tetapi Anda harus menggunakan sesuatu yang lebih logis berdasarkan kode Anda.

Menggunakan preprocessor memungkinkan Anda untuk membagi berbagai hal tanpa harus khawatir tentang perubahan API yang merupakan mimpi buruk dalam produksi.

Setelah produksi selesai, Anda dapat benar-benar mengerjakan desain ulang.

Elf King
sumber
Ini tampak seperti hasil pengalaman yang bekerja-tetapi-awalnya-menyakitkan.
JBRWilkinson
Sebenarnya, ini adalah teknik yang digunakan dalam kompiler Borland C ++ untuk meniru Penggunaan gaya Pascal untuk mengelola file header. Terutama ketika mereka melakukan port awal sistem Windowing berbasis Teks.
Elf King
8

Yah saya mengerti rasa sakit Anda :) Saya sudah di beberapa proyek seperti itu juga dan itu tidak cantik. Tidak ada jawaban mudah untuk ini.

Salah satu pendekatan yang dapat bekerja untuk Anda adalah mulai menambahkan pelindung aman di semua fungsi, yaitu, memeriksa argumen, pra / pasca-kondisi dalam metode, lalu akhirnya menambahkan unit test semua untuk menangkap fungsionalitas sumber saat ini. Setelah Anda memiliki ini, Anda lebih siap untuk faktor ulang kode karena Anda akan memiliki pemberitahuan dan kesalahan muncul mengingatkan Anda jika Anda telah melupakan sesuatu.

Kadang-kadang meskipun ada kalanya refactoring hanya dapat membawa lebih banyak rasa sakit daripada manfaat. Maka mungkin lebih baik meninggalkan proyek asli dan dalam kondisi pemeliharaan semu dan mulai dari awal dan kemudian secara bertahap menambahkan fungsi dari beast.

claptrap
sumber
4

Anda seharusnya tidak khawatir dengan mengurangi ukuran file, melainkan dengan mengurangi ukuran kelas. Turun ke hampir sama, tetapi membuat Anda melihat masalah dari sudut yang berbeda (seperti @Brian Rasmussen menyarankan , kelas Anda tampaknya harus banyak tanggung jawab).

Björn Pollex
sumber
Seperti biasa, saya ingin mendapatkan penjelasan untuk downvote.
Björn Pollex
4

Apa yang Anda miliki adalah contoh klasik antipattern desain yang dikenal yang disebut gumpalan . Luangkan waktu untuk membaca artikel yang saya tunjukkan di sini, dan mungkin Anda mungkin menemukan sesuatu yang bermanfaat. Selain itu, jika proyek ini sebesar yang terlihat, Anda harus mempertimbangkan beberapa desain untuk mencegah berkembangnya kode yang tidak dapat Anda kontrol.

David Conde
sumber
4

Ini bukan jawaban untuk masalah besar, tetapi solusi teoretis untuk bagian spesifiknya:

  • Cari tahu di mana Anda ingin membagi file besar menjadi subfile. Masukkan komentar dalam beberapa format khusus di masing-masing poin tersebut.

  • Tulis skrip yang cukup sepele yang akan memecah file menjadi beberapa subfile pada saat itu. (Mungkin komentar khusus telah menyematkan nama file yang dapat digunakan skrip sebagai instruksi untuk membaginya.) Ini harus mempertahankan komentar sebagai bagian dari pemisahan.

  • Jalankan skrip. Hapus file asli.

  • Saat Anda perlu menggabungkan dari cabang, pertama buat ulang file besar dengan menggabungkan potongan-potongan kembali, lakukan penggabungan, lalu pisahkan kembali.

Juga, jika Anda ingin mempertahankan sejarah file SCC, saya berharap cara terbaik untuk melakukannya adalah dengan memberi tahu sistem kontrol sumber Anda bahwa masing-masing file part adalah salinan dari aslinya. Maka itu akan mempertahankan sejarah bagian yang disimpan dalam file itu, meskipun tentu saja itu juga akan mencatat bahwa sebagian besar "dihapus".

Brooks Moses
sumber
4

Salah satu cara untuk membaginya tanpa terlalu banyak bahaya adalah dengan melihat secara historis semua perubahan garis. Apakah ada fungsi tertentu yang lebih stabil daripada yang lain? Titik panas perubahan jika Anda mau.

Jika suatu baris belum diubah dalam beberapa tahun, Anda mungkin dapat memindahkannya ke file lain tanpa terlalu khawatir. Saya akan melihat sumber yang dianotasi dengan revisi terakhir yang menyentuh garis yang diberikan dan melihat apakah ada fungsi yang bisa Anda tarik.

Paul Rubel
sumber
Saya pikir orang lain mengusulkan hal serupa. Ini singkat dan to the point dan saya pikir ini mungkin merupakan titik awal yang valid untuk masalah aslinya.
Martin Ba
3

Wow, kedengarannya bagus. Saya pikir menjelaskan kepada atasan Anda, bahwa Anda perlu banyak waktu untuk memperbaiki binatang itu patut dicoba. Jika dia tidak setuju, berhenti adalah pilihan.

Bagaimanapun, apa yang saya sarankan pada dasarnya adalah membuang semua implementasi dan mengelompokkannya kembali ke dalam modul-modul baru, sebut saja "layanan global". "Modul utama" hanya akan meneruskan ke layanan-layanan itu dan APA PUN kode baru yang Anda tulis akan menggunakannya alih-alih "Modul Utama". Ini harus layak dalam jumlah waktu yang wajar (karena sebagian besar menyalin dan menempel), Anda tidak melanggar kode yang ada dan Anda dapat melakukannya satu versi pemeliharaan pada suatu waktu. Dan jika Anda masih memiliki waktu tersisa, Anda dapat menggunakannya untuk mengembalikan semua modul yang tergantung lama untuk juga menggunakan layanan global.

back2dos
sumber
3

Simpati saya - dalam pekerjaan saya sebelumnya saya menghadapi situasi yang sama dengan file yang beberapa kali lebih besar dari yang Anda harus berurusan dengan. Solusi adalah:

  1. Tulis kode untuk menguji fungsi dalam program tersebut secara mendalam. Sepertinya Anda belum memiliki ini di tangan ...
  2. Identifikasi beberapa kode yang dapat diabstraksi menjadi kelas pembantu / utilitas. Tidak perlu besar, hanya sesuatu yang tidak benar-benar bagian dari kelas 'utama' Anda.
  3. Refactor kode yang diidentifikasi dalam 2. menjadi kelas yang terpisah.
  4. Jalankan kembali tes Anda untuk memastikan tidak ada yang rusak.
  5. Ketika Anda punya waktu, goto 2. dan ulangi sesuai kebutuhan untuk membuat kode dapat dikelola.

Kelas yang Anda bangun di langkah 3. iterasi kemungkinan akan tumbuh untuk menyerap lebih banyak kode yang sesuai dengan fungsi mereka yang baru-jelas.

Saya juga bisa menambahkan:

0: beli buku Michael Feathers tentang bekerja dengan kode warisan

Sayangnya jenis pekerjaan ini terlalu umum, tetapi pengalaman saya adalah bahwa ada nilai yang besar untuk dapat membuat kode kerja tetapi mengerikan semakin sedikit lebih mengerikan sambil tetap bekerja.

Steve Townsend
sumber
2

Pertimbangkan cara untuk menulis ulang seluruh aplikasi dengan cara yang lebih masuk akal. Mungkin menulis ulang sebagian kecil sebagai prototipe untuk melihat apakah ide Anda layak.

Jika Anda telah mengidentifikasi solusi yang bisa diterapkan, refactor aplikasi yang sesuai.

Jika semua upaya untuk menghasilkan arsitektur yang lebih rasional gagal, maka setidaknya Anda tahu solusinya mungkin dalam mendefinisikan ulang fungsi program.

wallyk
sumber
+1 - tulis ulang di waktu Anda sendiri meskipun orang lain mungkin meludahi boneka mereka.
Jon Black
2

0,05 eurocents saya:

Rancang ulang seluruh kekacauan, pilah menjadi beberapa subsistem dengan mempertimbangkan persyaratan teknis dan bisnis (= banyak trek perawatan paralel dengan basis kode yang berpotensi berbeda untuk masing-masingnya, jelas ada kebutuhan untuk modifikasi yang tinggi, dll.).

Ketika membelah menjadi subsistem, analisis tempat-tempat yang paling berubah dan pisahkan dari bagian-bagian yang tidak berubah. Ini akan menunjukkan kepada Anda titik masalah. Pisahkan bagian-bagian yang paling berubah ke modul mereka sendiri (misalnya dll) sedemikian rupa sehingga API modul dapat tetap utuh dan Anda tidak perlu merusak BC sepanjang waktu. Dengan cara ini Anda dapat menggunakan versi modul yang berbeda untuk cabang pemeliharaan yang berbeda, jika perlu, sementara inti tidak berubah.

Desain ulang kemungkinan akan perlu menjadi proyek yang terpisah, mencoba melakukannya untuk target yang bergerak tidak akan berhasil.

Adapun sejarah kode sumber, pendapat saya: lupakan untuk kode baru. Tetapi simpan sejarah di suatu tempat sehingga Anda dapat memeriksanya, jika perlu. Saya yakin Anda tidak akan membutuhkannya setelah awal.

Anda kemungkinan besar perlu mendapatkan dukungan manajemen untuk proyek ini. Anda dapat berdebat mungkin dengan waktu pengembangan yang lebih cepat, lebih sedikit bug, pemeliharaan lebih mudah dan kekacauan keseluruhan lebih sedikit. Sesuatu di sepanjang baris "Secara proaktif memungkinkan kelayakan masa depan dan pemeliharaan aset aset kritis kami" :)

Ini adalah cara saya mulai mengatasi masalah setidaknya.

Slinky
sumber
2

Mulailah dengan menambahkan komentar. Dengan referensi ke tempat fungsi dipanggil dan jika Anda dapat memindahkan barang-barang. Ini bisa membuat segalanya bergerak. Anda benar-benar perlu menilai seberapa rapuh basis kode itu. Kemudian pindahkan bit fungsionalitas bersama secara bersamaan. Perubahan kecil pada suatu waktu.

Jesper Smith
sumber
2

Buku lain yang menurut Anda menarik / bermanfaat adalah Refactoring .

Derek
sumber
2

Sesuatu yang menurut saya berguna untuk dilakukan (dan saya melakukannya sekarang walaupun tidak pada skala yang Anda hadapi), adalah mengekstraksi metode sebagai kelas (metode objek refactoring). Metode yang berbeda di berbagai versi Anda akan menjadi kelas yang berbeda yang dapat disuntikkan ke pangkalan bersama untuk memberikan perilaku berbeda yang Anda butuhkan.

Channing Walton
sumber
2

Saya menemukan kalimat ini menjadi bagian paling menarik dari posting Anda:

> File tersebut digunakan dan secara aktif diubah dalam beberapa (> 10) versi pemeliharaan produk kami dan jadi sangat sulit untuk memperbaikinya

Pertama, saya akan merekomendasikan agar Anda menggunakan sistem kontrol sumber untuk mengembangkan 10 + versi pemeliharaan yang mendukung percabangan.

Kedua, saya akan membuat sepuluh cabang (satu untuk masing-masing versi pemeliharaan Anda).

Saya bisa merasakan Anda merasa ngeri! Tetapi kontrol sumber Anda tidak berfungsi untuk situasi Anda karena kurangnya fitur, atau tidak digunakan dengan benar.

Sekarang ke cabang tempat Anda bekerja - refactor sesuai keinginan Anda, aman dengan pengetahuan bahwa Anda tidak akan mengecewakan sembilan cabang lainnya dari produk Anda.

Saya akan sedikit khawatir bahwa Anda memiliki begitu banyak fungsi () utama Anda.

Dalam setiap proyek yang saya tulis, saya akan menggunakan main () hanya melakukan inisialisasi objek inti - seperti objek simulasi atau aplikasi - kelas-kelas ini adalah tempat pekerjaan nyata harus dilanjutkan.

Saya juga akan menginisialisasi objek log aplikasi untuk digunakan secara global di seluruh program.

Akhirnya, pada intinya saya juga menambahkan kode deteksi kebocoran di blok preprosesor yang memastikan itu hanya diaktifkan di build DEBUG. Ini semua yang akan saya tambahkan ke main (). Main () harus pendek!

Itu kata kamu

> File pada dasarnya berisi "kelas utama" (pengiriman dan koordinasi pekerjaan internal utama) dari program kami

Kedengarannya seperti dua tugas ini dapat dibagi menjadi dua objek terpisah - koordinator dan dispatcher kerja.

Ketika Anda membagi ini, Anda mungkin mengacaukan "alur kerja SCC" Anda, tetapi kedengarannya seperti menempel ketat pada alur kerja SCC Anda menyebabkan masalah pemeliharaan perangkat lunak. Parit, sekarang dan jangan melihat ke belakang, karena begitu Anda memperbaikinya, Anda akan mulai mudah tidur.

Jika Anda tidak dapat mengambil keputusan, bertarunglah dengan manajer Anda untuk hal itu - aplikasi Anda perlu di refactored - dan sangat buruk oleh suara itu! Jangan menerima jawaban tidak!

pengguna206705
sumber
Seperti yang saya pahami, masalahnya adalah ini: jika Anda menggigit peluru dan refactor, Anda tidak dapat lagi membawa patch di antara versi-versi tersebut. SCC mungkin sudah diatur dengan sempurna.
peterchen
@ Peterchen - persis masalah. SCC memang melakukan penggabungan pada level file. (Penggabungan 3 arah) Jika Anda memindahkan kode di antara file, Anda harus mulai memblok blok kode yang dimodifikasi secara manual dari satu file ke file lainnya. (Fitur GIT yang disebutkan orang lain dalam komentar lain hanya baik untuk sejarah, bukan untuk penggabungan sejauh yang saya tahu)
Martin Ba
2

Seperti yang telah Anda jelaskan, masalah utamanya adalah membedakan pre-split vs post-split, menggabungkan perbaikan bug dll. Alat di sekitarnya. Tidak perlu waktu lama untuk membuat hardcode pada skrip di Perl, Ruby, dll. Untuk merobek sebagian besar noise dari membedakan pre-split dengan gabungan post-split. Lakukan apa pun yang paling mudah dalam menangani kebisingan:

  • menghapus garis-garis tertentu sebelum / selama penyatuan (misalnya termasuk penjaga)
  • hapus hal-hal lain dari output diff jika perlu

Anda bahkan dapat membuatnya jadi setiap kali ada checkin, rangkaiannya berjalan dan Anda punya sesuatu yang dipersiapkan untuk berbeda dengan versi file tunggal.

Tony D
sumber
2
  1. Jangan pernah menyentuh file dan kode ini lagi!
  2. Memperlakukan seperti sesuatu yang Anda terjebak. Mulailah menulis adaptor untuk fungsi yang disandikan di sana.
  3. Tulis kode baru di unit yang berbeda dan bicaralah hanya dengan adaptor yang merangkum fungsi monster.
  4. ... jika hanya satu di atas yang tidak memungkinkan, keluar dari pekerjaan dan beri Anda yang baru.
paul_71
sumber
2
+/- 0 - serius, di mana Anda, orang-orang yang tinggal, Anda akan merekomendasikan berhenti dari pekerjaan berdasarkan detail teknis seperti ini?
Martin Ba
1

"File ini pada dasarnya berisi" kelas utama "(pengiriman dan koordinasi pekerjaan internal utama) dari program kami, jadi setiap kali fitur ditambahkan, itu juga memengaruhi file ini dan setiap kali tumbuh."

Jika itu SWITCH besar (yang saya pikir ada) menjadi masalah pemeliharaan utama, Anda bisa refactor untuk menggunakan kamus dan pola Command dan menghapus semua logika switch dari kode yang ada ke loader, yang mengisi peta itu, yaitu:

    // declaration
    std::map<ID, ICommand*> dispatchTable;
    ...

    // populating using some loader
    dispatchTable[id] = concreteCommand;

    ...
    // using
    dispatchTable[id]->Execute();
Grozz
sumber
2
Tidak, sebenarnya tidak ada saklar besar. Kalimat ini adalah yang paling dekat yang bisa saya gunakan untuk menggambarkan kekacauan ini :)
Martin Ba
1

Saya pikir cara termudah untuk melacak riwayat sumber ketika memisahkan file adalah seperti ini:

  1. Buat salinan dari kode sumber asli, dengan menggunakan perintah salin apa pun yang menjaga sejarah yang disediakan oleh sistem SCM Anda. Anda mungkin harus mengirimkan pada saat ini, tetapi belum perlu memberi tahu sistem build Anda tentang file-file baru, jadi itu tidak masalah.
  2. Hapus kode dari salinan ini. Itu seharusnya tidak mematahkan sejarah untuk garis yang Anda pertahankan.
Christopher Creutzig
sumber
"menggunakan perintah penyimpan riwayat apa pun yang disediakan oleh sistem SCM Anda" ... hal buruk yang tidak disediakannya
Martin Ba
Sangat buruk. Itu saja terdengar seperti alasan yang bagus untuk beralih ke sesuatu yang lebih modern. :-)
Christopher Creutzig
1

Saya pikir apa yang akan saya lakukan dalam situasi ini adalah sedikit peluru dan:

  1. Cari tahu bagaimana saya ingin membagi file (berdasarkan versi pengembangan saat ini)
  2. Letakkan kunci administratif pada file ("Tidak ada yang menyentuh mainmodule.cpp setelah jam 5 sore Jumat !!!"
  3. Habiskan akhir pekan panjang Anda dengan menerapkan perubahan itu ke> 10 versi pemeliharaan (dari terlama ke terbaru), hingga dan termasuk versi saat ini.
  4. Hapus mainmodule.cpp dari semua versi perangkat lunak yang didukung. Ini Zaman baru - tidak ada lagi mainmodule.cpp.
  5. Yakinkan Manajemen bahwa Anda tidak boleh mendukung lebih dari satu versi pemeliharaan perangkat lunak (setidaknya tanpa kontrak dukungan $$$ besar). Jika masing-masing pelanggan Anda memiliki versi unik mereka sendiri .... yeeeeeshhhh. Saya akan menambahkan arahan compiler daripada mencoba mempertahankan 10+ fork.

Melacak perubahan lama ke file hanya diselesaikan dengan komentar check-in pertama Anda mengatakan sesuatu seperti "split from mainmodule.cpp". Jika Anda perlu kembali ke sesuatu yang baru, kebanyakan orang akan mengingat perubahan, jika 2 tahun dari sekarang, komentar akan memberi tahu mereka ke mana harus mencari. Tentu saja, betapa berharganya untuk kembali lebih dari 2 tahun untuk melihat siapa yang mengubah kode dan mengapa?

BIBD
sumber