OO mejores prácticas para programas en C [cerrado]

19

"Si realmente quieres azúcar OO, ve a usar C ++" , fue la respuesta inmediata que recibí de uno de mis amigos cuando le pregunté esto. Sé que dos cosas están muy mal aquí. Primero OO NO es 'azúcar', y segundo, C ++ NO ha absorbido C.

Necesitamos escribir un servidor en C (el front-end que estará en Python), por lo que estoy explorando mejores formas de administrar grandes programas en C.

El modelado de un sistema grande en términos de objetos e interacciones de objetos lo hace más manejable, mantenible y extensible. Pero cuando intenta traducir este modelo a C que no contiene objetos (y todo lo demás), se enfrenta a algunas decisiones importantes.

¿Crea una biblioteca personalizada para proporcionar las abstracciones OO que necesita su sistema? Cosas como objetos, encapsulación, herencia, polimorfismo, excepciones, pub / sub (eventos / señales), espacios de nombres, introspección, etc. (por ejemplo, GObject o COS ).

O simplemente use las construcciones básicas de C ( structy funciones) para aproximar todas sus clases de objetos (y otras abstracciones) de manera ad-hoc. (por ejemplo, algunas de las respuestas a esta pregunta sobre SO )

El primer enfoque le brinda una forma estructurada de implementar todo su modelo en C. Pero también agrega una capa de complejidad que debe mantener. (Recuerde, la complejidad era lo que queríamos reducir mediante el uso de objetos en primer lugar).

No sé sobre el segundo enfoque, y qué tan efectivo es para aproximar todas las abstracciones que pueda necesitar.

Entonces, mis preguntas simples son: ¿Cuáles son las mejores prácticas para realizar un diseño orientado a objetos en C. Recuerde que no estoy preguntando CÓMO hacerlo. Esta y esta pregunta hablan de ello, e incluso hay un libro sobre esto. Lo que más me interesa son algunos consejos / ejemplos realistas que aborden los problemas reales que aparecen cuando dong esto.

Nota: por favor no aconseje por qué C no debe usarse a favor de C ++. Ya pasamos esa etapa.

codificador de árboles
fuente
3
Puede escribir el servidor C ++ para que su interfaz externa sea extern "C"y pueda usarse desde python. Puede hacerlo manualmente o puede hacer que SWIG lo ayude con eso. Por lo tanto, el deseo de la interfaz de Python no es motivo para no usar C ++. Eso no quiere decir que no haya razones válidas para querer quedarse con C.
Jan Hudec
1
Esta pregunta necesita aclaración. Actualmente, los párrafos 4 y 5 preguntan básicamente qué enfoque se debe tomar, pero luego dice que "no está preguntando CÓMO hacerlo" y en cambio quiere (¿una lista?) De mejores prácticas. Si no está buscando CÓMO hacerlo en C, ¿está solicitando una lista de "mejores prácticas" relacionadas con la OOP en general? Si es así, dígalo, pero tenga en cuenta que la pregunta probablemente se cerrará por ser subjetiva .
Caleb
:) Estoy pidiendo ejemplos reales (código o de otro tipo) donde se ha hecho, y los problemas que encontraron al hacerlo.
treecoder
44
Sus requisitos parecen confusos. Insiste en usar la orientación a objetos sin ninguna razón que pueda ver (en algunos lenguajes, es una forma de hacer que los programas sean más fáciles de mantener, pero no en C), e insiste en usar C. La orientación a objetos es un medio, no un fin o una panacea . Además, se beneficia enormemente con el soporte de idiomas. Si realmente quería OO, debería haberlo considerado durante la selección del idioma. Una pregunta sobre cómo hacer un gran sistema de software con C tendría mucho más sentido.
David Thornley, el
Es posible que desee echar un vistazo a "Diseño y modelado orientado a objetos". (Rumbaugh et al.): Hay una sección sobre el mapeo de diseños OO a idiomas como C.
Giorgio

Respuestas:

16

De mi respuesta a ¿Cómo debo estructurar proyectos complejos en C (no OO sino sobre la gestión de la complejidad en C):

