Buenas prácticas en Ansi C (2)
Tras comentar las buenas prácticas más básicas en Ansi C, veamos ahora las buenas prácticas cuando estamos haciendo una librería, ya sea estática o dinámica.
En concreto, me centraré en la librería estática y luego pasaré a dar algunas pautas para las dinámicas.
Encapsulación
El objetivo de lo que voy a exponer consiste en aislar en la medida de lo posible nuestra librería con el fin de que sea lo más reutilizable posible.
Veremos cómo proteger a nuestra librería del mal programador, así como al mal programador de nuestra librería :D
Hay que diferenciar aquí dos tipos de archivos de cabecera: los internos, utilizados por nuestros archivos .c
, y los externos, que será la forma que tengan otros programas de utilizar nuestra librería.
Macros
Quedan completamente prohibidas las macros en los archivos de cabecera externos.
Hay distintas razones para esto: no forman parte como tales de nuestra librería, no son fácilmente mantenibles, y son más difíciles de depurar.
Enumerados
No es buena idea utilizar enumerados en los archivos de cabecera externos. Sinceramente, me gustaría decir lo contrario, pero dependiendo de las opciones de compilación podemos conseguir verdaderos poltergeist por culpa de los enumerados.
Por lo tanto, para las constantes es mejor utilizar #define
.
En los internos dará igual, porque para cuando lleguen al usuario de nuestra librería ya están compilados.
Sólo lo indispensable
En los archivos de cabecera externos se pondrá exclusivamente lo indispensable:
- Cuanta más funcionalidad exportemos, más tendremos que mantener.
- Cuanto más complejo sea, más preguntas recibiremos.
- Cuanto más sencillo sea, más feliz el usuario de la libreria.
Estructuras
Nunca deis la oportunidad al programador de poder acceder a una estructura de vuestra librería.
Para ello yo suelo utilizar la siguiente técnica: en las cabeceras externas no hay ninguna estructura, pero sí punteros a void; en las funciones que exporto, lo primero que hago es transformar los punteros a void a estructuras internas (que pueden estar en archivos de cabecera internos).
Veamos un ejemplo.
Archivo ejemplo.h
:
|
|
Archivo ejemplo.c
:
|
|
Aquí hay algunas convenciones de nombrado que yo suelo utilizar, aunque seguro que podéis encontrar otras mejores.
Lo primero, es decir que el _t
de la estructura me hace referencia a “tipo”, con el fin de diferenciar lo que es un tipo de lo que es una variable. Se me hace muy raro ver cosas como:
|
|
Después vemos un _tp
que yo utilizo para nombrar mis tipos privados, para diferenciarlos de los que estarán en las cabeceras externas.
Para terminar, en mis métodos suelo llamar a las variables de los tipos públicos con una uve (v
) delante, con el fin de avisarme que ése es el void
.
Getters y setters
Como no dejamos al usuario acceder a nuestra estructura a mano, no podrá realizar modificaciones sobre las variables. Por lo tanto, será necesario utilizar getters y setters para este fin.
Y una vez dicho eso… No debería ser lo habitual utilizar estos getters y setters, ya que la librería no debe ser un almacén de datos, sino una unidad de proceso. Es decir: lo normal no debe ser modificar valores desde fuera, sino desde dentro, como resultado de operaciones. Es evidente que, aun así, siempre necesitaremos algún getter y setter, aunque trataremos de evitarlos.
Es bastante común tener que enviar mucha información de configuración. Ésta puede encapsularse en una estructura pública y recibirla en el constructor.
Orientación a objetos/TADs
En C no tenemos Orientación a objetos, pero sí tenemos TADs (Tipos Abstractos de Datos). Toda librería debería ser un TAD.
Por esta razón, será normal encontrar en nuestras librerías un constructor y un destructor. Antes ya puse un ejemplo de esto.
Si dejamos al usuario acceder a nuestras variables, tarde o temprano tendremos un alto acoplamiento.
Creciendo
Si necesitamos ampliar nuestras estructuras públicas, siempre deberían hacerlo por debajo. De esta manera se seguirá manteniendo compatibilidad hacia atrás.
A menudo suele ser buena idea utilizar un número de versión o el tamaño de la estructura en uno de sus propios campos, con el fin de determinar que estamos utilizando ésa versión. El problema de estas aproximaciones es que dependemos del buen uso que le dé el programador.
Una solución mejor es transformar esa estructura pública en un nuevo TAD que ya manejaremos nosotros, evitando cualquier tipo de imprudencia o descuido.
De todas maneras… ¡¡ya dije que no usárais estructuras públicas!! :P
Librerías dinámicas
Es importante que nuestra librería dinámica mantenga en su archivo de cabecera público tipos de datos con forma de puntero a función para facilitar la vida a quien vaya a usarla, ya que no se puede acceder directamente a las funciones, pero siempre habrá que cargarlas.
En el ejemplo anterior:
|
|
Fachadas
La creación de una librería dinámica es una invitación a la duplicación de código. Todos los programas que quieran utilizarla tendrán que cargar la librería, cargar sus funciones en variables/estructuras internas, realizar casts complejos, gestionar errores, etc.
Por lo tanto, es buena idea proporcionar una librería estática cuya función consista en acceder a la librería dinámica. Es el resultado de aplicar el patrón fachada o facade.
Esto facilitará mucho la vida al usuario. Veamos un ejemplo (continuando con el ejemplo de más arriba, que amplío un poco):
DLL
Archivo ejemplo.h
:
|
|
Archivo ejemplo.c
:
|
|
Lib
Archivo lejemplo.h
:
|
|
Archivo lejemplo.c
:
|
|
Se podría hacer hasta una carga “lazy”, esperando a cargar las funciones en el momento de su utilización, pero eso puede implicar una gestión de errores más compleja (con lo indicado, la gestión de errores está centralizada al inicializar).
Utilizando esta librería, al usuario se le quitan muchos dolores de cabeza, ya que tendría que realizar eso mismo de todas maneras.
Olvidos, descuidos, etc.
Seguramente he olvidado comentar muchísimas cosas, pero creo que ya me he extendido lo suficiente.
Se aceptan sugerencias para completar todos estos consejos.