Кращі практики OO для програм C [закрито]


19

"Якщо ви дійсно хочете OO цукру - перейдіть на C ++" - була негайна відповідь, яку я отримав від одного з своїх друзів, коли я запитав це. Я знаю, що тут помиляються дві речі. По-перше, OO НЕ "цукор", а по-друге, C ++ НЕ поглинає C.

Нам потрібно написати сервер на C (фронт якого буде в Python), і тому я вивчаю кращі способи управління великими програмами на C.

Моделювання великої системи з точки зору об'єктів та об'єктних взаємодій робить її більш керованою, підтримуваною та розширюваною. Але коли ви намагаєтеся перекласти цю модель на C, яка не містить об'єктів (і всього іншого), вам піддаються певні важливі рішення.

Чи створюєте ви власну бібліотеку для надання абстракцій OO, необхідних вашій системі? Такі речі, як об'єкти, інкапсуляція, успадкування, поліморфізм, винятки, паб / суб (події / сигнали), простори імен, самоаналіз тощо (наприклад, GObject або COS ).

Або ви просто використовуєте основні C-конструкції ( structта функції), щоб наблизити всі свої об'єкти-класи (та інші абстракції) спеціальними способами. (наприклад, деякі відповіді на це запитання щодо SO )

Перший підхід дає вам структурований спосіб втілити всю вашу модель в C. Але він також додає шар складності, який вам доведеться підтримувати. (Пам'ятайте, складність полягала в тому, що ми хотіли зменшити, в першу чергу, використовуючи предмети).

Я не знаю про другий підхід, і наскільки він ефективний у наближенні всіх абстракцій, які можуть знадобитися.

Отже, мої прості запитання: Які найкращі практики щодо реалізації об'єктно-орієнтованого дизайну в C. Пам'ятайте, я не прошу, ЯК це зробити. Це та ці питання говорять про це, і навіть є книга про це. Мене більше цікавлять кілька реалістичних порад / прикладів, які стосуються справжніх проблем, які з’являються під час цього.

Примітка. Будь ласка, не радить, чому C не слід використовувати на користь C ++. Ми вже пройшли цей етап.


3
Ви можете написати сервер C ++ так, щоб його зовнішній інтерфейс був extern "C"і може використовуватися з python. Ви можете це зробити вручну, або у вас може допомогти SWIG . Отже, бажання пітонного фронтену - це не привід не використовувати C ++. Це не так сказати, що немає поважних причин хотіти залишитися з C.
Ян Худек

1
Це питання потребує уточнення. Зараз параграфи 4 та 5 в основному запитують, який підхід слід застосовувати, але тоді ви кажете, що «не просите, як це зробити», а натомість хочете (перелік?) Найкращих практик. Якщо ви не шукаєте, ЯК це зробити в C, ви запитуєте список "найкращих практик", пов'язаних з ООП взагалі? Якщо так, то скажіть це, але майте на увазі, що питання, ймовірно, буде закритим для суб'єктивного .
Калеб

:) Я прошу реальних прикладів (код чи іншим чином), де це робилося, - і проблеми, з якими вони стикалися, роблячи це.
treecoder

4
Ваші вимоги здаються заплутаними. Ви наполягаєте на використанні об'єктної орієнтації без будь-якої причини, яку я бачу (деякими мовами це спосіб зробити програми більш рентабельними, але не на C), і наполягаєте на використанні C. Орієнтація на об'єкт - це засіб, а не ціль чи панацея . Більше того, від цього значною мірою користується мовна підтримка. Якщо ви насправді хотіли OO, ви повинні це врахувати під час вибору мови. Питання про те, як зробити велику програмну систему з C, матиме набагато більше сенсу.
Девід Торнлі

Ви можете поглянути на "Об'єктно-орієнтоване моделювання та дизайн". (Румбо та ін.): Є розділ щодо відображення конструкцій ОО на такі мови, як C.
Джорджіо

Відповіді:


16

З моєї відповіді на те, як я повинен структурувати складні проекти в C (не OO, а про управління складністю в C):