La clave es la modularidad. Esto es más fácil de diseñar, implementar, compilar y mantener.

  • Identifique módulos en su aplicación, como clases en una aplicación OO.
  • Interfaz e implementación separadas para cada módulo, coloque en la interfaz solo lo que necesitan otros módulos. Recuerde que no hay espacio de nombres en C, por lo que debe hacer que todo en sus interfaces sea único (por ejemplo, con un prefijo).
  • Ocultar variables globales en la implementación y usar funciones de acceso para lectura / escritura.
  • No pienses en términos de herencia, sino en términos de composición. Como regla general, no intente imitar C ++ en C, esto sería muy difícil de leer y mantener.

De mi respuesta a ¿Cuáles son las convenciones de nomenclatura típicas para las funciones públicas y privadas de OO C (creo que esta es una mejor práctica):

La convención que uso es:

  • Función pública (en archivo de encabezado):

    struct Classname;
    Classname_functionname(struct Classname * me, other args...);
  • Función privada (estática en el archivo de implementación)

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

Además, muchas herramientas UML pueden generar código C a partir de diagramas UML. Una de código abierto es Topcased .

Mouviciel
fuente
1
Gran respuesta. Apunta a la modularidad. Se supone que OO debe proporcionarlo, pero 1) en la práctica es muy común terminar con OO spaghetti y 2) no es la única forma. Para algunos ejemplos en la vida real, mire el kernel de Linux (modularidad de estilo C) y los proyectos que usan glib (OO de estilo C). He tenido la oportunidad de trabajar con ambos estilos, y la modularidad IMO C-style gana.
Joh
¿Y por qué exactamente la composición es un mejor enfoque que la herencia? Justificación y referencias de apoyo son bienvenidas. ¿O solo te referías a los programas en C?
Aleksandr Blekh
1
@AleksandrBlekh - Sí, me estoy refiriendo solo a C.
Mouviciel
16

Creo que necesita diferenciar OO y C ++ en esta discusión.

Implementar objetos es posible en C, y es bastante fácil: solo cree estructuras con punteros de función. Ese es su "segundo enfoque", y yo lo seguiría. Otra opción sería no utilizar punteros de función en la estructura, sino pasar la estructura de datos a las funciones en llamadas directas, como un puntero de "contexto". Eso es mejor, en mi humilde opinión, ya que es más legible, más fácil de rastrear y permite agregar funciones sin cambiar la estructura (fácil de heredar si no se agregan datos). De hecho, así es como C ++ generalmente implementa el thispuntero.

El polimorfismo se vuelve más complicado porque no hay herencia incorporada y soporte de abstracción, por lo que tendrá que incluir la estructura principal en su clase secundaria o hacer muchas pastas de copia, cualquiera de esas opciones es francamente horrible, aunque técnicamente sencillo. Enorme cantidad de errores esperando a suceder.

Las funciones virtuales se pueden lograr fácilmente a través de punteros de función que apuntan a diferentes funciones según sea necesario, una vez más, muy propensos a errores cuando se realizan manualmente, una gran cantidad de trabajo tedioso para inicializar esos punteros correctamente.

En cuanto a los espacios de nombres, excepciones, plantillas, etc., creo que si está limitado a C, debería renunciar a ellos. He trabajado en escribir OO en C, no lo haría si tuviera una opción (en ese lugar de trabajo, literalmente luché para introducir C ++, y los gerentes estaban "sorprendidos" de lo fácil que era integrarlo con el resto de los módulos C al final).

No hace falta decir que si puede usar C ++, use C ++. No hay una razón real para no hacerlo.

littleadv
fuente
En realidad, puede heredar de una estructura y agregar datos: simplemente declare el primer elemento de la estructura secundaria como una variable cuyo tipo es estructura principal. Luego eche como lo necesite.
mouviciel
1
@mouviciel: sí. Yo dije eso. " ... así que tendrás que incluir la estructura principal en la clase de tu hijo, o ... "
littleadv
55
No hay razón para intentar implementar la herencia. Como un medio para lograr la reutilización del código, es una idea defectuosa para empezar. La composición de objetos es más fácil y mejor.
KaptajnKold
@KaptajnKold: de acuerdo.
littleadv
8

Aquí están los conceptos básicos de cómo crear orientación de objeto en C

1. Crear objetos y encapsular

Por lo general, uno crea un objeto como

object_instance = create_object_typex(parameter);

Los métodos se pueden definir en una de las dos formas aquí.

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

