OO praktik terbaik untuk program C [ditutup]

19

"Jika Anda benar-benar ingin gula OO - gunakan C ++" - adalah tanggapan langsung yang saya dapatkan dari salah satu teman saya ketika saya menanyakan hal ini. Saya tahu ada dua hal yang salah di sini. OO pertama BUKAN 'gula', dan kedua, C ++ TIDAK menyerap C.

Kita perlu menulis server dalam C (front-end yang akan menggunakan Python), dan jadi saya mencari cara yang lebih baik untuk mengelola program C besar.

Pemodelan sistem besar dalam hal objek, dan interaksi objek membuatnya lebih mudah dikelola, dipelihara, dan diperluas. Tetapi ketika Anda mencoba menerjemahkan model ini ke dalam C yang tidak mengandung objek (dan yang lainnya), Anda ditantang dengan beberapa keputusan penting.

Apakah Anda membuat perpustakaan khusus untuk memberikan abstraksi OO yang dibutuhkan sistem Anda? Hal-hal seperti objek, enkapsulasi, pewarisan, polimorfisme, pengecualian, pub / sub (peristiwa / sinyal), ruang nama, introspeksi, dll. (Misalnya GObject atau COS ).

Atau, Anda cukup menggunakan konstruksi C dasar ( structdan fungsi) untuk memperkirakan semua kelas objek Anda (dan abstraksi lainnya) dengan cara ad-hoc. (misalnya, beberapa jawaban untuk pertanyaan ini di SO )

Pendekatan pertama memberi Anda cara terstruktur untuk mengimplementasikan seluruh model Anda dalam C. Tetapi juga menambahkan lapisan kompleksitas yang harus Anda pertahankan. (Ingat, kompleksitas adalah apa yang ingin kita kurangi dengan menggunakan objek sejak awal).

Saya tidak tahu tentang pendekatan kedua, dan seberapa efektif pendekatan ini untuk mendekati semua abstraksi yang mungkin Anda butuhkan.

Jadi, pertanyaan sederhana saya adalah: Apa praktik terbaik dalam mewujudkan desain berorientasi objek di C. Ingat saya tidak meminta BAGAIMANA melakukannya. Ini dan pertanyaan ini membicarakannya, dan bahkan ada buku tentang ini. Yang saya lebih tertarik adalah beberapa saran / contoh realistis yang membahas masalah nyata yang muncul ketika dong ini.

Catatan: tolong jangan menyarankan mengapa C tidak boleh digunakan untuk C ++. Kami telah melewati tahap itu.

treecoder
sumber
3
Anda dapat menulis server C ++ sehingga antarmuka eksternal extern "C"dan dapat digunakan dari python. Anda dapat melakukannya secara manual atau Anda dapat meminta SWIG membantu Anda. Jadi keinginan untuk python frontend bukan alasan untuk tidak menggunakan C ++. Itu tidak mengatakan bahwa tidak ada alasan yang sah untuk tetap tinggal dengan C.
Jan Hudec
1
Pertanyaan ini perlu diklarifikasi. Saat ini, paragraf 4 dan 5 pada dasarnya bertanya pendekatan apa yang harus diambil seseorang, tetapi kemudian Anda mengatakan Anda "tidak meminta BAGAIMANA melakukannya" dan alih-alih menginginkan (daftar?) Praktik terbaik. Jika Anda tidak mencari BAGAIMANA melakukannya di C, apakah Anda kemudian meminta daftar "praktik terbaik" yang terkait dengan OOP secara umum? Jika demikian, katakan itu, tetapi ketahuilah bahwa pertanyaan tersebut kemungkinan akan ditutup karena bersifat subjektif .
Caleb
:) Saya meminta contoh nyata (kode atau lainnya) di mana hal itu dilakukan - dan masalah yang mereka temui ketika melakukannya.
treecoder
4
Persyaratan Anda tampaknya membingungkan. Anda bersikeras menggunakan orientasi objek tanpa alasan yang dapat saya lihat (dalam beberapa bahasa, ini adalah cara untuk membuat program lebih mudah dikelola, tetapi tidak dalam C), dan bersikeras menggunakan C. Orientasi objek adalah cara, bukan tujuan atau obat mujarab . Selain itu, sangat diuntungkan oleh dukungan bahasa. Jika Anda benar-benar menginginkan OO, Anda harus mempertimbangkan itu selama pemilihan bahasa. Sebuah pertanyaan tentang bagaimana membuat sistem perangkat lunak besar dengan C akan jauh lebih masuk akal.
David Thornley
Anda mungkin ingin melihat "Pemodelan dan Desain Berorientasi Objek." (Rumbaugh et al.): Ada bagian tentang pemetaan desain OO ke bahasa seperti C.
Giorgio