Ключ - модульність. Це простіше проектувати, впроваджувати, компілювати та підтримувати.

  • Визначте модулі у вашій програмі, наприклад, класи в додатку OO.
  • Окремий інтерфейс та реалізація для кожного модуля, вкладіть в інтерфейс лише те, що потрібно для інших модулів. Пам'ятайте, що в C немає простору імен, тому вам доведеться зробити все у ваших інтерфейсах унікальним (наприклад, з префіксом).
  • Приховати глобальні змінні в реалізації та використовувати функції аксесуара для читання / запису.
  • Не думайте з точки зору спадкування, а з точки зору складу. Як правило, не намагайтеся імітувати C ++ на C, це було б дуже важко для читання та підтримки.

З моєї відповіді на те, які типові умови іменування публічних та приватних функцій OO C (я думаю, це найкраща практика):

Я використовую умову:

  • Загальнодоступна функція (у файлі заголовка):

    struct Classname;
    Classname_functionname(struct Classname * me, other args...);
  • Приватна функція (статична у файлі реалізації)

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

Крім того, багато інструментів UML здатні генерувати код C з діаграм UML. З відкритим кодом - Topcased .



1
Чудова відповідь. Прагнення до модульності. OO, як передбачається, надає це, але 1) на практиці все занадто часто зустрічається зі спагетті ОО і 2) це не єдиний спосіб. Деякі приклади реального життя подивіться на ядро ​​linux (модульність у стилі C) та проекти, які використовують glib (O-C-style OO). У мене випала нагода працювати з обома стилями, і модульність у стилі IMO перемагає.
Джон

І чому саме композиція є кращим підходом, ніж успадкування? Обґрунтування та допоміжні посилання вітаються. Або ви мали на увазі лише програми C?
Олександр Блех

1
@AleksandrBlekh - Так, я маю на увазі лише C.
mouviciel

16

Я думаю, що вам потрібно розмежувати OO та C ++ у цій дискусії.

Реалізація об'єктів можлива в C, і це досить просто - просто створюйте структури з функціональними вказівниками. Це ваш "другий підхід", і я б пішов з цим. Іншим варіантом було б не використовувати в структурі покажчики функцій, а скоріше передавати структуру даних функціям у прямих викликах як "контекстний" покажчик. Це краще, IMHO, оскільки він є більш читабельним, легше відстежувати та дозволяє додавати функції без зміни структури (легко у спадок, якщо дані не додаються). Це насправді, як C ++ зазвичай реалізує thisпокажчик.

Поліморфізм стає складнішим, оскільки немає вбудованої підтримки успадкування та абстракції, тому вам доведеться або включити батьківську структуру у свій дочірній клас, або зробити багато копій паст, будь-який з цих варіантів є відверто жахливим, хоча технічно просто. Величезна кількість помилок, які чекають цього.

Віртуальних функцій легко досягти за допомогою покажчиків функцій, які вказують на різні функції, на необхідність, знову ж таки - дуже схильні помилки, коли виконуються вручну, чимало копітка робота над ініціалізацією цих покажчиків правильно.

Що стосується просторів імен, винятків, шаблонів тощо - я думаю, що якщо ви обмежуєтесь C, ви просто повинні відмовитися від них. Я працював над написанням ОО в C, не робив би цього, якби у мене був вибір (на тому місці роботи я буквально боровся з тим, щоб запровадити C ++, і менеджери були "здивовані" тим, як легко його інтегрувати з рештою модулів С в кінці.).

Само собою зрозуміло, що якщо ви можете використовувати C ++ - використовуйте C ++. Немає реальної причини цього не робити.


Насправді ви можете успадкувати структуру та додати дані: просто оголосіть перший елемент дочірньої структури як змінну, тип якої - батьківська структура. Потім киньте, як вам потрібно.
mouviciel

1
@mouviciel - так. Я це сказав. " ... тож вам доведеться або включити батьківську структуру у свій дочірній клас, або ... "
littleadv

5
Немає підстав намагатися реалізувати спадщину. Як засіб для досягнення повторного використання коду, для початку це хибна ідея. Об'єктна композиція легша та краща.
KaptajnKold

@KaptajnKold - погоджуюсь.
littleadv

8

Ось основи того, як створити орієнтацію об’єкта в C

1. Створення об'єктів та інкапсуляція

Зазвичай - один створює такий предмет

object_instance = create_object_typex(parameter);

Тут можна визначити один із двох способів.

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

Зауважте, що в більшості випадків object_instance (or object_instance_private_data) повертається типу типу void *.Програма не може посилатися на окремих членів або функцій цього.

