Najlepsze praktyki OO dla programów C [zamknięte]

19

„Jeśli naprawdę chcesz cukru OO - idź użyć C ++” - to była natychmiastowa odpowiedź, którą otrzymałem od jednego z moich znajomych, kiedy o to zapytałem. Wiem, że dwie rzeczy są tutaj zupełnie nie tak. Pierwszy OO NIE jest „cukrem”, a po drugie C ++ NIE wchłonął C.

Musimy napisać serwer w C (front-end, do którego będzie w Pythonie), dlatego badam lepsze sposoby zarządzania dużymi programami C.

Modelowanie dużego systemu pod kątem obiektów i interakcji między obiektami sprawia, że ​​jest on łatwiejszy w zarządzaniu, utrzymywaniu i rozszerzaniu. Ale kiedy próbujesz przetłumaczyć ten model na C, który nie zawiera obiektów (i wszystkiego innego), musisz podjąć ważne decyzje.

Czy tworzysz niestandardową bibliotekę, aby zapewnić abstrakcje OO, których potrzebuje Twój system? Rzeczy takie jak obiekty, enkapsulacja, dziedziczenie, polimorfizm, wyjątki, pub / sub (zdarzenia / sygnały), przestrzenie nazw, introspekcja itp. (Na przykład GObject lub COS ).

Lub po prostu używasz podstawowych konstrukcji C ( structi funkcji) do aproksymacji wszystkich swoich klas obiektów (i innych abstrakcji). (na przykład niektóre odpowiedzi na to pytanie w SO )

Pierwsze podejście zapewnia ustrukturyzowany sposób implementacji całego modelu w C. Ale dodaje również warstwę złożoności, którą musisz zachować. (Pamiętaj, że złożoność była tym, co chcieliśmy zmniejszyć, używając obiektów w pierwszej kolejności).

Nie wiem o drugim podejściu i jego skuteczności w przybliżeniu wszystkich abstrakcji, których możesz potrzebować.

Tak więc moje proste pytania brzmią: jakie są najlepsze praktyki w realizacji projektowania obiektowego w C. Pamiętaj, że nie pytam JAK to zrobić. To i te pytania mówią o tym, a jest nawet książka na ten temat. Bardziej mnie interesują realistyczne porady / przykłady, które odnoszą się do prawdziwych problemów, które pojawiają się, gdy się to dzieje.

Uwaga: nie rób, dlaczego C nie powinien być używany na rzecz C ++. Minęliśmy już ten etap.

treecoder
źródło
3
Możesz napisać serwer C ++, aby jego zewnętrzny interfejs był extern "C"i mógł być używany z Pythona. Możesz to zrobić ręcznie lub możesz poprosić SWIG o pomoc. Zatem pragnienie interfejsu użytkownika w Pythonie nie jest powodem, aby nie używać C ++. Nie znaczy to, że nie ma uzasadnionych powodów, aby chcieć zostać z C.
Jan Hudec
1
To pytanie wymaga wyjaśnienia. Obecnie ust. 4 i 5 pytają w zasadzie, jakie podejście należy zastosować, ale mówisz, że „nie pytasz JAK to zrobić”, a zamiast tego chcesz (listę?) Najlepszych praktyk. Jeśli nie szukasz JAK to zrobić w C, to czy pytasz o listę „najlepszych praktyk” ogólnie związanych z OOP? Jeśli tak, powiedz to, ale pamiętaj, że pytanie prawdopodobnie zostanie zamknięte z powodu subiektywności .
Caleb,
:) Pytam o prawdziwe przykłady (kod lub w inny sposób), gdzie to zostało zrobione - i problemy, które napotkali, robiąc to.
treekoder
4
Twoje wymagania wydają się mylące. Nalegasz na używanie orientacji obiektowej bez powodu, dla którego nie widzę (w niektórych językach jest to sposób na uczynienie programów łatwiejszymi w utrzymaniu, ale nie w C), i nalegasz na używanie C. Orientacja obiektowa jest środkiem, a nie celem lub panaceum. . Co więcej, jest to bardzo korzystne ze względu na obsługę języków. Jeśli naprawdę chciałeś mieć OO, powinieneś wziąć to pod uwagę podczas wyboru języka. Pytanie o to, jak stworzyć duży system oprogramowania z C, miałoby dużo więcej sensu.
David Thornley,
Możesz rzucić okiem na „Modelowanie i projektowanie obiektowe”. (Rumbaugh i in.): Istnieje rozdział dotyczący mapowania projektów OO na języki takie jak C.
Giorgio