Jawaban:

16

Dari jawaban saya untuk Bagaimana saya harus menyusun proyek kompleks di C (bukan OO tetapi tentang mengelola kompleksitas dalam C):

Kuncinya adalah modularitas. Ini lebih mudah untuk merancang, mengimplementasikan, menyusun dan memelihara.

  • Identifikasi modul di aplikasi Anda, seperti kelas di aplikasi OO.
  • Pisahkan antarmuka dan implementasi untuk setiap modul, masukkan hanya antarmuka apa yang dibutuhkan oleh modul lain. Ingatlah bahwa tidak ada namespace di C, jadi Anda harus membuat segala sesuatu di antarmuka Anda unik (misalnya, dengan awalan).
  • Sembunyikan variabel global dalam implementasi dan gunakan fungsi accessor untuk membaca / menulis.
  • Jangan berpikir dalam hal warisan, tetapi dalam hal komposisi. Sebagai aturan umum, jangan mencoba meniru C ++ dalam C, ini akan sangat sulit untuk dibaca dan dipelihara.

Dari jawaban saya untuk Apa konvensi penamaan yang khas untuk fungsi publik dan pribadi OO C (saya pikir ini adalah praktik terbaik):

Konvensi yang saya gunakan adalah:

  • Fungsi publik (dalam file header):

    struct Classname;
    Classname_functionname(struct Classname * me, other args...);
  • Fungsi pribadi (statis dalam file implementasi)

    static functionname(struct Classname * me, other args...)

Selain itu, banyak alat UML dapat menghasilkan kode C dari diagram UML. Sumber terbuka adalah Topcased .

mouviciel
sumber
1
Jawaban yang bagus Bertujuan untuk modularitas. OO seharusnya menyediakannya, tetapi 1) dalam praktiknya terlalu umum untuk berakhir dengan OO spaghetti dan 2) itu bukan satu-satunya cara. Untuk beberapa contoh dalam kehidupan nyata, lihat kernel linux (modularitas C-style) dan proyek-proyek yang menggunakan glib (C-style OO). Saya memiliki kesempatan untuk bekerja dengan kedua gaya, dan modularitas IMO C-style menang.
Yoh
Dan mengapa komposisi sebenarnya merupakan pendekatan yang lebih baik daripada pewarisan? Rasional dan referensi pendukung dipersilakan. Atau Anda hanya merujuk ke program C?
Aleksandr Blekh
1
@AlexandrBlekh - Ya saya hanya mengacu pada C.
mouviciel
16

Saya pikir Anda perlu membedakan OO dan C ++ dalam diskusi ini.

Mengimplementasikan objek dimungkinkan dalam C, dan itu cukup mudah - cukup buat struktur dengan pointer fungsi. Itu "pendekatan kedua" Anda, dan saya akan melakukannya. Pilihan lain adalah tidak menggunakan pointer fungsi di struct, melainkan meneruskan struct data ke fungsi dalam panggilan langsung, sebagai pointer "konteks". Itu lebih baik, IMHO, karena lebih mudah dibaca, lebih mudah dilacak, dan memungkinkan penambahan fungsi tanpa mengubah struct (mudah pada warisan jika tidak ada data yang ditambahkan). Itu sebenarnya bagaimana C ++ biasanya mengimplementasikan thispointer.

Polimorfisme menjadi lebih rumit karena tidak ada warisan bawaan dan dukungan abstraksi, jadi Anda harus memasukkan struct induk ke dalam kelas anak Anda, atau melakukan banyak copy paste, salah satu dari opsi-opsi itu benar-benar mengerikan, walaupun secara teknis sederhana. Sejumlah besar bug menunggu untuk terjadi.

Fungsi virtual dapat dengan mudah dicapai melalui pointer fungsi yang menunjuk ke fungsi yang berbeda seperti yang diperlukan, sekali lagi - sangat rawan bug ketika dilakukan secara manual, banyak pekerjaan yang membosankan dalam menginisialisasi pointer tersebut dengan benar.

Mengenai ruang nama, pengecualian, template, dll - Saya pikir jika Anda terbatas pada C - Anda harus menyerah saja. Saya telah bekerja menulis OO dalam C, tidak akan melakukannya jika saya punya pilihan (di tempat kerja itu saya benar-benar berjuang untuk memperkenalkan C ++, dan para manajer "terkejut" betapa mudahnya mengintegrasikannya. dengan sisa modul C di akhir.).