Крім цього, кожен метод використовує ці object_in substance для подальшого методу.

2. Поліморфізм

Ми можемо використовувати багато функцій та покажчиків функцій, щоб перекрити певну функціональність під час виконання.

наприклад - всі object_methods визначаються як покажчик функції, який можна поширювати на загальнодоступні, а також приватні методи.

Ми також можемо застосовувати перевантаження функцій у обмеженому сенсі шляхом використання var_args яку дуже схоже на те, як змінна кількість аргументів, визначених у printf. так, це не так гнучко в C ++ - але це найближчий шлях.

3. Визначення спадкування

Визначення спадкування трохи складне, але можна зробити наступне зі структурами.

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); 

тут doctorі engineerє похідне від людини, і можна набрати його на більш високий рівень, скажімоperson .

Найкращий приклад цього використовується в GObject та похідних від нього об'єктах.

4. Створення віртуальних класів Я наводжу приклад із реального життя бібліотекою під назвою libjpeg, яка використовується всіма браузерами для декодування jpeg. Він створює віртуальний клас під назвою error_manager, який додаток може створювати конкретний екземпляр і назад -

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;
};

Зауважте, що JMETHOD розширюється у функціональному вказівнику через макрос, який потрібно завантажувати відповідно правильними методами.


Я намагався сказати стільки речей без занадто багато індивідуальних пояснень. Але я сподіваюся, що люди можуть спробувати свої речі. Однак мій намір просто показати, як все відображається.

Також буде багато аргументів, що це не буде точно справжньою властивістю еквівалента C ++. Я знаю, що ОО в С-не буде таким суворим до свого визначення. Але працюючи так, можна було б зрозуміти деякі основні принципи.

Важливо не те, що ОО є настільки суворим, як у C ++ та JAVA. Це те, що можна структурно організувати код, маючи на увазі ОО, і керувати ним таким чином.

Я настійно рекомендую людям побачити справжній дизайн libjpeg та наступні ресурси

а. Об'єктно-орієнтоване програмування в C
b. це хороше місце, де люди обмінюються ідеями
c. і ось повна книга


3

Орієнтація на об'єкт зводиться до трьох речей:

1) Модульне проектування програми з автономними класами.

2) Захист даних за допомогою приватного інкапсуляції.

3) Спадщина / поліморфізм та інші корисні синтаксиси, такі як конструктори / деструктори, шаблони тощо.

1 є найважливішим на сьогоднішній день, і він також є абсолютно незалежним від мови, все стосується дизайну програми. У C ви робите це, створюючи автономні "кодові модулі", що складаються з одного .h-файлу та одного .c-файлу. Розглядайте це як еквівалент класу ОО. Ви можете вирішити, що слід розмістити всередині цього модуля за допомогою здорового глузду, UML або будь-якого методу дизайну ОО, який ви використовуєте для програм C ++.

2 також є досить важливим не тільки для захисту навмисного доступу до приватних даних, але й для захисту від ненавмисного доступу, тобто "захаращення простору імен". C ++ робить це більш елегантно, ніж C, але це все одно можна досягти в C, використовуючи статичне ключове слово. Усі змінні, які ви оголосили б приватними в класі C ++, повинні бути оголошені статичними в C і розміщені в області файлу. Вони доступні лише з власного модуля коду (класу). Ви можете писати "сетери / геттери" так само, як і в C ++.

3 корисно, але не потрібно. Ви можете писати програми OO без спадкування або без конструкторів / деструкторів. Ці речі приємно мати, вони, безумовно, можуть зробити програми більш витонченими і, можливо, також безпечнішими (або навпаки, якщо недбало їх використовувати). Але вони не потрібні. Оскільки C не підтримує жодної з цих корисних функцій, вам просто доведеться обійтися без них. Конструктори можуть бути замінені функціями init / destruct.

Спадкування можна зробити за допомогою різних хитрощів, але я б радив проти цього, оскільки це, ймовірно, просто зробить вашу програму складнішою без будь-якої вигоди (успадкування взагалі слід обережно застосовувати не лише на С, а на будь-якій мові).

Нарешті, кожен трюк OO можна зробити у книзі К. Акселя-Тобіаса Шрайнера "Об'єктно-орієнтоване програмування з ANSI C" з початку 90-х років. Однак я б не рекомендував нікому цю книгу: вона додає неприємних, дивних складностей вашим програмам C, що просто не вартує метушні. (Книга доступна безкоштовно тут для тих , хто по- як і раніше зацікавлені , незважаючи на моє попередження.)

