Kami memulai proyek baru, dari awal. Sekitar delapan pengembang, selusin atau lebih subsistem, masing-masing dengan empat atau lima file sumber.
Apa yang bisa kita lakukan untuk mencegah "sundulan neraka", AKA "sundulan spaghetti"?
- Satu tajuk per file sumber?
- Ditambah satu per subsistem?
- Pisahkan typdefs, stucts & enums dari prototipe fungsi?
- Pisahkan subsistem internal dari subsistem eksternal?
- Bersikeras bahwa setiap file tunggal, apakah header atau sumber harus dapat dikompilasi secara mandiri?
Saya tidak meminta cara “terbaik”, cukup tunjukkan apa yang harus diperhatikan dan apa yang dapat menyebabkan kesedihan, sehingga kita dapat mencoba menghindarinya.
Ini akan menjadi proyek C ++, tetapi info C akan membantu pembaca masa depan.
Jawaban:
Metode sederhana: Satu tajuk per file sumber. Jika Anda memiliki subsistem lengkap di mana pengguna tidak diharapkan tahu tentang file sumber, minta satu header untuk subsistem termasuk semua file header yang diperlukan.
File header apa pun harus dapat dikompilasi sendiri (atau katakanlah file sumber termasuk header tunggal mana pun harus dikompilasi). Sungguh menyakitkan jika saya menemukan file header yang berisi apa yang saya inginkan, dan kemudian saya harus memburu file header lainnya. Cara sederhana untuk menegakkan ini adalah meminta setiap file sumber menyertakan file header-nya terlebih dahulu (terima kasih doug65536, saya pikir saya melakukan itu sebagian besar waktu tanpa menyadarinya).
Pastikan Anda menggunakan alat yang tersedia untuk menjaga waktu kompilasi tetap rendah - setiap header harus disertakan hanya sekali, gunakan header yang dikompilasi sebelumnya untuk menjaga waktu kompilasi tetap rendah, gunakan modul yang telah dikompilasi jika mungkin untuk menjaga waktu kompilasi lebih jauh ke bawah.
sumber
Sejauh ini persyaratan yang paling penting adalah mengurangi ketergantungan antara file sumber Anda. Dalam C ++ itu umum untuk menggunakan satu file sumber dan satu header per kelas. Oleh karena itu, jika Anda memiliki desain kelas yang bagus, Anda bahkan tidak akan mendekati header hell.
Anda juga dapat melihat ini sebaliknya: Jika Anda sudah memiliki header header di proyek Anda, Anda dapat yakin bahwa desain perangkat lunak perlu ditingkatkan.
Untuk menjawab pertanyaan spesifik Anda:
sumber
Selain rekomendasi lainnya, sejalan dengan pengurangan ketergantungan (terutama berlaku untuk C ++):
sumber
Satu header per file sumber, yang mendefinisikan apa yang mengimplementasikan / ekspor file sumbernya.
Sebanyak mungkin file header, termasuk dalam setiap file sumber (dimulai dengan header sendiri).
Hindari memasukkan (meminimalkan penyertaan) file header dalam file header lainnya (untuk menghindari dependensi melingkar). Untuk detail, lihat jawaban ini untuk "dapatkah dua kelas saling melihat menggunakan C ++?"
Ada seluruh buku tentang hal ini, Desain Perangkat Lunak C ++ Skala Besar oleh Lakos. Ini menggambarkan memiliki "lapisan" perangkat lunak: lapisan tingkat tinggi menggunakan lapisan tingkat yang lebih rendah bukan sebaliknya, yang lagi-lagi menghindari dependensi melingkar.
sumber
Saya akan mengklaim pertanyaan Anda pada dasarnya tidak dapat dijawab, karena ada dua jenis header header:
masalahnya adalah, jika Anda mencoba untuk menghindari yang pertama Anda akhirnya, sampai batas tertentu, dengan yang terakhir, dan sebaliknya.
Ada juga jenis neraka ketiga, yaitu dependensi melingkar. Ini mungkin muncul jika Anda tidak hati-hati ... menghindarinya tidak terlalu rumit, tetapi Anda perlu meluangkan waktu untuk memikirkan bagaimana melakukannya. Lihat John Lakos berbicara tentang Levelisasi di CppCon 2016 (atau hanya slide ).
sumber
Decoupling
Ini pada akhirnya tentang decoupling kepada saya pada akhir hari di tingkat desain yang paling mendasar tanpa nuansa karakteristik kompiler dan linker kami. Maksud saya Anda dapat melakukan hal-hal seperti membuat setiap header mendefinisikan hanya satu kelas, menggunakan pimpl, meneruskan deklarasi ke jenis yang hanya perlu dideklarasikan, tidak didefinisikan, bahkan mungkin menggunakan header yang hanya berisi deklarasi maju (mis .:)
<iosfwd>
, satu header per file sumber , mengatur sistem secara konsisten berdasarkan jenis hal yang dinyatakan / didefinisikan, dll.Teknik untuk Mengurangi "Ketergantungan Waktu Kompilasi"
Dan beberapa teknik dapat membantu sedikit tetapi Anda dapat menemukan diri Anda melelahkan praktik ini dan masih menemukan file sumber rata-rata di sistem Anda perlu pembukaan dua halaman dari
#include
arahan untuk melakukan sesuatu yang sedikit bermakna dengan waktu pembuatan yang melambung tinggi jika Anda terlalu fokus mengurangi ketergantungan waktu kompilasi di tingkat tajuk tanpa mengurangi ketergantungan logis pada desain antarmuka Anda, dan sementara itu mungkin tidak dianggap "spageti header" dengan tegas, saya Masih akan mengatakan itu diterjemahkan ke masalah merugikan serupa dengan produktivitas dalam praktek. Pada akhirnya, jika unit kompilasi Anda masih membutuhkan banyak informasi untuk dapat melakukan apa saja, maka itu akan diterjemahkan untuk menambah waktu pembuatan dan mengalikan alasan Anda harus berpotensi kembali dan harus mengubah hal-hal sambil membuat pengembang merasa seperti mereka headbutting sistem hanya mencoba menyelesaikan koding harian mereka. Saya t'Anda dapat, misalnya, membuat setiap subsistem menyediakan satu file header dan antarmuka yang sangat abstrak. Tetapi jika subsistem tidak dipisahkan satu sama lain, maka Anda mendapatkan sesuatu yang menyerupai spaghetti lagi dengan antarmuka subsistem tergantung pada antarmuka subsistem lainnya dengan grafik dependensi yang terlihat seperti berantakan untuk bekerja.
Teruskan Deklarasi ke Tipe Eksternal
Dari semua teknik yang saya gunakan untuk mendapatkan basis kode sebelumnya yang membutuhkan waktu dua jam untuk membangun sementara para pengembang kadang-kadang menunggu 2 hari untuk giliran mereka di CI pada server build kami (Anda hampir dapat membayangkan mesin-mesin itu sebagai binatang buangan yang kelelahan karena mencoba dengan panik. untuk mengikuti dan gagal saat pengembang mendorong perubahan mereka), yang paling dipertanyakan bagi saya adalah meneruskan menyatakan jenis yang didefinisikan dalam header lain. Dan saya berhasil mendapatkan basis kode itu hingga 40 menit atau lebih setelah bertahun-tahun melakukan ini dalam langkah-langkah tambahan sedikit ketika mencoba untuk mengurangi "header spaghetti", praktik yang paling dipertanyakan di belakang (seperti membuat saya melupakan sifat dasar dari desain sementara tunnel visioned pada saling ketergantungan header) maju menyatakan jenis didefinisikan dalam header lainnya.
Jika Anda membayangkan
Foo.hpp
tajuk yang memiliki sesuatu seperti:Dan itu hanya menggunakan
Bar
di header cara yang membutuhkannya deklarasi, bukan definisi. maka mungkin tampak seperti no-brainer untuk menyatakanclass Bar;
untuk menghindari membuat definisi yangBar
terlihat di header. Kecuali dalam praktek sering Anda akan menemukan sebagian besar unit kompilasi yang menggunakanFoo.hpp
masih akhirnya perluBar
didefinisikan pula dengan beban tambahan karena harus memasukkanBar.hpp
diri mereka di atasFoo.hpp
, atau Anda mengalami skenario lain di mana itu benar-benar membantu dan 99 % dari unit kompilasi Anda dapat bekerja tanpa termasukBar.hpp
, kecuali kemudian menimbulkan pertanyaan desain yang lebih mendasar (atau setidaknya saya pikir seharusnya saat ini) mengapa mereka perlu melihat deklarasiBar
dan mengapaFoo
bahkan perlu repot untuk mengetahuinya jika itu tidak relevan dengan kebanyakan kasus penggunaan (mengapa membebani desain dengan dependensi ke yang lain yang hampir tidak pernah digunakan?).Karena secara konseptual kita belum benar-benar dipisahkan
Foo
dariBar
. Kami baru saja membuatnya sehingga tajukFoo
tidak perlu banyak info tentang tajukBar
, dan itu tidak hampir sama pentingnya dengan desain yang benar-benar membuat keduanya sepenuhnya independen satu sama lain.Script Tertanam
Ini benar-benar untuk basis kode skala yang lebih besar tetapi teknik lain yang saya temukan sangat berguna adalah dengan menggunakan bahasa skrip tertanam untuk setidaknya bagian paling tinggi dari sistem Anda. Saya menemukan saya dapat menanamkan Lua dalam satu hari dan secara seragam dapat memanggil semua perintah di sistem kami (untungnya perintah itu abstrak, untungnya). Sayangnya saya menemui hambatan di mana para devs tidak mempercayai pengenalan bahasa lain dan, mungkin yang paling aneh, dengan kinerja sebagai kecurigaan terbesar mereka. Namun sementara saya dapat memahami masalah lain, kinerja seharusnya bukan masalah jika kita hanya menggunakan skrip untuk menjalankan perintah ketika pengguna mengklik tombol, misalnya, yang tidak melakukan loop sendiri yang berat (apa yang kami coba lakukan, khawatir tentang perbedaan nanodetik dalam waktu respons untuk klik tombol?).
Contoh
Sementara itu cara paling efektif yang pernah saya saksikan setelah teknik melelahkan untuk mengurangi waktu kompilasi dalam basis kode besar adalah arsitektur yang benar-benar mengurangi jumlah informasi yang diperlukan untuk satu hal dalam sistem untuk bekerja, tidak hanya memisahkan satu header dari yang lain dari kompiler. perspektif tetapi mengharuskan pengguna antarmuka ini untuk melakukan apa yang perlu mereka lakukan sambil mengetahui (baik dari sudut pandang manusia dan kompiler, benar decoupling yang melampaui dependensi kompiler) minimal.
ECS hanyalah salah satu contoh (dan saya tidak menyarankan Anda menggunakan salah satunya), tetapi dengan melihatnya menunjukkan kepada saya bahwa Anda dapat memiliki beberapa basis kode yang benar-benar epik yang masih dibangun dengan sangat cepat sambil dengan senang hati menggunakan templat dan banyak barang lainnya karena ECS, oleh alam, menciptakan arsitektur yang sangat terpisah dimana sistem hanya perlu tahu tentang database ECS dan biasanya hanya segelintir jenis komponen (kadang-kadang hanya satu) untuk melakukan hal mereka:
Desain, Desain, Desain
Dan jenis-jenis desain arsitektur yang terpisah pada tingkat manusia, konseptual ini lebih efektif dalam hal meminimalkan waktu kompilasi daripada teknik apa pun yang saya eksplorasi di atas ketika basis kode Anda tumbuh dan tumbuh dan tumbuh, karena pertumbuhan itu tidak sesuai dengan rata-rata Anda unit kompilasi mengalikan informasi jumlah yang diperlukan pada saat kompilasi dan waktu tautan untuk bekerja (sistem apa pun yang mengharuskan pengembang rata-rata Anda untuk memasukkan banyak hal untuk melakukan apa pun juga mengharuskan mereka dan bukan hanya kompiler untuk mengetahui banyak informasi untuk melakukan apa pun ). Ini juga memiliki lebih banyak manfaat daripada mengurangi waktu pembuatan dan mengurai tajuk, karena itu juga berarti pengembang Anda tidak perlu tahu banyak tentang sistem di luar apa yang diperlukan segera untuk melakukan sesuatu dengannya.
Jika, misalnya, Anda dapat menyewa pengembang fisika ahli untuk mengembangkan mesin fisika untuk game AAA Anda yang menjangkau jutaan LOC, dan ia dapat memulai dengan sangat cepat sambil mengetahui informasi minimum mutlak yang absolut sejauh hal-hal seperti jenis dan antarmuka yang tersedia serta konsep sistem Anda, maka itu akan secara alami diterjemahkan ke mengurangi jumlah informasi untuknya dan kompiler yang diperlukan untuk membangun mesin fisika, dan juga menerjemahkan ke pengurangan besar dalam waktu membangun sementara umumnya menyiratkan bahwa tidak ada yang menyerupai spaghetti di mana saja dalam sistem. Dan itulah yang saya sarankan untuk memprioritaskan di atas semua teknik lain ini: bagaimana Anda merancang sistem Anda. Melelahkan teknik lain akan menjadi icing di atas jika Anda melakukannya sementara, jika tidak,
sumber
Ini masalah pendapat. Lihat jawaban ini dan yang itu . Dan itu juga sangat tergantung pada ukuran proyek (jika Anda yakin Anda akan memiliki jutaan baris sumber dalam proyek Anda, itu tidak sama dengan memiliki beberapa lusin ribu dari mereka).
Berbeda dengan jawaban lain, saya merekomendasikan satu header publik (agak besar) per sub-sistem (yang dapat menyertakan header "pribadi", mungkin memiliki file terpisah untuk implementasi banyak fungsi yang digarisbawahi). Anda bahkan dapat mempertimbangkan tajuk yang hanya memiliki beberapa
#include
arahan.Saya tidak berpikir bahwa banyak file header direkomendasikan. Secara khusus, saya tidak merekomendasikan satu file header per kelas, atau banyak file header kecil masing-masing beberapa lusin baris.
(Jika Anda memiliki banyak file kecil yang besar, Anda harus memasukkan banyak file di setiap unit terjemahan kecil , dan keseluruhan waktu pembangunan mungkin akan berkurang)
Yang Anda inginkan adalah mengidentifikasi, untuk setiap sub-sistem dan file, pengembang utama yang bertanggung jawab untuknya.
Akhirnya, untuk proyek kecil (mis. Kurang dari seratus ribu baris kode sumber), itu tidak terlalu penting. Selama proyek Anda akan cukup mudah untuk melakukan refactor kode dan mengatur ulang dalam file yang berbeda. Anda hanya akan menyalin & menempelkan potongan kode ke file baru (header), bukan masalah besar (yang lebih sulit adalah merancang dengan bijak bagaimana Anda akan mengatur ulang file Anda, dan itu adalah proyek khusus).
(preferensi pribadi saya adalah untuk menghindari file yang terlalu besar dan terlalu kecil; Saya sering memiliki file sumber masing-masing beberapa ribu baris; dan saya tidak takut dengan file header -termasuk definisi fungsi yang diuraikan- dari ratusan baris atau bahkan beberapa ribuan dari mereka)
Perhatikan bahwa jika Anda ingin menggunakan header yang telah dikompilasi sebelumnya dengan GCC (yang kadang - kadang merupakan pendekatan yang masuk akal untuk menurunkan waktu kompilasi) Anda memerlukan file header tunggal (termasuk semua yang lainnya, dan header sistem juga).
Perhatikan bahwa di C ++, file header standar menarik banyak kode . Sebagai contoh
#include <vector>
adalah menarik lebih dari sepuluh ribu baris pada GCC 6 saya di Linux (18100 baris). Dan#include <map>
berkembang hingga hampir 40KLOC. Oleh karena itu, jika Anda memiliki banyak file header kecil termasuk header standar, Anda akhirnya menguraikan ribuan baris selama pembuatan, dan waktu kompilasi Anda akan terganggu. Inilah sebabnya saya tidak suka memiliki banyak baris sumber C ++ kecil (dari beberapa ratus baris paling banyak), tetapi lebih suka memiliki lebih sedikit tetapi lebih besar file C ++ (dari beberapa ribu baris).(sehingga memiliki ratusan file C ++ kecil yang selalu menyertakan -bahkan secara tidak langsung- beberapa file header standar memberi waktu pembuatan yang besar, yang mengganggu pengembang)
Dalam kode C, cukup sering header file meluas ke sesuatu yang lebih kecil, sehingga pertukarannya berbeda.
Lihat juga, untuk inspirasi, ke dalam praktik sebelumnya dalam proyek perangkat lunak gratis yang ada (misalnya di github ).
Perhatikan bahwa dependensi dapat ditangani dengan sistem otomasi build yang baik . Pelajari dokumentasi pembuatan GNU . Waspadai berbagai
-M
flag preprocessor ke GCC (berguna untuk menghasilkan dependensi secara otomatis).Dengan kata lain, proyek Anda (dengan kurang dari seratus file dan selusin pengembang) mungkin tidak cukup besar untuk benar-benar prihatin dengan "header hell", sehingga kekhawatiran Anda tidak dibenarkan . Anda hanya dapat memiliki selusin file header (atau bahkan lebih sedikit), Anda dapat memilih untuk memiliki satu file header per unit terjemahan, Anda bahkan dapat memilih untuk memiliki satu file header tunggal, dan apa pun yang Anda pilih untuk dilakukan tidak akan menjadi "header hell" (dan refactoring dan reorganisasi file Anda akan tetap cukup mudah, sehingga pilihan awal tidak terlalu penting ).
(Jangan memfokuskan upaya Anda pada "sundulan neraka" - yang bukan masalah bagi Anda -, tetapi fokuskanlah mereka untuk merancang arsitektur yang baik)
sumber