Tak perlu dikatakan lagi jika Anda bisa menggunakan C ++ - gunakan C ++. Tidak ada alasan untuk tidak melakukannya.

littleadv
sumber
Sebenarnya, Anda bisa mewarisi dari sebuah struct dan menambahkan data: cukup deklarasikan item pertama dari child struct sebagai variabel yang tipenya adalah parent struct. Kemudian gips sesuai kebutuhan Anda.
mouviciel
1
@mouviciel - ya. Saya mengatakan itu. " ... jadi kamu harus memasukkan struct induk ke dalam kelas anakmu, atau ... "
littleadv
5
Tidak ada alasan untuk mencoba menerapkan warisan. Sebagai cara untuk mencapai penggunaan kembali kode, itu adalah ide yang cacat untuk memulai. Komposisi objek lebih mudah dan lebih baik.
KaptajnKold
@KaptajnKold - setuju.
littleadv
8

Berikut adalah dasar-dasar cara membuat orientasi objek di C

1. Membuat Objek dan Enkapsulasi

Biasanya - satu menciptakan objek seperti

object_instance = create_object_typex(parameter);

Metode dapat didefinisikan dalam salah satu dari dua cara di sini.

object_type_method_function(object_instance,parameter1)
OR
object_instance->method_function(object_instance_private_data,parameter1)

Perhatikan bahwa dalam kebanyakan kasus, object_instance (or object_instance_private_data)dikembalikan adalah tipevoid *. . Aplikasi tidak dapat menjadi referensi anggota individu atau fungsi ini.

Lebih jauh dari ini setiap metode menggunakan object_instance ini untuk metode selanjutnya.

2. Polimorfisme

Kita dapat menggunakan banyak fungsi dan fungsi pointer untuk mengesampingkan fungsionalitas tertentu dalam waktu berjalan.

misalnya - semua object_methods didefinisikan sebagai pointer fungsi yang dapat diperluas ke metode publik maupun pribadi.

Kita juga dapat menerapkan fungsi overloading dalam arti terbatas, dengan cara menggunakan var_args yang sangat mirip dengan bagaimana jumlah variabel argumen didefinisikan dalam printf. ya, ini tidak sefleksibel dalam C ++ - tapi ini cara terdekat.

3. Menentukan warisan

Mendefinisikan pewarisan agak sulit tetapi seseorang dapat melakukan hal berikut dengan struktur.

typedef struct { 
     int age,
     int sex,
} person; 

typedef struct { 
     person p,
     enum specialty s;
} doctor;

typedef struct { 
     person p,
     enum subject s;
} engineer;

// use it like
engineer e1 = create_engineer(); 
get_person_age( (person *)e1); 

di sini doctordan engineerditurunkan dari orang dan dimungkinkan untuk mengetikkannya ke tingkat yang lebih tinggi, katakanlah person.

Contoh terbaik dari ini digunakan dalam GObject dan objek yang diturunkan dari itu

4. Membuat kelas virtual Saya mengutip contoh kehidupan nyata oleh perpustakaan bernama libjpeg yang digunakan oleh semua browser untuk decoding jpeg. Ini menciptakan kelas virtual yang disebut error_manager dimana aplikasi dapat membuat contoh konkret dan memasok kembali -

struct djpeg_dest_struct {
  /* start_output is called after jpeg_start_decompress finishes.
   * The color map will be ready at this time, if one is needed.
   */
  JMETHOD(void, start_output, (j_decompress_ptr cinfo,
                               djpeg_dest_ptr dinfo));
  /* Emit the specified number of pixel rows from the buffer. */
  JMETHOD(void, put_pixel_rows, (j_decompress_ptr cinfo,
                                 djpeg_dest_ptr dinfo,
                                 JDIMENSION rows_supplied));
  /* Finish up at the end of the image. */
  JMETHOD(void, finish_output, (j_decompress_ptr cinfo,
                                djpeg_dest_ptr dinfo));

  /* Target file spec; filled in by djpeg.c after object is created. */
  FILE * output_file;

  /* Output pixel-row buffer.  Created by module init or start_output.
   * Width is cinfo->output_width * cinfo->output_components;
   * height is buffer_height.
   */
  JSAMPARRAY buffer;
  JDIMENSION buffer_height;
};

Perhatikan di sini bahwa JMETHOD berkembang dalam penunjuk fungsi melalui makro yang perlu dimuat dengan metode yang benar masing-masing.