Тож моя порада здійснити 1) та 2) вище та пропустити решту. Це спосіб написання програм на С, який виявився успішним протягом майже 20 років.


2

Запозичення деякого досвіду різних виробничих завдань Objective-C, написання динамічної, поліморфної здатності OO в C не надто складно (зробити її швидкою та простою у використанні, з іншого боку, все ще триває після 25 років). Однак якщо ви реалізуєте можливість об'єкта в стилі Objective-C, не розширюючи синтаксис мови, тоді код, в якому ви закінчуєтесь, досить безладний:

  • кожен клас визначається структурою, що декларує його надклас, інтерфейси, яким він відповідає, повідомленнями, які він реалізує (як карта "селектора", ім'я повідомлення, "реалізація", функція, що забезпечує поведінку) та екземпляр класу змінний макет.
  • кожен екземпляр визначається структурою, яка містить вказівник на його клас, а потім його змінні екземпляри.
  • передача повідомлень реалізується (надайте або прийміть деякі особливі випадки) за допомогою функції, яка виглядає так objc_msgSend(object, selector, …). Знаючи, для якого класу об'єкт є екземпляром, він може знайти реалізацію, що відповідає селектору, і, таким чином, виконати правильну функцію.

Це все частина бібліотеки OO загального призначення, розробленої для того, щоб дозволити декільком розробникам використовувати та розширювати класи один одного, тому може бути зайвим для вашого власного проекту. Я часто розробляв проекти C як "статичні", орієнтовані на клас проекти, використовуючи структури та функції: - кожен клас - це визначення структури С, що визначає макет ivar - кожен екземпляр є лише екземпляром відповідної структури - об'єкти не можуть бути "обмінюються повідомленнями", але визначені подібні до методу функції, схожі на MyClass_doSomething(struct MyClass *object, …). Це робить коди чіткішими, ніж підхід ObjC, але має меншу гнучкість.

Де розміщувати брехню залежить від вашого власного проекту: це здається, що інші програмісти не використовуватимуть ваші інтерфейси C, тому вибір зводиться до внутрішніх переваг. Звичайно, якщо ви вирішите, що ви хочете щось подібне до бібліотеки виконання objc, тоді існують багатоплатформні бібліотеки виконання objc, які будуть обслуговуватись.


1

GObject насправді не приховує ніяких складностей і вносить певну складність у свою власну. Я б сказав, що спеціальна річ легша, ніж GObject, якщо вам не потрібні вдосконалені речі GObject, такі як сигнали або інтерфейсна техніка.

Справа дещо інша з COS, оскільки вона поставляється з препроцесором, який розширює синтаксис C деякими структурами OO. Існує аналогічний препроцесор для GObject, G Object Builder .

Ви також можете спробувати мову програмування Vala , яка є повноцінною мовою високого рівня, яка компілюється на C і таким чином, що дозволяє використовувати бібліотеки Vala з простого коду С. Він може використовувати або GObject, це власну об'єктну рамку, або спеціальний спосіб (з обмеженими можливостями).


1

По-перше, якщо це домашнє завдання або немає компілятора C ++ для цільового пристрою. Я не думаю, що вам потрібно використовувати C, ви можете забезпечити інтерфейс C із зв'язком C із C ++ досить легко.

По-друге, я хотів би поглянути на те, наскільки ви будете покладатися на поліморфізм та винятки та інші функції, які рамки можуть надати, якщо це не так багато простих структур із пов’язаними функціями, буде набагато простіше, ніж повнофункціональний фреймворк, якщо значна частина вашої дизайн потребує їх, а потім кусає кулю і використовує рамку, щоб вам не довелося самостійно реалізовувати функції.

Якщо у вас ще немає дизайну, щоб прийняти рішення, зробіть шип і подивіться, що вам підказує код.

нарешті, це не повинно бути тим чи іншим вибором (хоча якщо ви запускаєте рамки з самого початку, то, можливо, ви також можете дотримуватися цього), слід починати з простих структур для простих частин і додавати лише в бібліотеках по мірі необхідності.

EDIT: Отже, його рішення керівництвом права дозволяє тоді ігнорувати перший пункт.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.