Odpowiedzi:

16

Od mojej odpowiedzi do: Jak powinienem tworzyć złożone projekty w C (nie OO, ale o zarządzaniu złożonością w C):

Kluczem jest modułowość. Jest to łatwiejsze do zaprojektowania, wdrożenia, kompilacji i utrzymania.

  • Zidentyfikuj moduły w swojej aplikacji, takie jak klasy w aplikacji OO.
  • Oddzielny interfejs i implementacja dla każdego modułu, umieść w interfejsie tylko to, czego potrzebują inne moduły. Pamiętaj, że w C nie ma przestrzeni nazw, więc musisz uczynić wszystko w swoich interfejsach unikalnymi (np. Z prefiksem).
  • Ukryj zmienne globalne w implementacji i używaj funkcji akcesora do odczytu / zapisu.
  • Nie myśl w kategoriach dziedziczenia, ale w kategoriach składu. Zasadniczo nie próbuj naśladować C ++ w C, byłoby to bardzo trudne do odczytania i utrzymania.

Od mojej odpowiedzi do Jakie są typowe konwencje nazewnictwa dla funkcji publicznych i prywatnych OO C (myślę, że to najlepsza praktyka):

Konwencja, której używam to:

  • Funkcja publiczna (w pliku nagłówkowym):

    struct Classname;
    Classname_functionname(struct Classname * me, other args...);
  • Funkcja prywatna (statyczna w pliku implementacji)

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

Ponadto wiele narzędzi UML jest w stanie wygenerować kod C na podstawie diagramów UML. Wersja open source to Topcased .

mouviciel
źródło
+1 dla linku Jak powinienem
tworzyć
1
Świetna odpowiedź. Cel na modułowość. OO ma to zapewnić, ale 1) w praktyce zbyt często kończy się spaghetti OO i 2) to nie jedyny sposób. Aby zobaczyć kilka przykładów z prawdziwego życia, spójrz na jądro Linuksa (modułowość w stylu C) i projekty wykorzystujące glib (OO w stylu C). Miałem okazję pracować z oboma stylami, a modułowość IMO w stylu C wygrywa.
Jn
I dlaczego właśnie kompozycja jest lepszym podejściem niż dziedziczenie? Uzasadnienie i referencje są mile widziane. Czy miałeś na myśli tylko programy C?
Aleksandr Blekh
1
@AleksandrBlekh - Tak, mam na myśli tylko C.
mouviciel
16

Myślę, że musisz rozróżnić OO i C ++ w tej dyskusji.

Implementowanie obiektów jest możliwe w C i to całkiem proste - po prostu twórz struktury ze wskaźnikami funkcji. To jest twoje „drugie podejście” i chciałbym się z tym pogodzić. Inną opcją byłoby nie używanie wskaźników funkcji w strukturze, ale raczej przekazywanie struktury danych do funkcji w wywołaniach bezpośrednich, jako wskaźnika „kontekstowego”. To jest lepsze, IMHO, ponieważ jest bardziej czytelne, łatwiejsze do prześledzenia i umożliwia dodawanie funkcji bez zmiany struktury (łatwe w dziedziczeniu, jeśli żadne dane nie zostaną dodane). W rzeczywistości C ++ zazwyczaj implementuje thiswskaźnik.

Polimorfizm staje się bardziej skomplikowany, ponieważ nie ma wbudowanego wsparcia dziedziczenia i abstrakcji, więc musisz albo dołączyć strukturę nadrzędną do klasy dziecka, albo wykonać wiele kopii past, każda z tych opcji jest szczerze mówiąc okropna, chociaż technicznie prosty. Ogromna ilość błędów czeka na wystąpienie.