Saya telah mencoba mengatakan banyak hal tanpa terlalu banyak penjelasan pribadi. Tapi saya berharap orang bisa mencoba barang sendiri. Namun, maksud saya hanya untuk menunjukkan bagaimana hal-hal dipetakan.

Juga, akan ada banyak argumen bahwa ini bukan properti yang benar yang setara dengan C ++. Saya tahu bahwa OO di C-tidak akan seketat itu definisi. Tetapi bekerja seperti itu akan memahami beberapa prinsip inti.

Yang penting bukan bahwa OO seketat di C ++ dan JAVA. Ini adalah bahwa seseorang dapat secara struktural mengatur kode dengan pemikiran OO dalam pikiran dan mengoperasikannya seperti itu.

Saya akan sangat menyarankan orang untuk melihat desain libjpeg yang sebenarnya dan sumber daya berikut

Sebuah. Pemrograman berorientasi objek dalam C
b. ini adalah tempat yang baik di mana orang bertukar ide
c. dan inilah buku lengkapnya

Dipan Mehta
sumber
3

Orientasi objek bermuara pada tiga hal:

1) Desain program modular dengan kelas otonom.

2) Perlindungan data dengan enkapsulasi pribadi.

3) Warisan / polimorfisme dan berbagai sintaks membantu lainnya seperti konstruktor / destruktor, template dll.

Sejauh ini, 1 adalah yang paling penting, dan juga sepenuhnya bebas bahasa, ini semua tentang desain program. Dalam C Anda melakukan ini dengan membuat "modul kode" otonom yang terdiri dari satu file .h dan satu file .c. Anggap ini setara dengan kelas OO. Anda dapat memutuskan apa yang harus ditempatkan di dalam modul ini melalui akal sehat, UML atau metode apa pun dari desain OO yang Anda gunakan untuk program C ++.

2 juga cukup penting, tidak hanya untuk melindungi akses sengaja yang disengaja ke data pribadi, tetapi juga untuk melindungi terhadap akses yang tidak disengaja, yaitu "namespace clutter". C ++ melakukan ini dengan cara yang lebih elegan daripada C, tetapi masih dapat dicapai dalam C dengan menggunakan kata kunci statis. Semua variabel yang Anda nyatakan sebagai pribadi dalam kelas C ++, harus dinyatakan sebagai statis di C dan ditempatkan pada ruang lingkup file. Mereka hanya dapat diakses dari dalam modul kode mereka sendiri (kelas). Anda dapat menulis "setter / getter" seperti yang Anda lakukan di C ++.

3 bermanfaat tetapi tidak perlu. Anda dapat menulis program OO tanpa warisan atau tanpa konstruktor / penghancur. Hal-hal ini baik untuk dimiliki, mereka pasti dapat membuat program lebih elegan dan mungkin juga lebih aman (atau sebaliknya jika digunakan dengan sembarangan). Tetapi mereka tidak perlu. Karena C tidak mendukung kedua fitur bermanfaat ini, Anda hanya perlu melakukannya tanpa mereka. Konstruktor dapat diganti dengan fungsi init / destruct.

Warisan dapat dilakukan melalui berbagai trik struct, tetapi saya akan menyarankan untuk tidak melakukannya, karena itu mungkin hanya akan membuat program Anda lebih kompleks tanpa keuntungan (warisan secara umum harus diterapkan dengan hati-hati, tidak hanya dalam bahasa C tetapi dalam bahasa apa pun).

Akhirnya, setiap trik OO dapat dilakukan dalam buku C. Axel-Tobias Schreiner "pemrograman berorientasi objek dengan ANSI C" dari awal 90-an membuktikan ini. Namun, saya tidak akan merekomendasikan buku itu kepada siapa pun: itu menambah kompleksitas, tidak menyenangkan aneh untuk program C Anda yang tidak sepadan dengan keributan. (Buku ini tersedia gratis di sini untuk mereka yang masih tertarik meskipun saya sudah diperingatkan.)

Jadi saran saya adalah menerapkan 1) dan 2) di atas dan melewati sisanya. Ini adalah cara penulisan program C yang telah terbukti berhasil selama lebih dari 20 tahun.


sumber
2