Tenga en cuenta que en la mayoría de los casos, object_instance (or object_instance_private_data)se devuelve es de tipo void *.La aplicación no puede hacer referencia a miembros individuales o funciones de este.

Además de esto, cada método utiliza estas instancia_objeto para el método posterior.

2. Polimorfismo

Podemos usar muchas funciones y punteros de función para anular ciertas funciones en tiempo de ejecución.

por ejemplo, todos los métodos_objetos se definen como un puntero de función que se puede extender a los métodos públicos y privados.

También podemos aplicar la sobrecarga de funciones en un sentido limitado, mediante el uso de var_args que es muy similar a la cantidad variable de argumentos definidos en printf. sí, esto no es tan flexible en C ++, pero esta es la forma más cercana.

3. Definiendo herencia

Definir la herencia es un poco complicado, pero se puede hacer lo siguiente con las estructuras.

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

aquí el doctory engineerse deriva de la persona y es posible escribirlo a un nivel superior, digamos person.

El mejor ejemplo de esto se usa en GObject y los objetos derivados de él.

4. Creación de clases virtuales Cito un ejemplo de la vida real de una biblioteca llamada libjpeg utilizada por todos los navegadores para la decodificación de jpeg. Crea una clase virtual llamada error_manager que la aplicación puede crear instancias concretas y suministrar de nuevo:

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

Tenga en cuenta que JMETHOD se expande en un puntero de función a través de una macro que debe cargarse con los métodos correctos, respectivamente.


He tratado de decir tantas cosas sin demasiadas explicaciones individuales. Pero espero que la gente pueda probar sus propias cosas. Sin embargo, mi intención es solo mostrar cómo se mapean las cosas.

Además, habrá muchos argumentos de que esto no será exactamente una propiedad verdadera del equivalente de C ++. Sé que OO en C no será tan estricto con su definición. Pero trabajando así se entenderían algunos de los principios básicos.

Lo importante no es que OO sea tan estricto como en C ++ y JAVA. Es que uno puede organizar estructuralmente el código con OO pensando y operarlo de esa manera.

Recomiendo encarecidamente a las personas que vean el diseño real de libjpeg y los siguientes recursos

a. Programación orientada a objetos en C
b. Este es un buen lugar donde las personas intercambian ideas
c. y aquí está el libro completo

Dipan Mehta
fuente
3

La orientación a objetos se reduce a tres cosas:

1) Diseño de programa modular con clases autónomas.

2) Protección de datos con encapsulación privada.

3) Herencia / polimorfismo y varias otras sintaxis útiles como constructores / destructores, plantillas, etc.

1 es el más importante con diferencia, y también es completamente independiente del idioma, se trata del diseño del programa. En C lo hace creando "módulos de código" autónomos que consisten en un archivo .h y un archivo .c. Considera esto como el equivalente de una clase OO. Puede decidir qué se debe colocar dentro de este módulo a través del sentido común, UML o cualquier método de diseño OO que esté utilizando para programas C ++.

2 también es bastante importante, no solo para proteger contra el acceso intencional a datos privados, sino también para proteger contra el acceso no intencional, es decir, "desorden del espacio de nombres". C ++ hace esto de formas más elegantes que C, pero aún se puede lograr en C usando la palabra clave estática. Todas las variables que habría declarado como privadas en una clase de C ++ deberían declararse como estáticas en C y ubicarse en el alcance del archivo. Solo son accesibles desde su propio módulo de código (clase). Puede escribir "setters / getters" tal como lo haría en C ++.

3 es útil pero no necesario. Puede escribir programas OO sin herencia o sin constructores / destructores. Es bueno tener estas cosas, ciertamente pueden hacer que los programas sean más elegantes y quizás también más seguros (o lo contrario si se usan descuidadamente). Pero no son necesarios. Como C no admite ninguna de estas útiles funciones, simplemente tendrá que prescindir de ellas. Los constructores pueden ser reemplazados por funciones init / destruct.

La herencia se puede hacer a través de varios trucos de estructura, pero le aconsejaría que no lo haga, ya que probablemente hará que su programa sea más complejo sin ninguna ganancia (la herencia en general debe aplicarse con cuidado, no solo en C sino en cualquier lenguaje).