Funkcje wirtualne można łatwo osiągnąć poprzez wskaźniki funkcji wskazujące na różne funkcje zgodnie z wymaganiami, ponownie - bardzo podatne na błędy, gdy wykonuje się je ręcznie, wiele żmudnej pracy nad prawidłową inicjalizacją tych wskaźników.

Jeśli chodzi o przestrzenie nazw, wyjątki, szablony itp. - Myślę, że jeśli jesteś ograniczony do C - powinieneś po prostu zrezygnować z nich. Pracowałem nad pisaniem OO w C, nie zrobiłbym tego, gdybym miał wybór (w tym miejscu pracy dosłownie walczyłem o wprowadzenie C ++, a menedżerowie byli „zaskoczeni” tym, jak łatwo było to zintegrować z resztą modułów C na końcu.).

Nie trzeba dodawać, że jeśli możesz używać C ++ - użyj C ++. Nie ma prawdziwego powodu, aby tego nie robić.

littleadv
źródło
W rzeczywistości możesz dziedziczyć po strukturze i dodawać dane: po prostu zadeklaruj pierwszy element struktury potomnej jako zmienną, której typem jest struktura nadrzędna. Następnie rzuć, jak potrzebujesz.
mouviciel
1
@mouviciel - tak. Powiedziałem to. „ ... więc będziesz musiał albo włączyć strukturę nadrzędną do swojej klasy dziecięcej, albo ...
littleadv
5
Nie ma powodu, aby próbować wdrożyć dziedziczenie. Jako sposób na ponowne użycie kodu, jest to wadliwy pomysł na początek. Kompozycja obiektów jest łatwiejsza i lepsza.
KaptajnKold
@KaptajnKold - zgadzam się.
littleadv,
8

Oto podstawy tworzenia orientacji obiektowej w C

1. Tworzenie obiektów i enkapsulacja

Zwykle tworzy się obiekt taki jak

object_instance = create_object_typex(parameter);

Metody można zdefiniować tutaj na jeden z dwóch sposobów.

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

Należy pamiętać, że w większości przypadków object_instance (or object_instance_private_data)zwracany jest typ void *.Aplikacja nie może odwoływać się do poszczególnych elementów ani funkcji tego.

Ponadto każda metoda używa tych instancji_obiektu dla kolejnej metody.

2. Polimorfizm

Możemy użyć wielu funkcji i wskaźników funkcji, aby zastąpić niektóre funkcje w czasie wykonywania.

na przykład - wszystkie metody_obiektu są zdefiniowane jako wskaźnik funkcji, który można rozszerzyć na metody publiczne i prywatne.

Możemy również zastosować przeciążenie funkcji w ograniczonym sensie, używając sposobu, var_args który jest bardzo podobny do zmiennej liczby argumentów zdefiniowanych w printf. tak, nie jest to tak elastyczne w C ++ - ale jest to najbliższy sposób.

3. Definiowanie dziedziczenia

Definiowanie dziedziczenia jest nieco trudne, ale można wykonać następujące czynności za pomocą 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); 

tutaj doctori engineerpochodzi od osoby i można to zrobić na wyższym poziomie, powiedzmy person.

Najlepszy przykład tego jest używany w GObject i pochodnych obiektach z niego.

4. Tworzenie wirtualnych klas Cytuję prawdziwy przykład z biblioteki o nazwie libjpeg używanej przez wszystkie przeglądarki do dekodowania jpeg. Tworzy wirtualną klasę o nazwie error_manager, którą aplikacja może utworzyć konkretną instancję i dostarczyć z powrotem -

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

Zauważ, że JMETHOD rozwija się we wskaźniku funkcji przez makro, które należy załadować odpowiednio odpowiednimi metodami.


Próbowałem powiedzieć tak wiele rzeczy bez zbyt wielu indywidualnych wyjaśnień. Ale mam nadzieję, że ludzie będą mogli spróbować swoich własnych rzeczy. Jednak moim zamiarem jest tylko pokazanie, jak rzeczy się mapują.

