Refaktoring: tablicowanie funkcji
Dawno nic nie pisałem na tę stronę, a że natrafiłem na pewien specyficzny kod, który aż prosi się o poprawę, toteż może kilka słów na temat pewnych mechanizmów, które warto by użyć podczas pisania kodu. Tu uproszczę trochę całość problemu i przedstawię dość schematycznie, po to aby lepiej było widać ideę. Jest sobie funkcja inicjująca (tutaj void main_init()), w której, aby nie była zbyt długa, wywołujemy inne funkcje inicjujące różne rzeczy (tutaj void init_x(), gdzie x jest tu od 1 do 9). Problem jest taki, że mimo wszystko finalnie funkcja i tak ma około 300 linii kodu, dodatkowo funkcje inicjujące zmieniają się w zależności od projektu, reszta jak zauważyłem robiona jest metodą copy/paste.... Nie dość, że łatwo o błędy, funkcja jest niepotrzebnie rozrośnięta, to jeszcze trzeba wszystko tworzyć osobno dla każdego projektu. Tak więc mamy brzydki kod wyglądający:#include <stdio.h> #include <stdio.h> #define DBG_FLAG_INIT 1 void init_1() { printf("Init 1 works\n"); } void init_2() { printf("Init 2 works\n"); } void init_3() { printf("Init 3 works\n"); } void init_4() { printf("Init 4 works\n"); } void init_5() { printf("Init 5 works\n"); } void init_6() { printf("Init 6 works\n"); } void init_7() { printf("Init 7 works\n"); } void init_8() { printf("Init 8 works\n"); } void init_9() { printf("Init 9 works\n"); } void main_init() { printf("before init\n"); init_1(); if (DBG_FLAG_INIT) printf("DBG: init_1 done\n"); init_2(); if (DBG_FLAG_INIT) printf("DBG: init_2 done\n"); init_3(); if (DBG_FLAG_INIT) printf("DBG: init_3 done\n"); init_4(); if (DBG_FLAG_INIT) printf("DBG: init_4 done\n"); init_5(); if (DBG_FLAG_INIT) printf("DBG: init_5 done\n"); init_6(); if (DBG_FLAG_INIT) printf("DBG: init_6 done\n"); init_7(); if (DBG_FLAG_INIT) printf("DBG: init_7 done\n"); init_8(); if (DBG_FLAG_INIT) printf("DBG: init_8 done\n"); init_9(); if (DBG_FLAG_INIT) printf("DBG: init_9 done\n"); printf("after init\n"); } int main() { main_init(); return 0; }Oczywiście to co jest w funkcjach init_x jest w rzeczywistości inne niż tu pokazane, ale chodzi o przedstawienie problemu. Jak widać aż chciałoby się wywołać te funkcje inicjujące w pętli. Co będziemy się zastanawiać, zróbmy to:
typedef void (*Tinit_func)(); void main_init() { const Tinit_func c_init_func_arr[] = {init_1, init_2, init_3, init_4, init_5, init_6, init_7, init_8, init_9}; int lcnt = sizeof(c_init_func_arr)/sizeof(Tinit_func); int i; printf("before init\n"); for (i = 0; i < lcnt; ++i) c_init_func_arr[i](); printf("after init\n"); }Jest prawie tak samo, ale brakuje nam jeszcze informacji wypisywanej przy załączonej opcji z debuglogiem, więc zmieńmy to:
typedef void (*Tinit_func)(); void main_init() { const Tinit_func c_init_func_arr[] = {init_1, init_2, init_3, init_4, init_5, init_6, init_7, init_8, init_9}; const char * c_initfunc_names_arr[] = {"init_1", "init_2", "init_3", "init_4", "init_5", "init_6", "init_7", "init_8", "init_9"}; int lcnt = sizeof(c_init_func_arr)/sizeof(Tinit_func); int i; printf("before init\n"); for (i = 0; i < lcnt; ++i) { c_init_func_arr[i](); if (DBG_FLAG_INIT) printf("DBG: %s done\n", c_initfunc_names_arr[i]); } printf("after init\n"); }Działa tak samo, ale pewien niesmak pozostaje, wprowadzona dodatkowa tablica.... Hmmm może by jakoś to scalić?
typedef void (*Tinit_func)(); #define KOD_INI_FUNC(a) {a,#a} struct Tinit_func_str { Tinit_func func; char *name; }; void main_init() { const struct Tinit_func_str c_init_func_arr[] = {KOD_INI_FUNC(init_1), KOD_INI_FUNC(init_2), KOD_INI_FUNC(init_3), KOD_INI_FUNC(init_4), KOD_INI_FUNC(init_5), KOD_INI_FUNC(init_6), KOD_INI_FUNC(init_7), KOD_INI_FUNC(init_8), KOD_INI_FUNC(init_9)}; int lcnt = sizeof(c_init_func_arr)/sizeof(struct Tinit_func_str); int i; printf("before init\n"); for (i = 0; i < lcnt; ++i) { c_init_func_arr[i].func(); if (DBG_FLAG_INIT) printf("DBG: %s done\n", c_init_func_arr[i].name); } printf("after init\n"); }No to mamy już ładną strukturę - jak to w piosence możemy usłyszeć "jest dobrze, ale nie najgorzej jest". Brakuje nam jeszcze najważniejszego: pewnej uniwersalności, tak by mieć przetestowaną funkcję, do której będziemy tylko podawać co ma zainicjować, gdyż przypominam, że w różnych projektach funkcja ma wywoływać różne zestawy funkcji. Tak więc stwórzmy sobie w osobnym pliku:
#include "initcodelib.h" #define DBG_FLAG_INIT 1 void main_init(const struct Tinit_func_str *ainit_func_arr, size_t acnt) { int i; printf("before init\n"); for (i = 0; i < acnt; ++i) { ainit_func_arr[i].func(); if (DBG_FLAG_INIT) printf("DBG: %s done\n", ainit_func_arr[i].name); } printf("after init\n"); }natomiast zmodyfikujmy naszego maina w ten sposób:
int main() { const struct Tinit_func_str c_init_func_arr[] = {KOD_INI_FUNC(init_1), KOD_INI_FUNC(init_2), KOD_INI_FUNC(init_3), KOD_INI_FUNC(init_4), KOD_INI_FUNC(init_5), KOD_INI_FUNC(init_6), KOD_INI_FUNC(init_7), KOD_INI_FUNC(init_8), KOD_INI_FUNC(init_9)}; int lcnt = sizeof(c_init_func_arr)/sizeof(struct Tinit_func_str); main_init(c_init_func_arr, lcnt); return 0; }I teraz w zależności od projektu zmieniać nam się będzie plik główny programu, natomiast funkcję inicjującą mamy już krótką i przetestowaną! W razie występujących problemów, będzie to ostatnie miejsce, do którego będziemy musieli zajrzeć, gdyż z dużym prawdopodobieństwem problem będzie w innej części kodu. Oczywiście, można by pomyśleć jeszcze czy flaga, czy mają być wyświetlane informacje dla debuggera, czy nie, ma być stałą, czy może parametryzować to w zależności od sposobu wywołania programu - to jest coś co tez warto rozważyć, ale zostawiam to Wam, drodzy czytelnicy, jako "pracę domową". Dla chętnych do pobrania przykładowe kody do testów
refactoring język C tablica funkcji