Finalmente, cada truco de OO se puede hacer en el libro de C. Axel-Tobias Schreiner "Programación orientada a objetos con ANSI C" de principios de los 90 lo demuestra. Sin embargo, no recomendaría ese libro a nadie: agrega una complejidad desagradable y extraña a sus programas en C que simplemente no vale la pena. (El libro está disponible de forma gratuita aquí para aquellos aún interesados ​​a pesar de mi advertencia).

Entonces, mi consejo es implementar 1) y 2) arriba y omitir el resto. Es una forma de escribir programas en C que ha demostrado tener éxito durante más de 20 años.


fuente
2

Tomando prestada algo de experiencia de los diversos tiempos de ejecución de Objective-C, escribir una capacidad de OO dinámica y polimórfica en C no es demasiado difícil (por otro lado, hacer que sea rápido y fácil de usar, parece continuar después de 25 años). Sin embargo, si implementa una capacidad de objeto de estilo Objective-C sin extender la sintaxis del lenguaje, entonces el código con el que termina es bastante desordenado:

  • cada clase está definida por una estructura que declara su superclase, las interfaces a las que se ajusta, los mensajes que implementa (como un mapa de "selector", el nombre del mensaje, a "implementación", la función que proporciona el comportamiento) y la instancia de la clase diseño variable
  • cada instancia está definida por una estructura que contiene un puntero a su clase, luego sus variables de instancia.
  • El envío de mensajes se implementa (más o menos casos especiales) utilizando una función que se parece objc_msgSend(object, selector, …). Al saber de qué clase es una instancia el objeto, puede encontrar la implementación que coincida con el selector y así ejecutar la función correcta.

Todo eso es parte de una biblioteca OO de propósito general diseñada para permitir que múltiples desarrolladores usen y amplíen las clases de los demás, por lo que podría ser excesivo para su propio proyecto. A menudo he diseñado proyectos C como proyectos orientados a clases "estáticas" utilizando estructuras y funciones: - cada clase es la definición de una estructura C que especifica el diseño ivar - cada instancia es solo una instancia de la estructura correspondiente - los objetos no pueden ser "mensajeado", pero las funciones de método similar aMyClass_doSomething(struct MyClass *object, …) se definen . Esto aclara las cosas en el código que el enfoque ObjC pero tiene menos flexibilidad.

Dónde intercambiar mentiras depende de su propio proyecto: parece que otros programadores no usarán sus interfaces C, por lo que la elección se reduce a las preferencias internas. Por supuesto, si decides que quieres algo como la biblioteca objc runtime, entonces hay bibliotecas multiplataforma objc runtime que servirán.


fuente
1

GObject realmente no oculta ninguna complejidad e introduce algo de complejidad propia. Diría que lo ad-hoc es más fácil que GObject a menos que necesite las cosas avanzadas de GObject como señales o la maquinaria de interfaz.

La cosa es ligeramente diferente con COS ya que viene con un preprocesador que extiende la sintaxis C con algunas construcciones OO. Hay un preprocesador similar para GObject, el G Object Builder .

También puede probar el lenguaje de programación Vala , que es un lenguaje completo de alto nivel que se compila en C y de una manera que permite usar las bibliotecas Vala a partir de código C simple. Puede usar GObject, su propio marco de objetos o la forma ad-hoc (con características limitadas).

Jan Hudec
fuente
1

En primer lugar, a menos que sea tarea o no haya un compilador de C ++ para el dispositivo de destino, no creo que tenga que usar C, puede proporcionar una interfaz C con enlace C desde C ++ con bastante facilidad.

En segundo lugar, vería cuánto dependerá del polimorfismo y las excepciones y las otras características que los marcos pueden proporcionar, si no es mucho más simple, las estructuras con funciones relacionadas serán mucho más fáciles que un marco completo, si una porción significativa de su el diseño los necesita, luego muerde la viñeta y usa un marco para que no tenga que implementar las características usted mismo.

Si todavía no tiene un diseño para tomar la decisión, haga un pico y vea lo que le dice el código.

Por último, no tiene por qué ser una opción u otra (aunque si va marco desde el principio, posiblemente también pueda seguir con él), debería ser posible comenzar con estructuras simples para las partes simples y solo agregar bibliotecas según sea necesario.

EDITAR: Por lo tanto, es una decisión de la gerencia correcta ignoremos el primer punto entonces.

jk.
fuente