Ponadto będzie wiele argumentów, że nie będzie to dokładnie prawdziwa właściwość odpowiednika C ++. Wiem, że OO w C-nie będzie tak ścisły w swojej definicji. Ale działając w ten sposób można zrozumieć niektóre podstawowe zasady.

Ważną rzeczą nie jest to, że OO jest tak surowe jak w C ++ i JAVA. Chodzi o to, że można strukturalnie zorganizować kod z myślą OO i obsługiwać go w ten sposób.

Zdecydowanie polecam ludziom zobaczyć prawdziwy projekt libjpeg i śledzić zasoby

za. Programowanie obiektowe w C
b. jest to dobre miejsce, w którym ludzie wymieniają się pomysłami
c. a tu jest pełna książka

Dipan Mehta
źródło
3

Orientacja obiektowa sprowadza się do trzech rzeczy:

1) Modułowy projekt programu z autonomicznymi klasami.

2) Ochrona danych z enkapsulacją prywatną.

3) Dziedziczenie / polimorfizm i różne inne pomocne składnie, takie jak konstruktory / destruktory, szablony itp.

1 jest jak dotąd najważniejszy, a także całkowicie niezależny od języka, chodzi o projektowanie programu. W C robisz to, tworząc autonomiczne „moduły kodu” składające się z jednego pliku .h i jednego pliku .c. Potraktuj to jako odpowiednik klasy OO. Możesz zdecydować, co powinno być umieszczone w tym module poprzez zdrowy rozsądek, UML lub dowolną metodę projektowania OO, której używasz dla programów C ++.

2 jest również bardzo ważne, nie tylko w celu ochrony umyślnego dostępu do prywatnych danych, ale także w celu ochrony przed niezamierzonym dostępem, tj. „Bałaganem przestrzeni nazw”. C ++ robi to w bardziej elegancki sposób niż C, ale nadal można to osiągnąć w C za pomocą słowa kluczowego static. Wszystkie zmienne, które zadeklarowałbyś jako prywatne w klasie C ++, powinny być zadeklarowane jako statyczne w C i umieszczone w zakresie plików. Są one dostępne tylko z poziomu własnego modułu kodu (klasy). Możesz pisać „setters / getters” tak jak w C ++.

3 jest pomocne, ale nie konieczne. Możesz pisać programy OO bez dziedziczenia lub bez konstruktorów / destruktorów. Te rzeczy są miłe, z pewnością mogą uczynić programy bardziej eleganckimi i być może także bezpieczniejszymi (lub odwrotnie, jeśli są używane niedbale). Ale nie są konieczne. Ponieważ C nie obsługuje żadnej z tych pomocnych funkcji, po prostu musisz się bez nich obejść. Konstruktory można zastąpić funkcjami init / destruct.

Dziedziczenia można dokonać za pomocą różnych sztuczek strukturalnych, ale odradzałbym to, ponieważ prawdopodobnie uczyni to twój program bardziej złożonym bez żadnego zysku (dziedziczenie powinno być ostrożnie stosowane, nie tylko w języku C, ale w dowolnym języku).

Wreszcie, każdą sztuczkę OO można wykonać w książce C. Axela-Tobiasa Schreinera „Programowanie obiektowe z ANSI C” z początku lat 90. to potwierdza. Jednak nie poleciłbym tej książki nikomu: dodaje nieprzyjemnej, dziwnej złożoności twoim programom C, która po prostu nie jest warta zamieszania. (Książka jest dostępna tutaj za darmo dla osób, które pomimo mojego ostrzeżenia są nadal zainteresowane).

Radzę więc zastosować 1) i 2) powyżej i pominąć resztę. Jest to sposób pisania programów w C, który od ponad 20 lat sprawdza się.


źródło
2