Meminjam beberapa pengalaman dari berbagai runtime Objective-C, menulis kemampuan OO polimorfik yang dinamis dalam C tidak terlalu sulit (membuatnya cepat dan mudah digunakan, di sisi lain, tampaknya masih berlangsung setelah 25 tahun). Namun, jika Anda menerapkan kemampuan objek gaya Objective-C tanpa memperluas sintaks bahasa maka kode yang Anda gunakan cukup berantakan:

  • setiap kelas didefinisikan oleh struktur yang mendeklarasikan superclass-nya, antarmuka yang sesuai dengannya, pesan yang diimplementasikan (sebagai peta "selector", nama pesan, hingga "implementasi", fungsi yang menyediakan perilaku) dan instance kelas tata letak variabel.
  • setiap instance didefinisikan oleh struktur yang berisi pointer ke kelasnya, lalu variabel instansinya.
  • pengiriman pesan diimplementasikan (memberi atau menerima beberapa kasus khusus) menggunakan fungsi yang mirip objc_msgSend(object, selector, …). Dengan mengetahui kelas apa objek adalah instance, ia dapat menemukan implementasi yang cocok dengan pemilih dan dengan demikian menjalankan fungsi yang benar.

Itu semua adalah bagian dari pustaka tujuan umum OO yang dirancang untuk memungkinkan banyak pengembang untuk menggunakan dan memperluas kelas masing-masing, sehingga bisa jadi berlebihan untuk proyek Anda sendiri. Saya sering mendesain proyek C sebagai proyek "statis" yang berorientasi pada kelas dengan menggunakan struktur dan fungsi: - setiap kelas adalah definisi dari struktur C yang menetapkan tata letak ivar - setiap contoh hanyalah contoh dari struktur yang sesuai - objek tidak dapat "Pesan", tetapi fungsi seperti metode terlihat sepertiMyClass_doSomething(struct MyClass *object, …) didefinisikan. Ini membuat hal-hal lebih jelas dalam kode daripada pendekatan ObjC tetapi kurang fleksibel.

Tempat trade-off terletak tergantung pada proyek Anda sendiri: kedengarannya seperti programmer lain tidak akan menggunakan antarmuka C Anda sehingga pilihan turun ke preferensi internal. Tentu saja, jika Anda memutuskan ingin sesuatu seperti perpustakaan runtime objc, maka ada perpustakaan runtime objc lintas-platform yang akan melayani.


sumber
1

GObject tidak benar-benar menyembunyikan kompleksitas apa pun dan memperkenalkan beberapa kompleksitasnya sendiri. Saya akan mengatakan bahwa hal ad-hoc lebih mudah daripada GObject kecuali Anda memerlukan hal-hal GObject canggih seperti sinyal atau mesin antarmuka.

Masalahnya sedikit berbeda dengan COS karena yang datang dengan preprocessor yang memperluas sintaks C dengan beberapa konstruksi OO. Ada preprocessor serupa untuk GObject, para G Object Builder .

Anda juga bisa mencoba bahasa pemrograman Vala , yang merupakan bahasa tingkat tinggi penuh yang dikompilasi ke C dan dengan cara yang memungkinkan menggunakan perpustakaan Vala dari kode C biasa. Ia dapat menggunakan GObject, kerangka objeknya sendiri atau cara ad-hoc (dengan fitur terbatas).

Jan Hudec
sumber
1

Pertama, kecuali ini adalah pekerjaan rumah atau tidak ada kompiler C ++ untuk perangkat target Saya tidak berpikir Anda harus menggunakan C, Anda dapat menyediakan antarmuka C dengan tautan C dari C ++ dengan cukup mudah.

Kedua, saya akan melihat seberapa besar Anda akan bergantung pada polimorfisme dan pengecualian dan fitur-fitur lain yang dapat disediakan kerangka kerja, jika tidak banyak struct sederhana dengan fungsi terkait akan jauh lebih mudah daripada kerangka berfitur lengkap, jika sebagian besar dari Anda desain membutuhkan mereka kemudian menggigit peluru dan menggunakan kerangka kerja sehingga Anda tidak harus mengimplementasikan fitur sendiri.

Jika Anda belum benar-benar memiliki desain untuk membuat keputusan, maka lakukan spike dan lihat apa yang dikatakan kode tersebut kepada Anda.

terakhir itu tidak harus menjadi satu atau pilihan lain (meskipun jika Anda pergi kerangka kerja dari awal Anda mungkin juga tetap dengan itu), harus dimungkinkan untuk memulai dengan struct sederhana untuk bagian-bagian sederhana dan hanya menambahkan perpustakaan sesuai kebutuhan.

EDIT: Jadi keputusan oleh manajemen yang tepat mari kita abaikan poin pertama.

jk.
sumber