Pożyczanie doświadczenia z różnych środowisk uruchomieniowych Objective-C, pisanie dynamicznej, polimorficznej funkcji OO w C nie jest zbyt trudne (z drugiej strony wydaje się, że jest nadal w użyciu po 25 latach). Jeśli jednak zaimplementujesz funkcję obiektu w stylu Objective-C bez rozszerzania składni języka, kod, z którym się skończysz, jest dość niechlujny:

  • każda klasa jest zdefiniowana przez strukturę deklarującą swoją nadklasę, interfejsy, z którymi się dostosowuje, komunikaty, które implementuje (jako mapę „selektora”, nazwy komunikatu, „implementacji”, funkcji zapewniającej zachowanie) oraz instancję klasy zmienny układ.
  • każda instancja jest zdefiniowana przez strukturę, która zawiera wskaźnik do swojej klasy, a następnie zmienne instancji.
  • wysyłanie wiadomości jest realizowane (daj lub weź kilka szczególnych przypadków) za pomocą funkcji, która wygląda objc_msgSend(object, selector, …). Wiedząc, jakiej klasy jest obiekt, może znaleźć implementację pasującą do selektora, a tym samym wykonać poprawną funkcję.

To wszystko jest częścią biblioteki OO ogólnego przeznaczenia zaprojektowanej, aby umożliwić wielu programistom wzajemne korzystanie i wzajemne rozszerzanie klas, więc może to być przesada w twoim własnym projekcie. Często projektowałem projekty C jako „statyczne” zorientowane na klasy projekty wykorzystujące struktury i funkcje: - każda klasa jest definicją struktury C określającej układ ivar - każda instancja jest tylko instancją odpowiedniej struktury - obiektów nie można „messaged”, ale MyClass_doSomething(struct MyClass *object, …)zdefiniowane są funkcje podobne do metod . To sprawia, że ​​w kodzie jest bardziej przejrzyste niż podejście ObjC, ale ma mniejszą elastyczność.

Miejsce, w którym kłamstwa zależą od twojego projektu: brzmi to tak, jakby inni programiści nie używali interfejsów C, więc wybór sprowadza się do wewnętrznych preferencji. Oczywiście, jeśli zdecydujesz, że chcesz coś w rodzaju biblioteki wykonawczej objc, wówczas będą dostępne międzyplatformowe biblioteki wykonawcze objc.


źródło
1

GObject tak naprawdę nie kryje żadnej złożoności i wprowadza własną złożoność. Powiedziałbym, że ad-hoc jest łatwiejszy niż GObject, chyba że potrzebujesz zaawansowanych rzeczy GObject, takich jak sygnały lub maszyny interfejsu.

Sprawa jest nieco inna w przypadku COS, ponieważ zawiera preprocesor, który rozszerza składnię C o niektóre konstrukcje OO. Istnieje podobny preprocesor dla GObject, G Object Builder .

Możesz także wypróbować język programowania Vala , który jest pełnym językiem wysokiego poziomu, który kompiluje się do C oraz w sposób umożliwiający korzystanie z bibliotek Vala ze zwykłego kodu C. Może używać GObject, własnej struktury obiektowej lub metody ad-hoc (z ograniczonymi funkcjami).

Jan Hudec
źródło
1

Po pierwsze, chyba że jest to zadanie domowe lub nie ma kompilatora C ++ dla urządzenia docelowego. Nie sądzę, że musisz używać C, możesz dość łatwo zapewnić interfejs C z łączeniem C z C ++.

Po drugie, przyjrzałbym się, jak bardzo będziesz polegać na polimorfizmie i wyjątkach, a także na innych funkcjach zapewnianych przez frameworki, jeśli nie będzie to zbyt proste struktury z powiązanymi funkcjami, będą znacznie łatwiejsze niż w pełni funkcjonalne frameworki, jeśli znaczna część twojego projekt potrzebuje ich, a następnie ugryź pocisk i skorzystaj z frameworka, abyś nie musiał sam implementować tych funkcji.

Jeśli tak naprawdę nie masz jeszcze projektu do podjęcia decyzji, zrób skok i zobacz, co mówi kod.

wreszcie nie musi to być jeden lub inny wybór (chociaż jeśli od samego początku wybierasz framework, prawdopodobnie możesz go trzymać), powinno być możliwe rozpoczęcie od prostych struktur dla prostych części i dodawanie tylko w bibliotekach w razie potrzeby.

EDYCJA: Więc decyzja kierownictwa pozwala zignorować pierwszy punkt.

jk.
źródło